Merge branch 'dev' into dev
This commit is contained in:
@@ -21,8 +21,12 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
router.push(`/console/${value}`);
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
window['umami'].track({ url: '/page-view', referrer: 'https://www.google.com' });
|
||||
function handleRunScript() {
|
||||
window['umami'].track(props => ({
|
||||
...props,
|
||||
url: '/page-view',
|
||||
referrer: 'https://www.google.com',
|
||||
}));
|
||||
window['umami'].track('track-event-no-data');
|
||||
window['umami'].track('track-event-with-data', {
|
||||
test: 'test-data',
|
||||
@@ -44,7 +48,7 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleIdentifyClick() {
|
||||
function handleRunIdentify() {
|
||||
window['umami'].identify({
|
||||
userId: 123,
|
||||
name: 'brian',
|
||||
@@ -145,10 +149,10 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
||||
</div>
|
||||
<div className={styles.group}>
|
||||
<div className={styles.header}>Javascript events</div>
|
||||
<Button id="manual-button" variant="primary" onClick={handleClick}>
|
||||
<Button id="manual-button" variant="primary" onClick={handleRunScript}>
|
||||
Run script
|
||||
</Button>
|
||||
<Button id="manual-button" variant="primary" onClick={handleIdentifyClick}>
|
||||
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
|
||||
Run identify
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,14 @@ export function WebsiteHeader({
|
||||
path: '/reports',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.eventData),
|
||||
label: formatMessage(labels.sessions),
|
||||
icon: <Icons.User />,
|
||||
path: '/sessions',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.events),
|
||||
icon: <Icons.Nodes />,
|
||||
path: '/event-data',
|
||||
path: '/events',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -144,7 +144,10 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
||||
const { events, visitors } = data;
|
||||
|
||||
let logs = [
|
||||
...events.map(e => ({ __type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW, ...e })),
|
||||
...events.map(e => ({
|
||||
__type: e.eventName ? TYPE_EVENT : TYPE_PAGEVIEW,
|
||||
...e,
|
||||
})),
|
||||
...visitors.map(v => ({ __type: TYPE_SESSION, ...v })),
|
||||
].sort(thenby.firstBy('timestamp', -1));
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useSessions } from 'components/hooks';
|
||||
import SessionsTable from './SessionsTable';
|
||||
import DataTable from 'components/common/DataTable';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function SessionsDataTable({
|
||||
websiteId,
|
||||
children,
|
||||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const queryResult = useSessions(websiteId);
|
||||
|
||||
if (queryResult?.result?.data?.length === 0) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataTable queryResult={queryResult} allowSearch={false}>
|
||||
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
import WebsiteHeader from '../WebsiteHeader';
|
||||
import SessionsDataTable from './SessionsDataTable';
|
||||
|
||||
export function SessionsPage({ websiteId }) {
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
<SessionsDataTable websiteId={websiteId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsPage;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { GridColumn, GridTable, useBreakpoint } from 'react-basics';
|
||||
import { useMessages } from 'components/hooks';
|
||||
|
||||
export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
return (
|
||||
<GridTable data={data} cardMode={['xs', 'sm', 'md'].includes(breakpoint)}>
|
||||
<GridColumn name="id" label="ID" />
|
||||
<GridColumn name="country" label={formatMessage(labels.country)} />
|
||||
<GridColumn name="city" label={formatMessage(labels.city)} />
|
||||
<GridColumn name="browser" label={formatMessage(labels.browser)} />
|
||||
<GridColumn name="os" label={formatMessage(labels.os)} />
|
||||
<GridColumn name="device" label={formatMessage(labels.device)} />
|
||||
<GridColumn name="createdAt" label={formatMessage(labels.created)} />
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsTable;
|
||||
10
src/app/(main)/websites/[websiteId]/sessions/page.tsx
Normal file
10
src/app/(main)/websites/[websiteId]/sessions/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SessionsPage from './SessionsPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function ({ params: { websiteId } }) {
|
||||
return <SessionsPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sessions',
|
||||
};
|
||||
28
src/app/api/scripts/telemetry/route.ts
Normal file
28
src/app/api/scripts/telemetry/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
|
||||
|
||||
export async function GET() {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.DISABLE_TELEMETRY &&
|
||||
process.env.PRIVATE_MODE
|
||||
) {
|
||||
const script = `
|
||||
(()=>{const i=document.createElement('img');
|
||||
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
|
||||
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
|
||||
document.body.appendChild(i);})();
|
||||
`;
|
||||
|
||||
return new Response(script.replace(/\s\s+/g, ''), {
|
||||
headers: {
|
||||
'content-type': 'text/javascript',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('/* telemetry disabled */', {
|
||||
headers: {
|
||||
'content-type': 'text/javascript',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export * from './queries/useLogin';
|
||||
export * from './queries/useRealtime';
|
||||
export * from './queries/useReport';
|
||||
export * from './queries/useReports';
|
||||
export * from './queries/useSessions';
|
||||
export * from './queries/useShareToken';
|
||||
export * from './queries/useTeam';
|
||||
export * from './queries/useTeams';
|
||||
|
||||
20
src/components/hooks/queries/useSessions.ts
Normal file
20
src/components/hooks/queries/useSessions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useApi } from './useApi';
|
||||
import { useFilterQuery } from './useFilterQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useSessions(websiteId: string, params?: { [key: string]: string | number }) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`websites`);
|
||||
|
||||
return useFilterQuery({
|
||||
queryKey: ['sessions', { websiteId, modified, ...params }],
|
||||
queryFn: (data: any) => {
|
||||
return get(`/websites/${websiteId}/sessions`, {
|
||||
...data,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default useSessions;
|
||||
@@ -265,7 +265,7 @@ export const labels = defineMessages({
|
||||
journey: { id: 'label.journey', defaultMessage: 'Journey' },
|
||||
journeyDescription: {
|
||||
id: 'label.journey-description',
|
||||
defaultMessage: 'Understand how users nagivate through your website.',
|
||||
defaultMessage: 'Understand how users navigate through your website.',
|
||||
},
|
||||
compare: { id: 'label.compare', defaultMessage: 'Compare' },
|
||||
current: { id: 'label.current', defaultMessage: 'Current' },
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "انضم",
|
||||
"label.join-team": "انضم للفريق",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "اللغة",
|
||||
"label.languages": "اللغات",
|
||||
"label.laptop": "لابتوب",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Мова",
|
||||
"label.languages": "Мовы",
|
||||
"label.laptop": "Ноўтбук",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Присъедини се",
|
||||
"label.join-team": "Присъедини се към екип",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Език",
|
||||
"label.languages": "Езици",
|
||||
"label.laptop": "Лаптоп",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "ভাষা",
|
||||
"label.languages": "ভাষা",
|
||||
"label.laptop": "ল্যাপটপ",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Učlani se",
|
||||
"label.join-team": "Učlani se u tim",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Jezik",
|
||||
"label.languages": "Jezici",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Přenosný počítač",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Sprog",
|
||||
"label.languages": "Sprog",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Biträte",
|
||||
"label.join-team": "Team biträte",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Sprach",
|
||||
"label.languages": "Sprache",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Λάπτοπ",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Unir",
|
||||
"label.join-team": "Unirse al equipo",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Idioma",
|
||||
"label.languages": "Idiomas",
|
||||
"label.laptop": "Portátil",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "زبان",
|
||||
"label.languages": "زبانها",
|
||||
"label.laptop": "لپتاپ",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Kieli",
|
||||
"label.languages": "Kielet",
|
||||
"label.laptop": "Kannettava tietokone",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Fartelda",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Rejoindre",
|
||||
"label.join-team": "Rejoindre une équipe",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Langue",
|
||||
"label.languages": "Langues",
|
||||
"label.laptop": "Portable",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Idioma",
|
||||
"label.languages": "Idiomas",
|
||||
"label.laptop": "Portátil",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "לפטופ",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "लैपटॉप",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Jezik",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Bahasa",
|
||||
"label.languages": "Bahasa",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Lingua",
|
||||
"label.languages": "Lingue",
|
||||
"label.laptop": "Portatile",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "参加",
|
||||
"label.join-team": "チームに参加",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "言語",
|
||||
"label.languages": "言語",
|
||||
"label.laptop": "ノートPC",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "ភាសា",
|
||||
"label.languages": "ភាសា",
|
||||
"label.laptop": "កុំព្យូទ័រយួរដៃ",
|
||||
|
||||
@@ -1,267 +1,267 @@
|
||||
{
|
||||
"label.access-code": "Access code",
|
||||
"label.access-code": "액세스 코드",
|
||||
"label.actions": "액션",
|
||||
"label.activity-log": "Activity log",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.activity-log": "활동 기록",
|
||||
"label.add": "추가",
|
||||
"label.add-description": "설명 추가",
|
||||
"label.add-member": "멤버 추가",
|
||||
"label.add-step": "단계 추가",
|
||||
"label.add-website": "웹사이트 추가",
|
||||
"label.admin": "관리자",
|
||||
"label.after": "After",
|
||||
"label.after": "이후",
|
||||
"label.all": "전체",
|
||||
"label.all-time": "All time",
|
||||
"label.analytics": "Analytics",
|
||||
"label.average": "Average",
|
||||
"label.all-time": "전체 시간",
|
||||
"label.analytics": "분석",
|
||||
"label.average": "평균",
|
||||
"label.back": "뒤로",
|
||||
"label.before": "Before",
|
||||
"label.before": "이전",
|
||||
"label.bounce-rate": "이탈률",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
"label.breakdown": "세부 사항",
|
||||
"label.browser": "브라우저",
|
||||
"label.browsers": "브라우저",
|
||||
"label.cancel": "취소",
|
||||
"label.change-password": "비밀번호 변경",
|
||||
"label.cities": "Cities",
|
||||
"label.city": "City",
|
||||
"label.clear-all": "Clear all",
|
||||
"label.compare": "Compare",
|
||||
"label.confirm": "Confirm",
|
||||
"label.change-password": "비밀번호 변경하기",
|
||||
"label.cities": "도시",
|
||||
"label.city": "도시",
|
||||
"label.clear-all": "모두 지우기",
|
||||
"label.compare": "비교",
|
||||
"label.confirm": "확인",
|
||||
"label.confirm-password": "비밀번호 확인",
|
||||
"label.contains": "Contains",
|
||||
"label.continue": "Continue",
|
||||
"label.count": "Count",
|
||||
"label.contains": "포함",
|
||||
"label.continue": "계속",
|
||||
"label.count": "수",
|
||||
"label.countries": "국가",
|
||||
"label.country": "Country",
|
||||
"label.create": "Create",
|
||||
"label.create-report": "Create report",
|
||||
"label.create-team": "Create team",
|
||||
"label.create-user": "Create user",
|
||||
"label.created": "Created",
|
||||
"label.created-by": "Created By",
|
||||
"label.current": "Current",
|
||||
"label.country": "국가",
|
||||
"label.create": "생성",
|
||||
"label.create-report": "리포트 생성",
|
||||
"label.create-team": "팀 생성",
|
||||
"label.create-user": "사용자 생성",
|
||||
"label.created": "생성됨",
|
||||
"label.created-by": "작성자",
|
||||
"label.current": "현재",
|
||||
"label.current-password": "현재 비밀번호",
|
||||
"label.custom-range": "범위 지정",
|
||||
"label.dashboard": "대시보드",
|
||||
"label.data": "Data",
|
||||
"label.date": "Date",
|
||||
"label.data": "데이터",
|
||||
"label.date": "날짜",
|
||||
"label.date-range": "날짜 범위",
|
||||
"label.day": "Day",
|
||||
"label.day": "일",
|
||||
"label.default-date-range": "기본 날짜 범위",
|
||||
"label.delete": "삭제",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-team": "Delete team",
|
||||
"label.delete-user": "Delete user",
|
||||
"label.delete-report": "리포트 삭제",
|
||||
"label.delete-team": "팀 삭제",
|
||||
"label.delete-user": "사용자 삭제",
|
||||
"label.delete-website": "웹사이트 삭제",
|
||||
"label.description": "Description",
|
||||
"label.description": "설명",
|
||||
"label.desktop": "데스크탑",
|
||||
"label.details": "Details",
|
||||
"label.device": "Device",
|
||||
"label.details": "세부 사항",
|
||||
"label.device": "기기",
|
||||
"label.devices": "기기",
|
||||
"label.dismiss": "무시하기",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.does-not-contain": "포함하지 않음",
|
||||
"label.domain": "도메인",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.dropoff": "이탈",
|
||||
"label.edit": "편집",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-dashboard": "대시보드 편집",
|
||||
"label.edit-member": "회원 편집",
|
||||
"label.enable-share-url": "URL 공유 활성화",
|
||||
"label.end-step": "End Step",
|
||||
"label.entry": "Entry URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.end-step": "종료 단계",
|
||||
"label.entry": "입장 URL",
|
||||
"label.event": "이벤트",
|
||||
"label.event-data": "이벤트 데이터",
|
||||
"label.events": "이벤트",
|
||||
"label.exit": "Exit URL",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.filter": "Filter",
|
||||
"label.exit": "퇴장 URL",
|
||||
"label.false": "거짓",
|
||||
"label.field": "필드",
|
||||
"label.fields": "필드",
|
||||
"label.filter": "필터",
|
||||
"label.filter-combined": "합쳐서 보기",
|
||||
"label.filter-raw": "전체 보기",
|
||||
"label.filters": "Filters",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.goal": "Goal",
|
||||
"label.goals": "Goals",
|
||||
"label.goals-description": "Track your goals for pageviews and events.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
"label.host": "Host",
|
||||
"label.hosts": "Hosts",
|
||||
"label.insights": "Insights",
|
||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
||||
"label.is": "Is",
|
||||
"label.is-not": "Is not",
|
||||
"label.is-not-set": "Is not set",
|
||||
"label.is-set": "Is set",
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.filters": "필터",
|
||||
"label.funnel": "퍼널",
|
||||
"label.funnel-description": "사용자 전환율 및 이탈률을 살펴보세요.",
|
||||
"label.goal": "목표",
|
||||
"label.goals": "목표",
|
||||
"label.goals-description": "페이지뷰 및 이벤트 목표를 추적합니다.",
|
||||
"label.greater-than": "이상",
|
||||
"label.greater-than-equals": "이상",
|
||||
"label.host": "호스트",
|
||||
"label.hosts": "호스트",
|
||||
"label.insights": "인사이트",
|
||||
"label.insights-description": "세그먼트 및 필터를 사용하여 데이터를 더 자세히 살펴보세요.",
|
||||
"label.is": "해당",
|
||||
"label.is-not": "해당하지 않음",
|
||||
"label.is-not-set": "설정되지 않음",
|
||||
"label.is-set": "설정됨",
|
||||
"label.join": "가입",
|
||||
"label.join-team": "팀 가입",
|
||||
"label.journey": "여정",
|
||||
"label.journey-description": "사용자가 웹사이트를 탐색하는 경로를 살펴보세요.",
|
||||
"label.language": "언어",
|
||||
"label.languages": "언어",
|
||||
"label.laptop": "노트북",
|
||||
"label.last-days": "최근 {x} 일간",
|
||||
"label.last-days": "최근 {x} 일",
|
||||
"label.last-hours": "최근 {x} 시간",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.leave": "Leave",
|
||||
"label.leave-team": "Leave team",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.last-months": "최근 {x} 개월",
|
||||
"label.leave": "떠나기",
|
||||
"label.leave-team": "팀 떠나기",
|
||||
"label.less-than": "미만",
|
||||
"label.less-than-equals": "이하",
|
||||
"label.login": "로그인",
|
||||
"label.logout": "로그아웃",
|
||||
"label.manage": "Manage",
|
||||
"label.manager": "Manager",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.members": "Members",
|
||||
"label.min": "Min",
|
||||
"label.manage": "관리",
|
||||
"label.manager": "관리자",
|
||||
"label.max": "최대",
|
||||
"label.member": "멤버",
|
||||
"label.members": "멤버",
|
||||
"label.min": "최소",
|
||||
"label.mobile": "모바일",
|
||||
"label.more": "더 보기",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "내 계정",
|
||||
"label.my-websites": "내 웹사이트",
|
||||
"label.name": "이름",
|
||||
"label.new-password": "새 비밀번호",
|
||||
"label.none": "None",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.owner": "Owner",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.page-views": "페이지 뷰(PV)",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.none": "없음",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {레코드}}",
|
||||
"label.ok": "확인",
|
||||
"label.os": "운영체제",
|
||||
"label.overview": "개요",
|
||||
"label.owner": "소유자",
|
||||
"label.page-of": "{total} 중 {current} 페이지",
|
||||
"label.page-views": "페이지 뷰",
|
||||
"label.pageTitle": "페이지 제목",
|
||||
"label.pages": "페이지",
|
||||
"label.password": "비밀번호",
|
||||
"label.powered-by": "이 시스템은 {name}에서 구동되고 있습니다.",
|
||||
"label.previous": "Previous",
|
||||
"label.previous-period": "Previous period",
|
||||
"label.previous-year": "Previous year",
|
||||
"label.previous": "이전",
|
||||
"label.previous-period": "이전 기간",
|
||||
"label.previous-year": "이전 연도",
|
||||
"label.profile": "프로필",
|
||||
"label.property": "Property",
|
||||
"label.queries": "Queries",
|
||||
"label.query": "Query",
|
||||
"label.query-parameters": "Query parameters",
|
||||
"label.property": "속성",
|
||||
"label.queries": "쿼리",
|
||||
"label.query": "쿼리",
|
||||
"label.query-parameters": "쿼리 매개변수",
|
||||
"label.realtime": "실시간",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrer": "리퍼러",
|
||||
"label.referrers": "리퍼러",
|
||||
"label.refresh": "새로고침",
|
||||
"label.regenerate": "Regenerate",
|
||||
"label.region": "Region",
|
||||
"label.regions": "Regions",
|
||||
"label.remove": "Remove",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.reports": "Reports",
|
||||
"label.regenerate": "다시 생성",
|
||||
"label.region": "지역",
|
||||
"label.regions": "지역",
|
||||
"label.remove": "제거",
|
||||
"label.remove-member": "멤버 제거",
|
||||
"label.reports": "리포트",
|
||||
"label.required": "필수",
|
||||
"label.reset": "리셋",
|
||||
"label.reset-website": "Reset statistics",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
||||
"label.role": "Role",
|
||||
"label.run-query": "Run query",
|
||||
"label.reset-website": "웹사이트 초기화",
|
||||
"label.retention": "리텐션",
|
||||
"label.retention-description": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하십시오.",
|
||||
"label.role": "역할",
|
||||
"label.run-query": "쿼리 실행",
|
||||
"label.save": "저장",
|
||||
"label.screens": "Screens",
|
||||
"label.search": "Search",
|
||||
"label.select": "Select",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-role": "Select role",
|
||||
"label.select-website": "Select website",
|
||||
"label.sessions": "Sessions",
|
||||
"label.screens": "스크린",
|
||||
"label.search": "검색",
|
||||
"label.select": "선택",
|
||||
"label.select-date": "날짜 선택",
|
||||
"label.select-role": "역할 선택",
|
||||
"label.select-website": "웹사이트 선택",
|
||||
"label.sessions": "세션",
|
||||
"label.settings": "설정",
|
||||
"label.share-url": "공유 URL",
|
||||
"label.single-day": "하루",
|
||||
"label.start-step": "Start Step",
|
||||
"label.steps": "Steps",
|
||||
"label.sum": "Sum",
|
||||
"label.start-step": "시작 단계",
|
||||
"label.steps": "단계",
|
||||
"label.sum": "합계",
|
||||
"label.tablet": "태블릿",
|
||||
"label.team": "Team",
|
||||
"label.team-id": "Team ID",
|
||||
"label.team-manager": "Team manager",
|
||||
"label.team-member": "Team member",
|
||||
"label.team-name": "Team name",
|
||||
"label.team-owner": "Team owner",
|
||||
"label.team-view-only": "Team view only",
|
||||
"label.team-websites": "Team websites",
|
||||
"label.teams": "Teams",
|
||||
"label.theme": "Theme",
|
||||
"label.team": "팀",
|
||||
"label.team-id": "팀 ID",
|
||||
"label.team-manager": "팀 관리자",
|
||||
"label.team-member": "팀 멤버",
|
||||
"label.team-name": "팀 이름",
|
||||
"label.team-owner": "팀 소유자",
|
||||
"label.team-view-only": "팀 보기 전용",
|
||||
"label.team-websites": "팀 웹사이트",
|
||||
"label.teams": "팀",
|
||||
"label.theme": "테마",
|
||||
"label.this-month": "이번 달",
|
||||
"label.this-week": "이번 주",
|
||||
"label.this-year": "올해",
|
||||
"label.timezone": "표준 시간대",
|
||||
"label.title": "Title",
|
||||
"label.title": "제목",
|
||||
"label.today": "오늘",
|
||||
"label.toggle-charts": "Toggle charts",
|
||||
"label.total": "Total",
|
||||
"label.total-records": "Total records",
|
||||
"label.toggle-charts": "차트 전환",
|
||||
"label.total": "합계",
|
||||
"label.total-records": "총 레코드",
|
||||
"label.tracking-code": "추적 코드",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.true": "True",
|
||||
"label.type": "Type",
|
||||
"label.unique": "Unique",
|
||||
"label.transfer": "전송",
|
||||
"label.transfer-website": "웹사이트 전송",
|
||||
"label.true": "참",
|
||||
"label.type": "유형",
|
||||
"label.unique": "고유",
|
||||
"label.unique-visitors": "순방문자(UV)",
|
||||
"label.unknown": "알 수 없음",
|
||||
"label.untitled": "Untitled",
|
||||
"label.update": "Update",
|
||||
"label.untitled": "제목 없음",
|
||||
"label.update": "업데이트",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "User",
|
||||
"label.urls": "URL",
|
||||
"label.user": "사용자",
|
||||
"label.username": "사용자명",
|
||||
"label.users": "Users",
|
||||
"label.users": "사용자",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.value": "Value",
|
||||
"label.view": "View",
|
||||
"label.utm-description": "UTM 매개변수를 통해 캠페인을 추적합니다.",
|
||||
"label.value": "값",
|
||||
"label.view": "보기",
|
||||
"label.view-details": "상세보기",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "보기 전용",
|
||||
"label.views": "조회수",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.views-per-visit": "방문당 조회수",
|
||||
"label.visit-duration": "평균 방문 시간",
|
||||
"label.visitors": "방문객",
|
||||
"label.visits": "Visits",
|
||||
"label.website": "Website",
|
||||
"label.website-id": "Website ID",
|
||||
"label.visits": "방문",
|
||||
"label.website": "웹사이트",
|
||||
"label.website-id": "웹사이트 ID",
|
||||
"label.websites": "웹사이트",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Yesterday",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"label.window": "창",
|
||||
"label.yesterday": "어제",
|
||||
"message.action-confirmation": "확인을 위해 아래 상자에 {confirmation}을(를) 입력하십시오.",
|
||||
"message.active-users": "{x}명의 사용자가 보는 중입니다.",
|
||||
"message.collected-data": "Collected data",
|
||||
"message.collected-data": "수집된 데이터",
|
||||
"message.confirm-delete": "{target}을(를) 삭제하시겠습니까?",
|
||||
"message.confirm-leave": "Are you sure you want to leave {target}?",
|
||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
||||
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
|
||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
||||
"message.delete-website-warning": "관련된 모든 데이터도 삭제됩니다.",
|
||||
"message.confirm-leave": "{target}을(를) 떠나시겠습니까?",
|
||||
"message.confirm-remove": "{target}을(를) 제거하시겠습니까?",
|
||||
"message.confirm-reset": "{target}을(를) 초기화하시겠습니까?",
|
||||
"message.delete-team-warning": "팀을 삭제하면 팀에 등록된 모든 웹사이트도 삭제됩니다.",
|
||||
"message.delete-website-warning": "관련된 모든 데이터가 삭제됩니다.",
|
||||
"message.error": "오류가 발생하였습니다.",
|
||||
"message.event-log": "{event} on {url}",
|
||||
"message.event-log": "{event} - {url}",
|
||||
"message.go-to-settings": "설정으로 이동",
|
||||
"message.incorrect-username-password": "사용자 이름/비밀번호가 잘못되었습니다..",
|
||||
"message.incorrect-username-password": "사용자 이름/비밀번호가 잘못되었습니다.",
|
||||
"message.invalid-domain": "잘못된 도메인",
|
||||
"message.min-password-length": "Minimum length of {n} characters",
|
||||
"message.new-version-available": "A new version of Umami {version} is available!",
|
||||
"message.min-password-length": "최소 길이는 {n}자입니다",
|
||||
"message.new-version-available": "새 버전이 사용 가능합니다! - Umami {version}",
|
||||
"message.no-data-available": "사용 가능한 데이터가 없습니다.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "사용 가능한 이벤트 데이터가 없습니다.",
|
||||
"message.no-match-password": "비밀번호가 일치하지 않음",
|
||||
"message.no-results-found": "No results were found.",
|
||||
"message.no-team-websites": "This team does not have any websites.",
|
||||
"message.no-teams": "You have not created any teams.",
|
||||
"message.no-users": "There are no users.",
|
||||
"message.no-websites-configured": "구성된 웹 사이트가 없습니다.",
|
||||
"message.no-results-found": "결과를 찾을 수 없습니다.",
|
||||
"message.no-team-websites": "이 팀에는 웹사이트가 없습니다.",
|
||||
"message.no-teams": "생성된 팀이 없습니다.",
|
||||
"message.no-users": "사용자가 없습니다.",
|
||||
"message.no-websites-configured": "설정된 웹사이트가 없습니다.",
|
||||
"message.page-not-found": "페이지를 찾을 수 없습니다.",
|
||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
||||
"message.reset-website-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
|
||||
"message.reset-website": "이 웹사이트를 초기화하려면, 아래 상자에 {confirmation}을(를) 입력하십시오.",
|
||||
"message.reset-website-warning": "이 웹사이트의 모든 통계가 삭제되지만 설정은 그대로 유지됩니다.",
|
||||
"message.saved": "성공적으로 저장되었습니다.",
|
||||
"message.share-url": "이것은 {target}의 공개적으로 공유된 URL입니다.",
|
||||
"message.team-already-member": "You are already a member of the team.",
|
||||
"message.team-not-found": "Team not found.",
|
||||
"message.team-websites-info": "Websites can be viewed by anyone on the team.",
|
||||
"message.tracking-code": "추적 코드",
|
||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
||||
"message.triggered-event": "Triggered event",
|
||||
"message.user-deleted": "User deleted.",
|
||||
"message.viewed-page": "Viewed page",
|
||||
"message.visitor-log": "{os} {device}에서 {browser}을(를) 사용하는 {country}의 방문자",
|
||||
"message.visitors-dropped-off": "Visitors dropped off"
|
||||
"message.share-url": "아래 링크를 통해 웹사이트의 통계를 누구나 볼 수 있습니다.",
|
||||
"message.team-already-member": "이미 팀의 회원입니다.",
|
||||
"message.team-not-found": "팀을 찾을 수 없습니다.",
|
||||
"message.team-websites-info": "웹사이트는 팀의 누구나 볼 수 있습니다.",
|
||||
"message.tracking-code": "이 웹사이트의 통계를 추적하려면, 다음 코드를 HTML의 <head>...</head> 섹션에 추가하십시오.",
|
||||
"message.transfer-team-website-to-user": "이 웹사이트를 당신의 계정으로 전송하시겠습니까?",
|
||||
"message.transfer-user-website-to-team": "이 웹사이트를 전송받을 팀을 선택하십시오.",
|
||||
"message.transfer-website": "웹사이트 소유권을 계정이나 다른 팀으로 전송합니다.",
|
||||
"message.triggered-event": "트리거된 이벤트",
|
||||
"message.user-deleted": "사용자가 삭제되었습니다.",
|
||||
"message.viewed-page": "페이지 조회",
|
||||
"message.visitor-log": "{country}의 {browser} 브라우저를 사용하는 {os} {device} 방문자",
|
||||
"message.visitors-dropped-off": "방문자가 이탈했습니다"
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Prisijungti",
|
||||
"label.join-team": "Prisijungti į komandą",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Kalba",
|
||||
"label.languages": "Kalbos",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Нэгдэх",
|
||||
"label.join-team": "Багт нэгдэх",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Хэл",
|
||||
"label.languages": "Хэл",
|
||||
"label.laptop": "Зөөврийн компьютер",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "ဝင်မည်",
|
||||
"label.join-team": "အသင်းဝင်မည်",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "ဘာသာစကား",
|
||||
"label.languages": "ဘာသာစကားများ",
|
||||
"label.laptop": "လက်တော့ပ်",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Språk",
|
||||
"label.languages": "Språk",
|
||||
"label.laptop": "Bærbar",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Lid worden",
|
||||
"label.join-team": "Word lid van een team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Taal",
|
||||
"label.languages": "Talen",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Dołącz",
|
||||
"label.join-team": "Dołącz do zespołu",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Język",
|
||||
"label.languages": "Języki",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Participar",
|
||||
"label.join-team": "Participar da equipe",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Idioma",
|
||||
"label.languages": "Idiomas",
|
||||
"label.laptop": "Notebook",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Língua",
|
||||
"label.languages": "Línguas",
|
||||
"label.laptop": "Portátil",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Alătură-te",
|
||||
"label.join-team": "Alătură-te echipei",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Limbă",
|
||||
"label.languages": "Limbi",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Присоединиться",
|
||||
"label.join-team": "Присоединиться к команде",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Язык",
|
||||
"label.languages": "Языки",
|
||||
"label.laptop": "Ноутбук",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "භාෂාව",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "Prenosný počítač",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Pridruži se",
|
||||
"label.join-team": "Pridruži se ekipi",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Jezik",
|
||||
"label.languages": "Jeziki",
|
||||
"label.laptop": "Prenosni računalnik",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Gå med",
|
||||
"label.join-team": "Gå med i team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Språk",
|
||||
"label.languages": "Språk",
|
||||
"label.laptop": "Bärbar",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.laptop": "மடிக்கணினி",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "ภาษา",
|
||||
"label.languages": "ภาษา",
|
||||
"label.laptop": "แล็ปท็อป",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Katıl",
|
||||
"label.join-team": "Takıma katıl",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Dil",
|
||||
"label.languages": "Diller",
|
||||
"label.laptop": "Dizüstü",
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"label.join": "Приєднатись",
|
||||
"label.join-team": "Приєднатись до команди",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Мова",
|
||||
"label.languages": "Мови",
|
||||
"label.laptop": "Ноутбук",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "زبانیں",
|
||||
"label.laptop": "لیپ ٹاپ",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "Join",
|
||||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Ngôn ngữ",
|
||||
"label.laptop": "Laptop",
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"label.join": "加入",
|
||||
"label.join-team": "加入團隊",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users nagivate through your website.",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "語言",
|
||||
"label.languages": "語言",
|
||||
"label.laptop": "筆記型電腦",
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ClickHouseClient, createClient } from '@clickhouse/client';
|
||||
import dateFormat from 'dateformat';
|
||||
import debug from 'debug';
|
||||
import { CLICKHOUSE } from 'lib/db';
|
||||
import { QueryFilters, QueryOptions } from './types';
|
||||
import { OPERATORS } from './constants';
|
||||
import { PageParams, QueryFilters, QueryOptions } from './types';
|
||||
import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
|
||||
import { fetchWebsite } from './load';
|
||||
import { maxDate } from './date';
|
||||
import { filtersToArray } from './params';
|
||||
@@ -32,7 +32,7 @@ function getClient() {
|
||||
} = new URL(process.env.CLICKHOUSE_URL);
|
||||
|
||||
const client = createClient({
|
||||
host: `${protocol}//${hostname}:${port}`,
|
||||
url: `${protocol}//${hostname}:${port}`,
|
||||
database: pathname.replace('/', ''),
|
||||
username: username,
|
||||
password,
|
||||
@@ -47,11 +47,11 @@ function getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
function getDateStringQuery(data: any, unit: string | number) {
|
||||
function getDateStringSQL(data: any, unit: string | number) {
|
||||
return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`;
|
||||
}
|
||||
|
||||
function getDateQuery(field: string, unit: string, timezone?: string) {
|
||||
function getDateSQL(field: string, unit: string, timezone?: string) {
|
||||
if (timezone) {
|
||||
return `date_trunc('${unit}', ${field}, '${timezone}')`;
|
||||
}
|
||||
@@ -95,6 +95,20 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {})
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
function getDateQuery(filters: QueryFilters = {}) {
|
||||
const { startDate, endDate } = filters;
|
||||
|
||||
if (startDate) {
|
||||
if (endDate) {
|
||||
return `and created_at between {startDate:DateTime64} and {endDate:DateTime64}`;
|
||||
} else {
|
||||
return `and created_at >= {startDate:DateTime64}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function getFilterParams(filters: QueryFilters = {}) {
|
||||
return filtersToArray(filters).reduce((obj, { name, value }) => {
|
||||
if (name && value !== undefined) {
|
||||
@@ -110,6 +124,7 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
|
||||
|
||||
return {
|
||||
filterQuery: getFilterQuery(filters, options),
|
||||
dateQuery: getDateQuery(filters),
|
||||
params: {
|
||||
...getFilterParams(filters),
|
||||
websiteId,
|
||||
@@ -119,6 +134,32 @@ async function parseFilters(websiteId: string, filters: QueryFilters = {}, optio
|
||||
};
|
||||
}
|
||||
|
||||
async function pagedQuery(
|
||||
query: string,
|
||||
queryParams: { [key: string]: any },
|
||||
pageParams: PageParams = {},
|
||||
) {
|
||||
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams;
|
||||
const size = +pageSize || DEFAULT_PAGE_SIZE;
|
||||
const offset = +size * (page - 1);
|
||||
const direction = sortDescending ? 'desc' : 'asc';
|
||||
|
||||
const statements = [
|
||||
orderBy && `order by ${orderBy} ${direction}`,
|
||||
+size > 0 && `limit ${+size} offset ${offset}`,
|
||||
]
|
||||
.filter(n => n)
|
||||
.join('\n');
|
||||
|
||||
const count = await rawQuery(`select count(*) as num from (${query}) t`, queryParams).then(
|
||||
res => res[0].num,
|
||||
);
|
||||
|
||||
const data = await rawQuery(`${query}${statements}`, queryParams);
|
||||
|
||||
return { data, count, page: +page, pageSize: size, orderBy };
|
||||
}
|
||||
|
||||
async function rawQuery<T = unknown>(
|
||||
query: string,
|
||||
params: Record<string, unknown> = {},
|
||||
@@ -136,7 +177,13 @@ async function rawQuery<T = unknown>(
|
||||
format: 'JSONEachRow',
|
||||
});
|
||||
|
||||
return resultSet.json();
|
||||
return resultSet.json() as T;
|
||||
}
|
||||
|
||||
async function insert(table: string, values: any[]) {
|
||||
await connect();
|
||||
|
||||
return clickhouse.insert({ table, values, format: 'JSONEachRow' });
|
||||
}
|
||||
|
||||
async function findUnique(data: any[]) {
|
||||
@@ -164,12 +211,14 @@ export default {
|
||||
client: clickhouse,
|
||||
log,
|
||||
connect,
|
||||
getDateStringQuery,
|
||||
getDateQuery,
|
||||
getDateStringSQL,
|
||||
getDateSQL,
|
||||
getDateFormat,
|
||||
getFilterQuery,
|
||||
parseFilters,
|
||||
pagedQuery,
|
||||
findUnique,
|
||||
findFirst,
|
||||
rawQuery,
|
||||
insert,
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@ import path from 'path';
|
||||
import { getClientIp } from 'request-ip';
|
||||
import { browserName, detectOS } from 'detect-browser';
|
||||
import isLocalhost from 'is-localhost-ip';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import maxmind from 'maxmind';
|
||||
import { safeDecodeURIComponent } from 'next-basics';
|
||||
|
||||
import {
|
||||
DESKTOP_OS,
|
||||
MOBILE_OS,
|
||||
@@ -137,3 +137,31 @@ export async function getClientInfo(req: NextApiRequestCollect) {
|
||||
|
||||
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
|
||||
}
|
||||
|
||||
export function hasBlockedIp(req: NextApiRequestCollect) {
|
||||
const ignoreIps = process.env.IGNORE_IP;
|
||||
|
||||
if (ignoreIps) {
|
||||
const ips = [];
|
||||
|
||||
if (ignoreIps) {
|
||||
ips.push(...ignoreIps.split(',').map(n => n.trim()));
|
||||
}
|
||||
|
||||
const clientIp = getIpAddress(req);
|
||||
|
||||
return ips.find(ip => {
|
||||
if (ip === clientIp) return true;
|
||||
|
||||
// CIDR notation
|
||||
if (ip.indexOf('/') > 0) {
|
||||
const addr = ipaddr.parse(clientIp);
|
||||
const range = ipaddr.parseCIDR(ip);
|
||||
|
||||
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ function getDateFormat(date: Date, format?: string): string {
|
||||
}
|
||||
|
||||
async function sendMessage(
|
||||
message: { [key: string]: string | number },
|
||||
topic: string,
|
||||
message: { [key: string]: string | number },
|
||||
): Promise<RecordMetadata[]> {
|
||||
await connect();
|
||||
|
||||
@@ -77,7 +77,7 @@ async function sendMessage(
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMessages(messages: { [key: string]: string | number }[], topic: string) {
|
||||
async function sendMessages(topic: string, messages: { [key: string]: string | number }[]) {
|
||||
await connect();
|
||||
|
||||
await producer.send({
|
||||
|
||||
@@ -60,7 +60,7 @@ function getCastColumnQuery(field: string, type: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getDateQuery(field: string, unit: string, timezone?: string): string {
|
||||
function getDateSQL(field: string, unit: string, timezone?: string): string {
|
||||
const db = getDatabaseType();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
@@ -81,7 +81,19 @@ function getDateQuery(field: string, unit: string, timezone?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getTimestampDiffQuery(field1: string, field2: string): string {
|
||||
export function getTimestampSQL(field: string) {
|
||||
const db = getDatabaseType();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return `floor(extract(epoch from ${field}))`;
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return `UNIX_TIMESTAMP(${field})`;
|
||||
}
|
||||
}
|
||||
|
||||
function getTimestampDiffSQL(field1: string, field2: string): string {
|
||||
const db = getDatabaseType();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
@@ -93,7 +105,7 @@ function getTimestampDiffQuery(field1: string, field2: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getSearchQuery(column: string): string {
|
||||
function getSearchSQL(column: string): string {
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
|
||||
@@ -137,6 +149,20 @@ function getFilterQuery(filters: QueryFilters = {}, options: QueryOptions = {}):
|
||||
return query.join('\n');
|
||||
}
|
||||
|
||||
function getDateQuery(filters: QueryFilters = {}) {
|
||||
const { startDate, endDate } = filters;
|
||||
|
||||
if (startDate) {
|
||||
if (endDate) {
|
||||
return `and website_event.created_at between {{startDate}} and {{endDate}}`;
|
||||
} else {
|
||||
return `and website_event.created_at >= {{startDate}}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function getFilterParams(filters: QueryFilters = {}) {
|
||||
return filtersToArray(filters).reduce((obj, { name, operator, value }) => {
|
||||
obj[name] = [OPERATORS.contains, OPERATORS.doesNotContain].includes(operator)
|
||||
@@ -161,6 +187,7 @@ async function parseFilters(
|
||||
? `inner join session on website_event.session_id = session.session_id`
|
||||
: '',
|
||||
filterQuery: getFilterQuery(filters, options),
|
||||
dateQuery: getDateQuery(filters),
|
||||
params: {
|
||||
...getFilterParams(filters),
|
||||
websiteId,
|
||||
@@ -191,8 +218,8 @@ async function rawQuery(sql: string, data: object): Promise<any> {
|
||||
return prisma.rawQuery(query, params);
|
||||
}
|
||||
|
||||
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) {
|
||||
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
|
||||
async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) {
|
||||
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
|
||||
const size = +pageSize || DEFAULT_PAGE_SIZE;
|
||||
|
||||
const data = await prisma.client[model].findMany({
|
||||
@@ -256,11 +283,11 @@ export default {
|
||||
getAddIntervalQuery,
|
||||
getCastColumnQuery,
|
||||
getDayDiffQuery,
|
||||
getDateQuery,
|
||||
getDateSQL,
|
||||
getFilterQuery,
|
||||
getSearchParameters,
|
||||
getTimestampDiffQuery,
|
||||
getSearchQuery,
|
||||
getTimestampDiffSQL,
|
||||
getSearchSQL,
|
||||
getQueryMode,
|
||||
pagedQuery,
|
||||
parseFilters,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ok } from 'next-basics';
|
||||
import { CURRENT_VERSION, TELEMETRY_PIXEL } from 'lib/constants';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.setHeader('content-type', 'text/javascript');
|
||||
|
||||
if (process.env.DISABLE_TELEMETRY || process.env.PRIVATE_MODE) {
|
||||
return res.send('/* telemetry disabled */');
|
||||
}
|
||||
|
||||
const script = `
|
||||
(()=>{const i=document.createElement('img');
|
||||
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${CURRENT_VERSION}');
|
||||
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
|
||||
document.body.appendChild(i);})();
|
||||
`;
|
||||
|
||||
return res.send(script.replace(/\s\s+/g, ''));
|
||||
}
|
||||
|
||||
return ok(res);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import { isbot } from 'isbot';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
} from 'next-basics';
|
||||
import { COLLECTION_TYPE, HOSTNAME_REGEX, IP_REGEX } from 'lib/constants';
|
||||
import { secret, visitSalt, uuid } from 'lib/crypto';
|
||||
import { getIpAddress } from 'lib/detect';
|
||||
import { hasBlockedIp } from 'lib/detect';
|
||||
import { useCors, useSession, useValidate } from 'lib/middleware';
|
||||
import { CollectionType, YupRequest } from 'lib/types';
|
||||
import { saveEvent, saveSessionData } from 'queries';
|
||||
@@ -122,7 +121,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
||||
urlPath = '/';
|
||||
}
|
||||
|
||||
if (referrerPath?.startsWith('http')) {
|
||||
if (/^[\w-]+:\/\/\w+/.test(referrerPath)) {
|
||||
const refUrl = new URL(referrer);
|
||||
referrerPath = refUrl.pathname;
|
||||
referrerQuery = refUrl.search.substring(1);
|
||||
@@ -166,31 +165,3 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
|
||||
function hasBlockedIp(req: NextApiRequestCollect) {
|
||||
const ignoreIps = process.env.IGNORE_IP;
|
||||
|
||||
if (ignoreIps) {
|
||||
const ips = [];
|
||||
|
||||
if (ignoreIps) {
|
||||
ips.push(...ignoreIps.split(',').map(n => n.trim()));
|
||||
}
|
||||
|
||||
const clientIp = getIpAddress(req);
|
||||
|
||||
return ips.find(ip => {
|
||||
if (ip === clientIp) return true;
|
||||
|
||||
// CIDR notation
|
||||
if (ip.indexOf('/') > 0) {
|
||||
const addr = ipaddr.parse(clientIp);
|
||||
const range = ipaddr.parseCIDR(ip);
|
||||
|
||||
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
42
src/pages/api/websites/[websiteId]/sessions.ts
Normal file
42
src/pages/api/websites/[websiteId]/sessions.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as yup from 'yup';
|
||||
import { canViewWebsite } from 'lib/auth';
|
||||
import { useAuth, useCors, useValidate } from 'lib/middleware';
|
||||
import { NextApiRequestQueryBody, PageParams } from 'lib/types';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { pageInfo } from 'lib/schema';
|
||||
import { getSessions } from 'queries';
|
||||
|
||||
export interface ReportsRequestQuery extends PageParams {
|
||||
websiteId: string;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
GET: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
...pageInfo,
|
||||
}),
|
||||
};
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<ReportsRequestQuery, any>,
|
||||
res: NextApiResponse,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
const { websiteId } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getSessions(websiteId, {}, req.query);
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { DATA_TYPE } from 'lib/constants';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import { flattenJSON, getStringValue } from 'lib/data';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import { DynamicData } from 'lib/types';
|
||||
@@ -59,6 +60,7 @@ async function clickhouseQuery(data: {
|
||||
}) {
|
||||
const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data;
|
||||
|
||||
const { insert } = clickhouse;
|
||||
const { getDateFormat, sendMessages } = kafka;
|
||||
|
||||
const jsonKeys = flattenJSON(eventData);
|
||||
@@ -79,7 +81,11 @@ async function clickhouseQuery(data: {
|
||||
};
|
||||
});
|
||||
|
||||
await sendMessages(messages, 'event_data');
|
||||
if (kafka.enabled) {
|
||||
await sendMessages('event_data', messages);
|
||||
} else {
|
||||
await insert('event_data', messages);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function getEventMetrics(
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { rawQuery, getDateQuery, parseFilters } = prisma;
|
||||
const { rawQuery, getDateSQL, parseFilters } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
@@ -25,7 +25,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
`
|
||||
select
|
||||
event_name x,
|
||||
${getDateQuery('website_event.created_at', unit, timezone)} t,
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} t,
|
||||
count(*) y
|
||||
from website_event
|
||||
${joinSession}
|
||||
@@ -45,7 +45,7 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<{ x: string; t: string; y: number }[]> {
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { rawQuery, getDateQuery, parseFilters } = clickhouse;
|
||||
const { rawQuery, getDateSQL, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.customEvent,
|
||||
@@ -55,7 +55,7 @@ async function clickhouseQuery(
|
||||
`
|
||||
select
|
||||
event_name x,
|
||||
${getDateQuery('created_at', unit, timezone)} t,
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
count(*) y
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -1,45 +1,33 @@
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import prisma from 'lib/prisma';
|
||||
import { QueryFilters } from 'lib/types';
|
||||
import { PageParams, QueryFilters } from 'lib/types';
|
||||
|
||||
export function getEvents(...args: [websiteId: string, filters: QueryFilters]) {
|
||||
export function getEvents(
|
||||
...args: [websiteId: string, filters: QueryFilters, pageParams?: PageParams]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { startDate } = filters;
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
|
||||
const { pagedQuery } = prisma;
|
||||
|
||||
return prisma.client.websiteEvent
|
||||
.findMany({
|
||||
where: {
|
||||
websiteId,
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
.then(a => {
|
||||
return Object.values(a).map(a => {
|
||||
return {
|
||||
...a,
|
||||
timestamp: new Date(a.createdAt).getTime() / 1000,
|
||||
};
|
||||
});
|
||||
});
|
||||
const where = {
|
||||
...filters,
|
||||
id: websiteId,
|
||||
};
|
||||
|
||||
return pagedQuery('website_event', { where }, pageParams);
|
||||
}
|
||||
|
||||
function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery } = clickhouse;
|
||||
const { startDate } = filters;
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
|
||||
const { pagedQuery, parseFilters } = clickhouse;
|
||||
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
return pagedQuery(
|
||||
`
|
||||
select
|
||||
event_id as id,
|
||||
@@ -48,16 +36,20 @@ function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
created_at as createdAt,
|
||||
toUnixTimestamp(created_at) as timestamp,
|
||||
url_path as urlPath,
|
||||
url_query as urlQuery,
|
||||
referrer_path as referrerPath,
|
||||
referrer_query as referrerQuery,
|
||||
referrer_domain as referrerDomain,
|
||||
page_title as pageTitle,
|
||||
event_type as eventType,
|
||||
event_name as eventName
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at >= {startDate:DateTime64}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
order by created_at desc
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
},
|
||||
params,
|
||||
pageParams,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EVENT_NAME_LENGTH, URL_LENGTH, EVENT_TYPE, PAGE_TITLE_LENGTH } from 'lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import { uuid } from 'lib/crypto';
|
||||
@@ -134,6 +135,7 @@ async function clickhouseQuery(data: {
|
||||
city,
|
||||
...args
|
||||
} = data;
|
||||
const { insert } = clickhouse;
|
||||
const { getDateFormat, sendMessage } = kafka;
|
||||
const eventId = uuid();
|
||||
const createdAt = getDateFormat(new Date());
|
||||
@@ -164,7 +166,11 @@ async function clickhouseQuery(data: {
|
||||
created_at: createdAt,
|
||||
};
|
||||
|
||||
await sendMessage(message, 'event');
|
||||
if (kafka.enabled) {
|
||||
await sendMessage('event', message);
|
||||
} else {
|
||||
await insert('website_event', [message]);
|
||||
}
|
||||
|
||||
if (eventData) {
|
||||
await saveEventData({
|
||||
|
||||
@@ -19,15 +19,15 @@ export async function getRealtimeData(
|
||||
const { startDate, timezone } = criteria;
|
||||
const filters = { startDate, endDate: new Date(), unit: 'minute', timezone };
|
||||
const [events, sessions, pageviews, sessionviews] = await Promise.all([
|
||||
getEvents(websiteId, { startDate }),
|
||||
getSessions(websiteId, { startDate }),
|
||||
getEvents(websiteId, { startDate }, { pageSize: 10000 }),
|
||||
getSessions(websiteId, { startDate }, { pageSize: 10000 }),
|
||||
getPageviewStats(websiteId, filters),
|
||||
getSessionStats(websiteId, filters),
|
||||
]);
|
||||
|
||||
const uniques = new Set();
|
||||
|
||||
const sessionStats = sessions.reduce(
|
||||
const sessionStats = sessions.data.reduce(
|
||||
(obj: { visitors: any; countries: any }, session: { id: any; country: any }) => {
|
||||
const { countries, visitors } = obj;
|
||||
const { id, country } = session;
|
||||
@@ -49,7 +49,7 @@ export async function getRealtimeData(
|
||||
},
|
||||
);
|
||||
|
||||
const eventStats = events.reduce(
|
||||
const eventStats = events.data.reduce(
|
||||
(
|
||||
obj: { urls: any; referrers: any; events: any },
|
||||
event: { urlPath: any; referrerDomain: any },
|
||||
@@ -81,9 +81,9 @@ export async function getRealtimeData(
|
||||
visitors: sessionviews,
|
||||
},
|
||||
totals: {
|
||||
views: events.filter(e => !e.eventName).length,
|
||||
views: events.data.filter(e => !e.eventName).length,
|
||||
visitors: uniques.size,
|
||||
events: events.filter(e => e.eventName).length,
|
||||
events: events.data.filter(e => e.eventName).length,
|
||||
countries: Object.keys(sessionStats.countries).length,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -18,11 +18,11 @@ async function relationalQuery(
|
||||
endDate: Date,
|
||||
search: string,
|
||||
) {
|
||||
const { rawQuery, getSearchQuery } = prisma;
|
||||
const { rawQuery, getSearchSQL } = prisma;
|
||||
let searchQuery = '';
|
||||
|
||||
if (search) {
|
||||
searchQuery = getSearchQuery(column);
|
||||
searchQuery = getSearchSQL(column);
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
|
||||
@@ -21,7 +21,7 @@ async function relationalQuery(
|
||||
): Promise<
|
||||
{ pageviews: number; visitors: number; visits: number; bounces: number; totaltime: number }[]
|
||||
> {
|
||||
const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma;
|
||||
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
@@ -34,7 +34,7 @@ async function relationalQuery(
|
||||
count(distinct t.session_id) as "visitors",
|
||||
count(distinct t.visit_id) as "visits",
|
||||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||
sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime"
|
||||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime"
|
||||
from (
|
||||
select
|
||||
website_event.session_id,
|
||||
@@ -71,8 +71,8 @@ async function clickhouseQuery(
|
||||
`
|
||||
select
|
||||
sum(t.c) as "pageviews",
|
||||
count(distinct t.session_id) as "visitors",
|
||||
count(distinct t.visit_id) as "visits",
|
||||
uniq(t.session_id) as "visitors",
|
||||
uniq(t.visit_id) as "visits",
|
||||
sum(if(t.c = 1, 1, 0)) as "bounces",
|
||||
sum(max_time-min_time) as "totaltime"
|
||||
from (
|
||||
|
||||
@@ -42,15 +42,18 @@ async function relationalQuery(
|
||||
const aggregrate = type === 'entry' ? 'min' : 'max';
|
||||
|
||||
entryExitQuery = `
|
||||
JOIN (select visit_id,
|
||||
${aggregrate}(created_at) target_created_at
|
||||
from website_event
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = {{eventType}}
|
||||
group by visit_id) x
|
||||
ON x.visit_id = website_event.visit_id
|
||||
and x.target_created_at = website_event.created_at`;
|
||||
join (
|
||||
select visit_id,
|
||||
${aggregrate}(created_at) target_created_at
|
||||
from website_event
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and event_type = {{eventType}}
|
||||
group by visit_id
|
||||
) x
|
||||
on x.visit_id = website_event.visit_id
|
||||
and x.target_created_at = website_event.created_at
|
||||
`;
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
@@ -97,15 +100,18 @@ async function clickhouseQuery(
|
||||
const aggregrate = type === 'entry' ? 'min' : 'max';
|
||||
|
||||
entryExitQuery = `
|
||||
JOIN (select visit_id,
|
||||
${aggregrate}(created_at) target_created_at
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
group by visit_id) x
|
||||
ON x.visit_id = website_event.visit_id
|
||||
and x.target_created_at = website_event.created_at`;
|
||||
join (
|
||||
select visit_id,
|
||||
${aggregrate}(created_at) target_created_at
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_type = {eventType:UInt32}
|
||||
group by visit_id
|
||||
) x
|
||||
on x.visit_id = website_event.visit_id
|
||||
and x.target_created_at = website_event.created_at
|
||||
`;
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function getPageviewStats(...args: [websiteId: string, filters: Que
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { getDateQuery, parseFilters, rawQuery } = prisma;
|
||||
const { getDateSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
@@ -22,7 +22,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateQuery('website_event.created_at', unit, timezone)} x,
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} x,
|
||||
count(*) y
|
||||
from website_event
|
||||
${joinSession}
|
||||
@@ -41,7 +41,7 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse;
|
||||
const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
@@ -50,11 +50,11 @@ async function clickhouseQuery(
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateStringQuery('g.t', unit)} as x,
|
||||
${getDateStringSQL('g.t', unit)} as x,
|
||||
g.y as y
|
||||
from (
|
||||
select
|
||||
${getDateQuery('created_at', unit, timezone)} as t,
|
||||
${getDateSQL('created_at', unit, timezone)} as t,
|
||||
count(*) as y
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -23,7 +23,7 @@ async function relationalQuery(
|
||||
y: number;
|
||||
}[]
|
||||
> {
|
||||
const { getTimestampDiffQuery, parseFilters, rawQuery } = prisma;
|
||||
const { getTimestampDiffSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(
|
||||
websiteId,
|
||||
{
|
||||
@@ -42,7 +42,7 @@ async function relationalQuery(
|
||||
count(distinct t.session_id) as "visitors",
|
||||
count(distinct t.visit_id) as "visits",
|
||||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||
sum(${getTimestampDiffQuery('t.min_time', 't.max_time')}) as "totaltime",
|
||||
sum(${getTimestampDiffSQL('t.min_time', 't.max_time')}) as "totaltime",
|
||||
${parseFieldsByName(fields)}
|
||||
from (
|
||||
select
|
||||
|
||||
@@ -35,14 +35,14 @@ async function relationalQuery(
|
||||
}[]
|
||||
> {
|
||||
const { startDate, endDate, timezone = 'UTC' } = filters;
|
||||
const { getDateQuery, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
|
||||
const { getDateSQL, getDayDiffQuery, getCastColumnQuery, rawQuery } = prisma;
|
||||
const unit = 'day';
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
WITH cohort_items AS (
|
||||
select session_id,
|
||||
${getDateQuery('created_at', unit, timezone)} as cohort_date
|
||||
${getDateSQL('created_at', unit, timezone)} as cohort_date
|
||||
from session
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
@@ -50,10 +50,7 @@ async function relationalQuery(
|
||||
user_activities AS (
|
||||
select distinct
|
||||
w.session_id,
|
||||
${getDayDiffQuery(
|
||||
getDateQuery('created_at', unit, timezone),
|
||||
'c.cohort_date',
|
||||
)} as day_number
|
||||
${getDayDiffQuery(getDateSQL('created_at', unit, timezone), 'c.cohort_date')} as day_number
|
||||
from website_event w
|
||||
join cohort_items c
|
||||
on w.session_id = c.session_id
|
||||
@@ -115,14 +112,14 @@ async function clickhouseQuery(
|
||||
}[]
|
||||
> {
|
||||
const { startDate, endDate, timezone = 'UTC' } = filters;
|
||||
const { getDateQuery, getDateStringQuery, rawQuery } = clickhouse;
|
||||
const { getDateSQL, getDateStringSQL, rawQuery } = clickhouse;
|
||||
const unit = 'day';
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
WITH cohort_items AS (
|
||||
select
|
||||
min(${getDateQuery('created_at', unit, timezone)}) as cohort_date,
|
||||
min(${getDateSQL('created_at', unit, timezone)}) as cohort_date,
|
||||
session_id
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
@@ -132,7 +129,7 @@ async function clickhouseQuery(
|
||||
user_activities AS (
|
||||
select distinct
|
||||
w.session_id,
|
||||
(${getDateQuery('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
|
||||
(${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
|
||||
from website_event w
|
||||
join cohort_items c
|
||||
on w.session_id = c.session_id
|
||||
@@ -157,7 +154,7 @@ async function clickhouseQuery(
|
||||
group by 1, 2
|
||||
)
|
||||
select
|
||||
${getDateStringQuery('c.cohort_date', unit)} as date,
|
||||
${getDateStringSQL('c.cohort_date', unit)} as date,
|
||||
c.day_number as day,
|
||||
s.visitors as visitors,
|
||||
c.visitors returnVisitors,
|
||||
|
||||
@@ -46,12 +46,12 @@ async function relationalQuery(
|
||||
timezone = 'UTC',
|
||||
unit = 'day',
|
||||
} = criteria;
|
||||
const { getDateQuery, rawQuery } = prisma;
|
||||
const { getDateSQL, rawQuery } = prisma;
|
||||
|
||||
const chartRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateQuery('website_event.created_at', unit, timezone)} time,
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} time,
|
||||
sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum,
|
||||
avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg,
|
||||
count(case when data_key = {{revenueProperty}} then 1 else 0 end) count,
|
||||
@@ -110,7 +110,7 @@ async function clickhouseQuery(
|
||||
timezone = 'UTC',
|
||||
unit = 'day',
|
||||
} = criteria;
|
||||
const { getDateStringQuery, getDateQuery, rawQuery } = clickhouse;
|
||||
const { getDateStringSQL, getDateSQL, rawQuery } = clickhouse;
|
||||
|
||||
const chartRes = await rawQuery<{
|
||||
time: string;
|
||||
@@ -121,14 +121,14 @@ async function clickhouseQuery(
|
||||
}>(
|
||||
`
|
||||
select
|
||||
${getDateStringQuery('g.time', unit)} as time,
|
||||
${getDateStringSQL('g.time', unit)} as time,
|
||||
g.sum as sum,
|
||||
g.avg as avg,
|
||||
g.count as count,
|
||||
g.uniqueCount as uniqueCount
|
||||
from (
|
||||
select
|
||||
${getDateQuery('created_at', unit, timezone)} as time,
|
||||
${getDateSQL('created_at', unit, timezone)} as time,
|
||||
sumIf(number_value, data_key = {revenueProperty:String}) as sum,
|
||||
avgIf(number_value, data_key = {revenueProperty:String}) as avg,
|
||||
countIf(data_key = {revenueProperty:String}) as count,
|
||||
|
||||
@@ -75,7 +75,7 @@ async function clickhouseQuery(
|
||||
`
|
||||
select
|
||||
${column} x,
|
||||
count(distinct session_id) y
|
||||
uniq(session_id) y
|
||||
${includeCountry ? ', country' : ''}
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function getSessionStats(...args: [websiteId: string, filters: Quer
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc', unit = 'day' } = filters;
|
||||
const { getDateQuery, parseFilters, rawQuery } = prisma;
|
||||
const { getDateSQL, parseFilters, rawQuery } = prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
@@ -22,7 +22,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateQuery('website_event.created_at', unit, timezone)} x,
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} x,
|
||||
count(distinct website_event.session_id) y
|
||||
from website_event
|
||||
${joinSession}
|
||||
@@ -41,7 +41,7 @@ async function clickhouseQuery(
|
||||
filters: QueryFilters,
|
||||
): Promise<{ x: string; y: number }[]> {
|
||||
const { timezone = 'UTC', unit = 'day' } = filters;
|
||||
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery } = clickhouse;
|
||||
const { parseFilters, rawQuery, getDateStringSQL, getDateSQL } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
@@ -50,11 +50,11 @@ async function clickhouseQuery(
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateStringQuery('g.t', unit)} as x,
|
||||
${getDateStringSQL('g.t', unit)} as x,
|
||||
g.y as y
|
||||
from (
|
||||
select
|
||||
${getDateQuery('created_at', unit, timezone)} as t,
|
||||
${getDateSQL('created_at', unit, timezone)} as t,
|
||||
count(distinct session_id) as y
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
|
||||
@@ -1,45 +1,33 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db';
|
||||
import { QueryFilters } from 'lib/types';
|
||||
import { PageParams, QueryFilters } from 'lib/types';
|
||||
|
||||
export async function getSessions(...args: [websiteId: string, filters: QueryFilters]) {
|
||||
export async function getSessions(
|
||||
...args: [websiteId: string, filters?: QueryFilters, pageParams?: PageParams]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { startDate } = filters;
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) {
|
||||
const { pagedQuery } = prisma;
|
||||
|
||||
return prisma.client.session
|
||||
.findMany({
|
||||
where: {
|
||||
websiteId,
|
||||
createdAt: {
|
||||
gte: startDate,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
.then(a => {
|
||||
return Object.values(a).map(a => {
|
||||
return {
|
||||
...a,
|
||||
timestamp: new Date(a.createdAt).getTime() / 1000,
|
||||
};
|
||||
});
|
||||
});
|
||||
const where = {
|
||||
...filters,
|
||||
id: websiteId,
|
||||
};
|
||||
|
||||
return pagedQuery('session', { where }, pageParams);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { rawQuery } = clickhouse;
|
||||
const { startDate } = filters;
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
|
||||
const { pagedQuery, parseFilters } = clickhouse;
|
||||
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
return pagedQuery(
|
||||
`
|
||||
select
|
||||
session_id as id,
|
||||
@@ -58,12 +46,11 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
city
|
||||
from website_event
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at >= {startDate:DateTime64}
|
||||
${dateQuery}
|
||||
${filterQuery}
|
||||
order by created_at desc
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
},
|
||||
params,
|
||||
pageParams,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import prisma from 'lib/prisma';
|
||||
import { DynamicData } from 'lib/types';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import kafka from 'lib/kafka';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
|
||||
export async function saveSessionData(data: {
|
||||
websiteId: string;
|
||||
@@ -81,6 +82,7 @@ async function clickhouseQuery(data: {
|
||||
}) {
|
||||
const { websiteId, sessionId, sessionData, createdAt } = data;
|
||||
|
||||
const { insert } = clickhouse;
|
||||
const { getDateFormat, sendMessages } = kafka;
|
||||
|
||||
const jsonKeys = flattenJSON(sessionData);
|
||||
@@ -98,7 +100,11 @@ async function clickhouseQuery(data: {
|
||||
};
|
||||
});
|
||||
|
||||
await sendMessages(messages, 'session_data');
|
||||
if (kafka.enabled) {
|
||||
await sendMessages('session_data', messages);
|
||||
} else {
|
||||
await insert('session_data', messages);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * from './admin/report';
|
||||
export * from './admin/team';
|
||||
export * from './admin/teamUser';
|
||||
export * from './admin/user';
|
||||
export * from './admin/website';
|
||||
export * from 'queries/prisma/report';
|
||||
export * from 'queries/prisma/team';
|
||||
export * from 'queries/prisma/teamUser';
|
||||
export * from 'queries/prisma/user';
|
||||
export * from 'queries/prisma/website';
|
||||
export * from './analytics/events/getEventMetrics';
|
||||
export * from './analytics/events/getEventUsage';
|
||||
export * from './analytics/events/getEvents';
|
||||
|
||||
@@ -17,9 +17,9 @@ export async function getReport(reportId: string): Promise<Report> {
|
||||
|
||||
export async function getReports(
|
||||
criteria: ReportFindManyArgs,
|
||||
filters: PageParams = {},
|
||||
pageParams: PageParams = {},
|
||||
): Promise<PageResult<Report[]>> {
|
||||
const { query } = filters;
|
||||
const { query } = pageParams;
|
||||
|
||||
const where: Prisma.ReportWhereInput = {
|
||||
...criteria.where,
|
||||
@@ -45,7 +45,7 @@ export async function getReports(
|
||||
]),
|
||||
};
|
||||
|
||||
return prisma.pagedQuery('report', { ...criteria, where }, filters);
|
||||
return prisma.pagedQuery('report', { ...criteria, where }, pageParams);
|
||||
}
|
||||
|
||||
export async function getUserReports(
|
||||
@@ -49,9 +49,9 @@ export async function getUserByUsername(username: string, options: GetUserOption
|
||||
|
||||
export async function getUsers(
|
||||
criteria: UserFindManyArgs,
|
||||
filters?: PageParams,
|
||||
pageParams?: PageParams,
|
||||
): Promise<PageResult<User[]>> {
|
||||
const { query } = filters;
|
||||
const { query } = pageParams;
|
||||
|
||||
const where: Prisma.UserWhereInput = {
|
||||
...criteria.where,
|
||||
@@ -68,7 +68,7 @@ export async function getUsers(
|
||||
{
|
||||
orderBy: 'createdAt',
|
||||
sortDescending: true,
|
||||
...filters,
|
||||
...pageParams,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -27,9 +27,9 @@ export async function getSharedWebsite(shareId: string) {
|
||||
|
||||
export async function getWebsites(
|
||||
criteria: WebsiteFindManyArgs,
|
||||
filters: PageParams,
|
||||
pageParams: PageParams,
|
||||
): Promise<PageResult<Website[]>> {
|
||||
const { query } = filters;
|
||||
const { query } = pageParams;
|
||||
|
||||
const where: Prisma.WebsiteWhereInput = {
|
||||
...criteria.where,
|
||||
@@ -42,7 +42,7 @@ export async function getWebsites(
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
return prisma.pagedQuery('website', { ...criteria, where }, filters);
|
||||
return prisma.pagedQuery('website', { ...criteria, where }, pageParams);
|
||||
}
|
||||
|
||||
export async function getAllWebsites(userId: string) {
|
||||
Reference in New Issue
Block a user