diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js
index 3d7b7001..ca93f391 100644
--- a/components/metrics/ActiveUsers.js
+++ b/components/metrics/ActiveUsers.js
@@ -1,8 +1,8 @@
import React, { useMemo } from 'react';
+import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css';
-import { FormattedMessage } from 'react-intl';
export default function ActiveUsers({ websiteId, token, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 });
diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js
index 4f10e83e..24f642c1 100644
--- a/components/metrics/BarChart.js
+++ b/components/metrics/BarChart.js
@@ -39,6 +39,8 @@ export default function BarChart({
const w = canvas.current.width;
switch (unit) {
+ case 'minute':
+ return dateFormat(d, 'h:mm', locale);
case 'hour':
return dateFormat(d, 'ha', locale);
case 'day':
diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js
index 9120a6c3..d20db5eb 100644
--- a/components/metrics/PageviewsChart.js
+++ b/components/metrics/PageviewsChart.js
@@ -26,7 +26,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
data: { datasets },
} = chart;
- datasets[0].data = data.uniques;
+ datasets[0].data = data.sessions;
datasets[0].label = intl.formatMessage({
id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors',
@@ -56,7 +56,7 @@ export default function PageviewsChart({ websiteId, data, unit, records, classNa
id: 'metrics.unique-visitors',
defaultMessage: 'Unique visitors',
}),
- data: data.uniques,
+ data: data.sessions,
lineTension: 0,
backgroundColor: colors.visitors.background,
borderColor: colors.visitors.border,
diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js
index 6a07afe5..13d369c0 100644
--- a/components/metrics/WebsiteChart.js
+++ b/components/metrics/WebsiteChart.js
@@ -45,14 +45,14 @@ export default function WebsiteChart({
{ onDataLoad, update: [modified] },
);
- const [pageviews, uniques] = useMemo(() => {
+ const chartData = useMemo(() => {
if (data) {
- return [
- getDateArray(data.pageviews, startDate, endDate, unit),
- getDateArray(data.uniques, startDate, endDate, unit),
- ];
+ return {
+ pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
+ sessions: getDateArray(data.sessions, startDate, endDate, unit),
+ };
}
- return [[], []];
+ return { pageviews: [], sessions: [] };
}, [data]);
function handleCloseFilter() {
@@ -87,7 +87,7 @@ export default function WebsiteChart({
{error && }
state.user);
+function filterTime(data, time) {
+ return data.filter(({ created_at }) => new Date(created_at).getTime() > time);
+}
+
+function mapData(data) {
+ let last = 0;
+ const arr = [];
+
+ data.reduce((obj, val) => {
+ const { created_at } = val;
+ const t = startOfMinute(parseISO(created_at));
+ if (t.getTime() > last) {
+ obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
+ arr.push(obj);
+ last = t;
+ } else {
+ obj.y += 1;
+ }
+ return obj;
+ }, {});
+
+ return arr;
+}
+
+export default function RealtimeDashboard() {
+ const [data, setData] = useState();
const [website, setWebsite] = useState();
- const { data } = useFetch('/api/websites');
+ const { data: init, loading } = useFetch('/api/realtime', { type: 'init' });
+ const { data: updates } = useFetch(
+ '/api/realtime',
+ { type: 'update' },
+ { disabled: !init?.token, interval: 5000, headers: { 'x-umami-token': init?.token } },
+ );
- if (!data || !user?.is_admin) {
+ const chartData = useMemo(() => {
+ if (data) {
+ const endDate = startOfMinute(new Date());
+ const startDate = subMinutes(endDate, 30);
+ const unit = 'minute';
+
+ console.log({ data });
+
+ return {
+ pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit),
+ sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit),
+ };
+ }
+ return { pageviews: [], sessions: [] };
+ }, [data]);
+
+ useEffect(() => {
+ if (init && !data) {
+ setData(init.data);
+ } else if (updates) {
+ const { pageviews, sessions, events } = updates;
+ const time = subMinutes(startOfMinute(new Date()), 30).getTime();
+ setData(state => ({
+ pageviews: filterTime(state.pageviews, time).concat(pageviews),
+ sessions: filterTime(state.sessions, time).concat(sessions),
+ events: filterTime(state.events, time).concat(events),
+ }));
+ }
+ }, [updates, init]);
+
+ if (!init || loading || !data) {
return null;
}
- const options = [{ label: 'All websites', value: 0 }].concat(
- data.map(({ name, website_id }) => ({ label: name, value: website_id })),
- );
+ const { websites } = init;
+
+ const options = [
+ { label: , value: 0 },
+ ].concat(websites.map(({ name, website_id }) => ({ label: name, value: website_id })));
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0;
function handleSelect(value) {
- setWebsite(data.find(({ website_id }) => website_id === value));
+ setWebsite(websites.find(({ website_id }) => website_id === value));
}
return (
- Real time
+
+
+
-
+
);
}
diff --git a/hooks/useFetch.js b/hooks/useFetch.js
index 0eb82b13..b74a8fe7 100644
--- a/hooks/useFetch.js
+++ b/hooks/useFetch.js
@@ -14,14 +14,14 @@ export default function useFetch(url, params = {}, options = {}) {
const keys = Object.keys(params)
.sort()
.map(key => params[key]);
- const { update = [], onDataLoad = () => {} } = options;
+ const { update = [], onDataLoad = () => {}, disabled, headers } = options;
async function loadData() {
try {
setLoadiing(true);
setError(null);
const time = performance.now();
- const { data, status } = await get(`${basePath}${url}`, params);
+ const { data, status } = await get(`${basePath}${url}`, params, headers);
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));
@@ -43,7 +43,7 @@ export default function useFetch(url, params = {}, options = {}) {
}
useEffect(() => {
- if (url) {
+ if (url && !disabled) {
const { interval, delay = 0 } = options;
setTimeout(() => loadData(), delay);
@@ -54,7 +54,7 @@ export default function useFetch(url, params = {}, options = {}) {
clearInterval(id);
};
}
- }, [url, ...keys, ...update]);
+ }, [url, disabled, ...keys, ...update]);
return { data, status, error, loading };
}
diff --git a/lang/da-DK.json b/lang/da-DK.json
index cd96cb3f..ae8c4321 100644
--- a/lang/da-DK.json
+++ b/lang/da-DK.json
@@ -18,6 +18,7 @@
"button.view-details": "Vis detajler",
"label.accounts": "Kontoer",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Godkendt adgangskode",
"label.current-password": "Nuværende adgangskode",
"label.custom-range": "Tilpasset interval",
@@ -36,6 +37,7 @@
"label.password": "Adgangskode",
"label.passwords-dont-match": "Adgangskoder matcher ikke",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Påkrævet",
"label.settings": "Indstillinger",
"label.this-month": "Denne måned",
diff --git a/lang/de-DE.json b/lang/de-DE.json
index 8d507693..fdc7c3a1 100644
--- a/lang/de-DE.json
+++ b/lang/de-DE.json
@@ -18,6 +18,7 @@
"button.view-details": "Details anzeigen",
"label.accounts": "Konten",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Passwort wiederholen",
"label.current-password": "Derzeitiges Passwort",
"label.custom-range": "Benutzerdefinierter Bereich",
@@ -36,6 +37,7 @@
"label.password": "Passwort",
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Erforderlich",
"label.settings": "Einstellungen",
"label.this-month": "Diesen Monat",
diff --git a/lang/el-GR.json b/lang/el-GR.json
index 14cd69ca..ec4b3217 100644
--- a/lang/el-GR.json
+++ b/lang/el-GR.json
@@ -18,6 +18,7 @@
"button.view-details": "Λεπτομέρειες",
"label.accounts": "Λογαριασμοί",
"label.administrator": "Διαχειριστής",
+ "label.all-websites": "All websites",
"label.confirm-password": "Επιβεβαίωση κωδικού",
"label.current-password": "Τωρινός κωδικός πρόσβασης",
"label.custom-range": "Προσαρμοσμένο εύρος",
@@ -36,6 +37,7 @@
"label.password": "Κωδικός",
"label.passwords-dont-match": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
"label.profile": "Προφίλ",
+ "label.realtime": "Realtime",
"label.required": "Απαιτείται",
"label.settings": "Ρυθμίσεις",
"label.this-month": "Αυτο το μήνα",
diff --git a/lang/en-US.json b/lang/en-US.json
index 7c513b95..7251c123 100644
--- a/lang/en-US.json
+++ b/lang/en-US.json
@@ -18,6 +18,7 @@
"button.view-details": "View details",
"label.accounts": "Accounts",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Confirm password",
"label.current-password": "Current password",
"label.custom-range": "Custom range",
@@ -36,6 +37,7 @@
"label.password": "Password",
"label.passwords-dont-match": "Passwords don't match",
"label.profile": "Profile",
+ "label.realtime": "Realtime",
"label.required": "Required",
"label.settings": "Settings",
"label.this-month": "This month",
diff --git a/lang/es-MX.json b/lang/es-MX.json
index 16d911ea..9e6a6206 100644
--- a/lang/es-MX.json
+++ b/lang/es-MX.json
@@ -18,6 +18,7 @@
"button.view-details": "Ver detalles",
"label.accounts": "Usuarios",
"label.administrator": "Administrador",
+ "label.all-websites": "All websites",
"label.confirm-password": "Confirmar contraseña",
"label.current-password": "Contraseña actual",
"label.custom-range": "Custom range",
@@ -36,6 +37,7 @@
"label.password": "Contraseña",
"label.passwords-dont-match": "Las contraseñas no coinciden",
"label.profile": "Perfil",
+ "label.realtime": "Realtime",
"label.required": "Requerido",
"label.settings": "Configuraciones",
"label.this-month": "Este mes",
diff --git a/lang/fo-FO.json b/lang/fo-FO.json
index db9e972b..2c379960 100644
--- a/lang/fo-FO.json
+++ b/lang/fo-FO.json
@@ -18,6 +18,7 @@
"button.view-details": "Vís upplýsingar",
"label.accounts": "Brúkarar",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Vátta loyniorð",
"label.current-password": "Núverandi loyniorð",
"label.custom-range": "Tillaga spenni",
@@ -36,6 +37,7 @@
"label.password": "Loyniorð",
"label.passwords-dont-match": "Loyniorðini eru ikki eins",
"label.profile": "Brúkari",
+ "label.realtime": "Realtime",
"label.required": "Krav",
"label.settings": "Stillingar",
"label.this-month": "Hendan mánan",
diff --git a/lang/fr-FR.json b/lang/fr-FR.json
index 762cae34..013f89e5 100644
--- a/lang/fr-FR.json
+++ b/lang/fr-FR.json
@@ -18,6 +18,7 @@
"button.view-details": "Voir les details",
"label.accounts": "Comptes",
"label.administrator": "Administrateur",
+ "label.all-websites": "All websites",
"label.confirm-password": "Confirmation du mot de passe",
"label.current-password": "Mot de passe actuel",
"label.custom-range": "Plage personnalisée",
@@ -36,6 +37,7 @@
"label.password": "Mot de passe",
"label.passwords-dont-match": "Les mots de passe ne correspondent pas",
"label.profile": "Profile",
+ "label.realtime": "Realtime",
"label.required": "Requis",
"label.settings": "Paramètres",
"label.this-month": "Ce mois ci",
diff --git a/lang/id-ID.json b/lang/id-ID.json
index 8099cd75..8b09c0a6 100644
--- a/lang/id-ID.json
+++ b/lang/id-ID.json
@@ -18,6 +18,7 @@
"button.view-details": "Lihat Detil",
"label.accounts": "Akun",
"label.administrator": "Pengelola",
+ "label.all-websites": "All websites",
"label.confirm-password": "Konfirmasi kata sandi",
"label.current-password": "Kata sandi sekarang",
"label.custom-range": "Rentang khusus",
@@ -36,6 +37,7 @@
"label.password": "Kata sandi",
"label.passwords-dont-match": "Kata sandi tidak cocok",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Wajib",
"label.settings": "Pengaturan",
"label.this-month": "Bulan ini",
diff --git a/lang/ja-JP.json b/lang/ja-JP.json
index 51cdaec1..bbf73ed5 100644
--- a/lang/ja-JP.json
+++ b/lang/ja-JP.json
@@ -18,6 +18,7 @@
"button.view-details": "詳細を見る",
"label.accounts": "アカウント",
"label.administrator": "管理者",
+ "label.all-websites": "All websites",
"label.confirm-password": "パスワード(確認)",
"label.current-password": "現在のパスワード",
"label.custom-range": "期間を指定する",
@@ -36,6 +37,7 @@
"label.password": "パスワード",
"label.passwords-dont-match": "パスワードが一致しません",
"label.profile": "プロファイル",
+ "label.realtime": "Realtime",
"label.required": "必須",
"label.settings": "設定",
"label.this-month": "今月",
diff --git a/lang/mn-MN.json b/lang/mn-MN.json
index 694e068e..70af8f29 100644
--- a/lang/mn-MN.json
+++ b/lang/mn-MN.json
@@ -18,6 +18,7 @@
"button.view-details": "Дэлгэрүүлж харах",
"label.accounts": "Хэрэглэгчид",
"label.administrator": "Админ",
+ "label.all-websites": "All websites",
"label.confirm-password": "Шинэ нууц үгээ давтах",
"label.current-password": "Ашиглаж буй нууц үг",
"label.custom-range": "Дурын хугацаа",
@@ -36,6 +37,7 @@
"label.password": "Нууц үг",
"label.passwords-dont-match": "Нууц үг тохирохгүй байна",
"label.profile": "Бүртгэл",
+ "label.realtime": "Realtime",
"label.required": "Шаардлагатай",
"label.settings": "Тохиргоо",
"label.this-month": "Энэ сар",
diff --git a/lang/nb-NO.json b/lang/nb-NO.json
index dcd97d47..8e93b05c 100644
--- a/lang/nb-NO.json
+++ b/lang/nb-NO.json
@@ -18,6 +18,7 @@
"button.view-details": "Vis detaljer",
"label.accounts": "Kontoer",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Godkjenn passord",
"label.current-password": "Nåværende passord",
"label.custom-range": "Egendefinert utvalg",
@@ -36,6 +37,7 @@
"label.password": "Passord",
"label.passwords-dont-match": "Passordene er ikke like",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Påkrevd",
"label.settings": "Innstillinger",
"label.this-month": "Denne måneden",
diff --git a/lang/nl-NL.json b/lang/nl-NL.json
index 54f7894f..8978d7ee 100644
--- a/lang/nl-NL.json
+++ b/lang/nl-NL.json
@@ -18,6 +18,7 @@
"button.view-details": "Meer details",
"label.accounts": "Gebruikers",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Wachtwoord bevestigen",
"label.current-password": "Huidig wachtwoord",
"label.custom-range": "Aangepast bereik",
@@ -36,6 +37,7 @@
"label.password": "Wachtwoord",
"label.passwords-dont-match": "Wachtwoorden komen niet overeen",
"label.profile": "Profiel",
+ "label.realtime": "Realtime",
"label.required": "Verplicht",
"label.settings": "Instellingen",
"label.this-month": "Deze maand",
diff --git a/lang/pt-PT.json b/lang/pt-PT.json
index db9a8a7f..971c1aa7 100644
--- a/lang/pt-PT.json
+++ b/lang/pt-PT.json
@@ -18,6 +18,7 @@
"button.view-details": "Ver detalhes",
"label.accounts": "Contas",
"label.administrator": "Administrador",
+ "label.all-websites": "All websites",
"label.confirm-password": "Confirmar palavra-passe",
"label.current-password": "Palavra-passe atual",
"label.custom-range": "Intervalo personalizado",
@@ -36,6 +37,7 @@
"label.password": "Palavra-passe",
"label.passwords-dont-match": "Palavra-passes não correspondem",
"label.profile": "Perfil",
+ "label.realtime": "Realtime",
"label.required": "Obrigatório",
"label.settings": "Definições",
"label.this-month": "Este mês",
diff --git a/lang/ro-RO.json b/lang/ro-RO.json
index edaf27d6..8e28f887 100644
--- a/lang/ro-RO.json
+++ b/lang/ro-RO.json
@@ -18,6 +18,7 @@
"button.view-details": "Vizualizare detalii",
"label.accounts": "Conturi",
"label.administrator": "Administrator",
+ "label.all-websites": "All websites",
"label.confirm-password": "Confirmare parolă",
"label.current-password": "Parola curentă",
"label.custom-range": "Interval personalizat",
@@ -36,6 +37,7 @@
"label.password": "Parolă",
"label.passwords-dont-match": "Parolele nu se potrivesc",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Obligatoriu",
"label.settings": "Setări",
"label.this-month": "Această lună",
diff --git a/lang/ru-RU.json b/lang/ru-RU.json
index 74985574..05c9e0da 100644
--- a/lang/ru-RU.json
+++ b/lang/ru-RU.json
@@ -18,6 +18,7 @@
"button.view-details": "Посмотреть детали",
"label.accounts": "Аккаунты",
"label.administrator": "Администратор",
+ "label.all-websites": "All websites",
"label.confirm-password": "Подтвердить пароль",
"label.current-password": "Текущий пароль",
"label.custom-range": "Другой период",
@@ -36,6 +37,7 @@
"label.password": "Пароль",
"label.passwords-dont-match": "Пароли не совпадают",
"label.profile": "Профиль",
+ "label.realtime": "Realtime",
"label.required": "Обязательное",
"label.settings": "Настройки",
"label.this-month": "Этот месяц",
diff --git a/lang/sv-SE.json b/lang/sv-SE.json
index 9e5e5958..5a1662a5 100644
--- a/lang/sv-SE.json
+++ b/lang/sv-SE.json
@@ -18,6 +18,7 @@
"button.view-details": "Visa detaljer",
"label.accounts": "Konton",
"label.administrator": "Administratör",
+ "label.all-websites": "All websites",
"label.confirm-password": "Bekräfta lösenord",
"label.current-password": "Nuvarande lösenord",
"label.custom-range": "Anpassat urval",
@@ -36,6 +37,7 @@
"label.password": "Lösenord",
"label.passwords-dont-match": "Lösenorden är inte samma",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Krävs",
"label.settings": "Inställningar",
"label.this-month": "Denna månad",
diff --git a/lang/tr-TR.json b/lang/tr-TR.json
index 2e84051d..b1b206f3 100644
--- a/lang/tr-TR.json
+++ b/lang/tr-TR.json
@@ -18,6 +18,7 @@
"button.view-details": "Detayı incele",
"label.accounts": "Hesaplar",
"label.administrator": "Yönetici",
+ "label.all-websites": "All websites",
"label.confirm-password": "Parolayı onayla",
"label.current-password": "Mevcut parola",
"label.custom-range": "Özelleştirilmiş aralık",
@@ -36,6 +37,7 @@
"label.password": "Parola",
"label.passwords-dont-match": "Parolalar uyuşmuyor",
"label.profile": "Profil",
+ "label.realtime": "Realtime",
"label.required": "Zorunlu alan",
"label.settings": "Ayarlar",
"label.this-month": "Bu ay",
diff --git a/lang/uk-UA.json b/lang/uk-UA.json
index 9c5065bf..4d077847 100644
--- a/lang/uk-UA.json
+++ b/lang/uk-UA.json
@@ -18,6 +18,7 @@
"button.view-details": "Переглянути деталі",
"label.accounts": "Облікові записи",
"label.administrator": "Адміністратор",
+ "label.all-websites": "All websites",
"label.confirm-password": "Підтвердити пароль",
"label.current-password": "Поточний пароль",
"label.custom-range": "Довільний період",
@@ -36,6 +37,7 @@
"label.password": "Пароль",
"label.passwords-dont-match": "Паролі не співпадають",
"label.profile": "Профіль",
+ "label.realtime": "Realtime",
"label.required": "Обов'язкове",
"label.settings": "Налаштування",
"label.this-month": "Поточний місяць",
diff --git a/lang/zh-CN.json b/lang/zh-CN.json
index d5bf54cd..761ed17a 100644
--- a/lang/zh-CN.json
+++ b/lang/zh-CN.json
@@ -18,6 +18,7 @@
"button.view-details": "查看更多",
"label.accounts": "账户",
"label.administrator": "管理员",
+ "label.all-websites": "All websites",
"label.confirm-password": "确认密码",
"label.current-password": "目前密码",
"label.custom-range": "自定义时间段",
@@ -36,6 +37,7 @@
"label.password": "密码",
"label.passwords-dont-match": "密码不一致",
"label.profile": "个人资料",
+ "label.realtime": "Realtime",
"label.required": "必填",
"label.settings": "设置",
"label.this-month": "本月",
diff --git a/lib/date.js b/lib/date.js
index cdfe322c..50d623bb 100644
--- a/lib/date.js
+++ b/lib/date.js
@@ -7,6 +7,7 @@ import {
addYears,
subHours,
subDays,
+ startOfMinute,
startOfHour,
startOfDay,
startOfWeek,
@@ -17,6 +18,7 @@ import {
endOfWeek,
endOfMonth,
endOfYear,
+ differenceInMinutes,
differenceInHours,
differenceInCalendarDays,
differenceInCalendarMonths,
@@ -114,6 +116,7 @@ export function getDateFromString(str) {
}
const dateFuncs = {
+ minute: [differenceInMinutes, addMinutes, startOfMinute],
hour: [differenceInHours, addHours, startOfHour],
day: [differenceInCalendarDays, addDays, startOfDay],
month: [differenceInCalendarMonths, addMonths, startOfMonth],
diff --git a/lib/queries.js b/lib/queries.js
index 6312057f..ecf351b1 100644
--- a/lib/queries.js
+++ b/lib/queries.js
@@ -166,16 +166,6 @@ export async function createSession(website_id, data) {
);
}
-export async function getSessionById(session_id) {
- return runQuery(
- prisma.session.findOne({
- where: {
- session_id,
- },
- }),
- );
-}
-
export async function getSessionByUuid(session_uuid) {
return runQuery(
prisma.session.findOne({
@@ -285,6 +275,57 @@ export async function createAccount(data) {
);
}
+export async function getSessions(websites, start_at) {
+ return runQuery(
+ prisma.session.findMany({
+ where: {
+ website: {
+ website_id: {
+ in: websites,
+ },
+ },
+ created_at: {
+ gte: start_at,
+ },
+ },
+ }),
+ );
+}
+
+export async function getPageviews(websites, start_at) {
+ return runQuery(
+ prisma.pageview.findMany({
+ where: {
+ website: {
+ website_id: {
+ in: websites,
+ },
+ },
+ created_at: {
+ gte: start_at,
+ },
+ },
+ }),
+ );
+}
+
+export async function getEvents(websites, start_at) {
+ return runQuery(
+ prisma.event.findMany({
+ where: {
+ website: {
+ website_id: {
+ in: websites,
+ },
+ },
+ created_at: {
+ gte: start_at,
+ },
+ },
+ }),
+ );
+}
+
export function getWebsiteStats(website_id, start_at, end_at, filters = {}) {
const params = [website_id, start_at, end_at];
const { url } = filters;
@@ -425,7 +466,7 @@ export function getActiveVisitors(website_id) {
);
}
-export function getEvents(
+export function getEventMetrics(
website_id,
start_at,
end_at,
diff --git a/lib/web.js b/lib/web.js
index aceb6498..24d1d982 100644
--- a/lib/web.js
+++ b/lib/web.js
@@ -19,13 +19,17 @@ export const apiRequest = (method, url, body, headers) =>
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
});
-export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`);
+export const get = (url, params, headers) =>
+ apiRequest('get', `${url}${getQueryString(params)}`, undefined, headers);
-export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`);
+export const del = (url, params, headers) =>
+ apiRequest('delete', `${url}${getQueryString(params)}`, undefined, headers);
-export const post = (url, params) => apiRequest('post', url, JSON.stringify(params));
+export const post = (url, params, headers) =>
+ apiRequest('post', url, JSON.stringify(params), headers);
-export const put = (url, params) => apiRequest('put', url, JSON.stringify(params));
+export const put = (url, params, headers) =>
+ apiRequest('put', url, JSON.stringify(params), headers);
export const hook = (_this, method, callback) => {
const orig = _this[method];
diff --git a/package.json b/package.json
index 8f9edabb..22dd957b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "umami",
- "version": "0.81.0",
+ "version": "0.82.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao ",
"license": "MIT",
diff --git a/pages/api/realtime.js b/pages/api/realtime.js
index 7717c9b9..2130521b 100644
--- a/pages/api/realtime.js
+++ b/pages/api/realtime.js
@@ -1,19 +1,48 @@
+import { subMinutes } from 'date-fns';
import { useAuth } from 'lib/middleware';
-import { ok, unauthorized, methodNotAllowed } from 'lib/response';
+import { ok, methodNotAllowed, badRequest } from 'lib/response';
+import { getEvents, getPageviews, getSessions, getUserWebsites } from 'lib/queries';
+import { createToken, parseToken } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);
- const { is_admin } = req.auth;
-
- if (!is_admin) {
- return unauthorized(res);
+ async function getData(websites, time) {
+ return Promise.all([
+ getPageviews(websites, time),
+ getSessions(websites, time),
+ getEvents(websites, time),
+ ]);
}
if (req.method === 'GET') {
- const [pageviews, sessions, events] = await Promise.all([[], [], []]);
+ const { type } = req.query;
+ const { user_id } = req.auth;
- return ok(res, { pageviews, sessions, events });
+ if (type === 'init') {
+ const websites = await getUserWebsites(user_id);
+ const ids = websites.map(({ website_id }) => website_id);
+ const [pageviews, sessions, events] = await getData(ids, subMinutes(new Date(), 30));
+ const token = await createToken({ websites: ids });
+
+ return ok(res, { websites, token, data: { pageviews, sessions, events } });
+ }
+
+ if (type === 'update') {
+ const token = req.headers['x-umami-token'];
+
+ if (!token) {
+ return badRequest(res);
+ }
+
+ const { websites } = await parseToken(token);
+
+ const [pageviews, sessions, events] = await getData(websites, new Date());
+
+ return ok(res, { pageviews, sessions, events });
+ }
+
+ return badRequest(res);
}
return methodNotAllowed(res);
diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js
index 0498052f..7d98717a 100644
--- a/pages/api/website/[id]/events.js
+++ b/pages/api/website/[id]/events.js
@@ -1,5 +1,5 @@
import moment from 'moment-timezone';
-import { getEvents } from 'lib/queries';
+import { getEventMetrics } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
@@ -21,7 +21,7 @@ export default async (req, res) => {
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
- const events = await getEvents(websiteId, startDate, endDate, tz, unit, { url });
+ const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { url });
return ok(res, events);
}
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js
index 07362359..965f28ae 100644
--- a/pages/api/website/[id]/pageviews.js
+++ b/pages/api/website/[id]/pageviews.js
@@ -21,12 +21,12 @@ export default async (req, res) => {
return badRequest(res);
}
- const [pageviews, uniques] = await Promise.all([
+ const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url),
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
]);
- return ok(res, { pageviews, uniques });
+ return ok(res, { pageviews, sessions });
}
return methodNotAllowed(res);