From de8f5c4413dddeaabac072f92a93168a275c4dd8 Mon Sep 17 00:00:00 2001 From: Shrutesh Sharma <102956391+shrutesh1@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:11:27 +0530 Subject: [PATCH 01/28] Added batching --- src/pages/api/batch.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/pages/api/batch.ts diff --git a/src/pages/api/batch.ts b/src/pages/api/batch.ts new file mode 100644 index 000000000..93632d114 --- /dev/null +++ b/src/pages/api/batch.ts @@ -0,0 +1,35 @@ +import sendHandler from './send'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + res.setHeader('Allow', ['POST']); + return res.status(405).end(`Method ${req.method} Not Allowed`); + } + + const events = req.body; + + if (!Array.isArray(events)) { + return res.status(400).json({ error: 'Invalid payload, expected an array.' }); + } + + try { + for (const event of events) { + const mockReq = { + ...req, + body: event, + headers: { ...req.headers, origin: req.headers.origin || 'http://localhost:3000' }, + }; + + const mockRes = { + ...res, + end: () => {}, // Prevent premature response closure + }; + + await sendHandler(mockReq, mockRes); + } + + return res.status(200).json({ success: true, message: `${events.length} events processed.` }); + } catch (error) { + return res.status(500).json({ error: 'Internal Server Error' }); + } +} From b196f02ac51cf7945f2107cefe087a3deaa01eb7 Mon Sep 17 00:00:00 2001 From: Dexalt142 <31777261+Dexalt142@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:40:39 +0700 Subject: [PATCH 02/28] feat: update indonesian translation --- src/lang/id-ID.json | 310 ++++++++++++++++++++++---------------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index 6cf7659ca..a3f80a0a9 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -1,145 +1,145 @@ { - "label.access-code": "Access code", + "label.access-code": "Kode akses", "label.actions": "Aksi", - "label.activity": "Activity log", - "label.add": "Add", - "label.add-description": "Add description", - "label.add-member": "Add member", - "label.add-step": "Add step", + "label.activity": "Catatan aktivitas", + "label.add": "Tambah", + "label.add-description": "Tambah deskripsi", + "label.add-member": "Tambah anggota", + "label.add-step": "Tambah langkah", "label.add-website": "Tambah situs web", "label.admin": "Pengelola", - "label.after": "After", + "label.after": "Setelah", "label.all": "Semua", "label.all-time": "Semua waktu", - "label.analytics": "Analytics", - "label.average": "Average", + "label.analytics": "Analitik", + "label.average": "Rata-rata", "label.back": "Kembali", - "label.before": "Before", + "label.before": "Sebelum", "label.bounce-rate": "Rasio pentalan", - "label.breakdown": "Breakdown", - "label.browser": "Browser", + "label.breakdown": "Rincian", + "label.browser": "Peramban", "label.browsers": "Peramban", "label.cancel": "Batal", "label.change-password": "Ganti kata sandi", - "label.cities": "Cities", - "label.city": "City", - "label.clear-all": "Clear all", - "label.compare": "Compare", - "label.confirm": "Confirm", + "label.cities": "Kota", + "label.city": "Kota", + "label.clear-all": "Hapus semua", + "label.compare": "Bandingkan", + "label.confirm": "Konfirmasi", "label.confirm-password": "Konfirmasi kata sandi", - "label.contains": "Contains", - "label.continue": "Continue", - "label.count": "Count", + "label.contains": "Mengandung", + "label.continue": "Lanjutkan", + "label.count": "Jumlah", "label.countries": "Negara", - "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": "Negara", + "label.create": "Buat", + "label.create-report": "Buat laporan", + "label.create-team": "Buat tim", + "label.create-user": "Buat pengguna", + "label.created": "Dibuat", + "label.created-by": "Dibuat oleh", + "label.current": "Saat ini", "label.current-password": "Kata sandi sekarang", "label.custom-range": "Rentang khusus", "label.dashboard": "Dasbor", "label.data": "Data", - "label.date": "Date", + "label.date": "Tanggal", "label.date-range": "Rentang tanggal", - "label.day": "Day", + "label.day": "Hari", "label.default-date-range": "Rentang tanggal bawaan", "label.delete": "Hapus", - "label.delete-report": "Delete report", - "label.delete-team": "Delete team", - "label.delete-user": "Delete user", + "label.delete-report": "Hapus laporan", + "label.delete-team": "Hapus tim", + "label.delete-user": "Hapus pengguna", "label.delete-website": "Hapus situs web", - "label.description": "Description", + "label.description": "Deskripsi", "label.desktop": "Desktop", - "label.details": "Details", - "label.device": "Device", + "label.details": "Detail", + "label.device": "Perangkat", "label.devices": "Perangkat", "label.dismiss": "Tutup", - "label.does-not-contain": "Does not contain", + "label.does-not-contain": "Tidak mengandung", "label.domain": "Domain", - "label.dropoff": "Dropoff", + "label.dropoff": "Penurunan", "label.edit": "Sunting", - "label.edit-dashboard": "Edit dashboard", - "label.edit-member": "Edit member", + "label.edit-dashboard": "Sunting dasbor", + "label.edit-member": "Sunting anggota", "label.enable-share-url": "Aktifkan URL berbagi", "label.end-step": "End Step", "label.entry": "Entry URL", - "label.event": "Event", - "label.event-data": "Event data", - "label.events": "Perihal", + "label.event": "Peristiwa", + "label.event-data": "Data peristiwa", + "label.events": "Peristiwa", "label.exit": "Exit URL", - "label.false": "False", - "label.field": "Field", - "label.fields": "Fields", + "label.false": "Salah", + "label.field": "Kolom", + "label.fields": "Kolom", "label.filter": "Filter", "label.filter-combined": "Gabungan", "label.filter-raw": "Mentah", "label.filters": "Filters", - "label.first-seen": "First seen", + "label.first-seen": "Pertama kali dilihat", "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.funnel-description": "Pahami tingkat konversi dan penurunan pengguna.", + "label.goal": "Tujuan", + "label.goals": "Tujuan", + "label.goals-description": "Lacak tujuan Anda untuk tampilan halaman dan peristiwa.", + "label.greater-than": "Lebih dari", + "label.greater-than-equals": "Lebih dari atau sama dengan", "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 navigate through your website.", + "label.insights": "Wawasan", + "label.insights-description": "Jelajahi data Anda lebih dalam dengan menggunakan segmen dan filter.", + "label.is": "Adalah", + "label.is-not": "Bukan", + "label.is-not-set": "Tidak diatur", + "label.is-set": "Diatur", + "label.join": "Gabung", + "label.join-team": "Gabung tim", + "label.journey": "Perjalanan", + "label.journey-description": "Pahami bagaimana pengguna menavigasi situs web Anda.", "label.language": "Bahasa", "label.languages": "Bahasa", "label.laptop": "Laptop", "label.last-days": "{x} hari terakhir", "label.last-hours": "{x} jam terakhir", - "label.last-months": "Last {x} months", - "label.last-seen": "Last seen", - "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} bulan terakhir", + "label.last-seen": "Terakhir kali dilihat", + "label.leave": "Keluar", + "label.leave-team": "Keluar dari tim", + "label.less-than": "Kurang dari", + "label.less-than-equals": "Kurang dari atau sama dengan", "label.login": "Masuk", "label.logout": "Keluar", - "label.manage": "Manage", - "label.manager": "Manager", - "label.max": "Max", - "label.member": "Member", - "label.members": "Members", + "label.manage": "Kelola", + "label.manager": "Pengelola", + "label.max": "Maks", + "label.member": "Anggota", + "label.members": "Anggota", "label.min": "Min", "label.mobile": "Ponsel", "label.more": "Lebih banyak", - "label.my-account": "My account", - "label.my-websites": "My websites", + "label.my-account": "Akun saya", + "label.my-websites": "Situs web saya", "label.name": "Nama", "label.new-password": "Kata sandi baru", - "label.none": "None", + "label.none": "Tidak ada", "label.number-of-records": "{x} {x, plural, one {record} other {records}}", "label.ok": "OK", "label.os": "OS", - "label.overview": "Overview", + "label.overview": "Tinjauan umum", "label.owner": "Pemilik", - "label.page-of": "Page {current} of {total}", + "label.page-of": "Halaman {current} dari {total}", "label.page-views": "Tampilan halaman", - "label.pageTitle": "Page title", + "label.pageTitle": "Judul halaman", "label.pages": "Halaman", "label.password": "Kata sandi", "label.path": "Path", "label.paths": "Paths", "label.powered-by": "Didukung oleh {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Sebelumnya", + "label.previous-period": "Periode sebelumnya", + "label.previous-year": "Tahun lalu", "label.profile": "Profil", "label.properties": "Properties", "label.property": "Property", @@ -147,133 +147,133 @@ "label.query": "Query", "label.query-parameters": "Query parameters", "label.realtime": "Waktu nyata", - "label.referrer": "Referrer", + "label.referrer": "Perujuk", "label.referrers": "Perujuk", "label.refresh": "Segarkan", - "label.regenerate": "Regenerate", - "label.region": "Region", - "label.regions": "Regions", - "label.remove": "Remove", - "label.remove-member": "Remove member", - "label.reports": "Reports", + "label.regenerate": "Buat ulang", + "label.region": "Wilayah", + "label.regions": "Wilayah", + "label.remove": "Hapus", + "label.remove-member": "Hapus anggota", + "label.reports": "Laporan", "label.required": "Wajib", "label.reset": "Atur ulang", "label.reset-website": "Atur ulang statistik", - "label.retention": "Retention", - "label.retention-description": "Measure your website stickiness by tracking how often users return.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", - "label.revenue-property": "Revenue Property", + "label.retention": "Retensi", + "label.retention-description": "Ukur daya tarik situs web Anda dengan melacak seberapa sering pengguna kembali.", + "label.revenue": "Pendapatan", + "label.revenue-description": "Lihat pendapatan Anda seiring waktu.", + "label.revenue-property": "Properti pendapatan", "label.role": "Role", "label.run-query": "Run query", "label.save": "Simpan", "label.screens": "Layar", - "label.search": "Search", - "label.select": "Select", - "label.select-date": "Select date", - "label.select-role": "Select role", - "label.select-website": "Select website", - "label.session": "Session", - "label.sessions": "Sessions", + "label.search": "Cari", + "label.select": "Pilih", + "label.select-date": "Pilih tanggal", + "label.select-role": "Pilih role", + "label.select-website": "Pilih situs web", + "label.session": "Sesi", + "label.sessions": "Sesi", "label.settings": "Pengaturan", "label.share-url": "Bagikan URL", "label.single-day": "Sehari", "label.start-step": "Start Step", - "label.steps": "Steps", + "label.steps": "Langkah", "label.sum": "Sum", "label.tablet": "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": "Tim", + "label.team-id": "ID tim", + "label.team-manager": "Pengelola tim", + "label.team-member": "Anggota tim", + "label.team-name": "Nama tim", + "label.team-owner": "Pemilik tim", "label.team-view-only": "Team view only", - "label.team-websites": "Team websites", - "label.teams": "Teams", + "label.team-websites": "Situs web tim", + "label.teams": "Tim", "label.theme": "Tema", "label.this-month": "Bulan ini", "label.this-week": "Minggu ini", "label.this-year": "Tahun ini", "label.timezone": "Zona waktu", - "label.title": "Title", + "label.title": "Judul", "label.today": "Hari ini", "label.toggle-charts": "Buka grafik", "label.total": "Total", - "label.total-records": "Total records", + "label.total-records": "Total baris", "label.tracking-code": "Kode lacak", - "label.transactions": "Transactions", + "label.transactions": "Transaksi", "label.transfer": "Transfer", - "label.transfer-website": "Transfer website", - "label.true": "True", - "label.type": "Type", - "label.unique": "Unique", + "label.transfer-website": "Transfer situs web", + "label.true": "Benar", + "label.type": "Tipe", + "label.unique": "Unik", "label.unique-visitors": "Pengunjung unik", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Kustomer unik", "label.unknown": "Tidak diketahui", - "label.untitled": "Untitled", - "label.update": "Update", + "label.untitled": "Tanpa judul", + "label.update": "Perbarui", "label.url": "URL", "label.urls": "URLs", - "label.user": "User", + "label.user": "Pengguna", "label.user-property": "User Property", "label.username": "Nama pengguna", - "label.users": "Users", + "label.users": "Pengguna", "label.utm": "UTM", - "label.utm-description": "Track your campaigns through UTM parameters.", - "label.value": "Value", - "label.view": "View", + "label.utm-description": "Lacak kampanye Anda melalui parameter UTM.", + "label.value": "Nilai", + "label.view": "Lihat", "label.view-details": "Lihat Detil", - "label.view-only": "View only", + "label.view-only": "Hanya melihat", "label.views": "Tampilan", - "label.views-per-visit": "Views per visit", + "label.views-per-visit": "Tampilan per kunjungan", "label.visit-duration": "Waktu kunjungan rata-rata", "label.visitors": "Pengunjung", - "label.visits": "Visits", - "label.website": "Website", - "label.website-id": "Website ID", + "label.visits": "Kunjungan", + "label.website": "Situs web", + "label.website-id": "ID situs web", "label.websites": "Situs web", "label.window": "Window", - "label.yesterday": "Yesterday", + "label.yesterday": "Kemarin", "message.action-confirmation": "Type {confirmation} in the box below to confirm.", "message.active-users": "{x} pengunjung saat ini", - "message.collected-data": "Collected data", + "message.collected-data": "Data dikumpulkan", "message.confirm-delete": "Apakah kamu yakin ingin menghapus {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-leave": "Apakah Anda yakin ingin meninggalkan {target}?", + "message.confirm-remove": "Apakah Anda yakin ingin menghapus {target}?", "message.confirm-reset": "Anda yakin ingin mengatur ulang statistik {target}?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", + "message.delete-team-warning": "Menghapus tim juga akan menghapus semua situs web yang terkait.", "message.delete-website-warning": "Semua data terkait juga akan dihapus.", "message.error": "Ada yang salah.", "message.event-log": "{event} on {url}", "message.go-to-settings": "Pergi ke pengaturan", "message.incorrect-username-password": "Nama pengguna/kata sandi salah.", "message.invalid-domain": "Domain tidak valid", - "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": "Minimal {n} karakter", + "message.new-version-available": "Versi baru dari Umami {version} telah tersedia!", "message.no-data-available": "Tidak ada data.", - "message.no-event-data": "No event data is available.", + "message.no-event-data": "Tidak ada data peristiwa", "message.no-match-password": "Kata sandi tidak cocok", - "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-results-found": "Tidak ada hasil yang ditemukan.", + "message.no-team-websites": "Tim ini tidak memiliki situs web.", + "message.no-teams": "Anda belum membuat tim.", + "message.no-users": "Tidak ada pengguna.", "message.no-websites-configured": "Anda tidak memiliki situs web yang dikonfigurasi.", "message.page-not-found": "Halaman tidak ditemukan.", - "message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.", - "message.reset-website-warning": "Semua statistik pada website ini akan dihapus, tetapi kode lacak akan tetap terpasang", + "message.reset-website": "Untuk mengatur ulang situs web ini, ketik {confirmation} pada kotak di bawah untuk mengonfirmasi.", + "message.reset-website-warning": "Semua statistik pada situs web ini akan dihapus, tetapi kode lacak akan tetap terpasang", "message.saved": "Berhasil disimpan.", "message.share-url": "Ini adalah URL yang dibagikan secara publik untuk {target}.", - "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.team-already-member": "Anda sudah menjadi anggota tim ini.", + "message.team-not-found": "Tim tidak ditemukan.", + "message.team-websites-info": "Situs web dapat dilihat oleh semua anggota tim.", "message.tracking-code": "Kode lacak", - "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.transfer-team-website-to-user": "Transfer situs web ini ke akun Anda?", + "message.transfer-user-website-to-team": "Pilih tim tujuan untuk mentransfer situs web ini.", + "message.transfer-website": "Transfer kepemilikan situs web ke akun Anda atau tim lain", + "message.triggered-event": "Peristiwa terjadi", + "message.user-deleted": "Pengguna telah dihapus.", + "message.viewed-page": "Halaman dilihat", "message.visitor-log": "Pengunjung dari {country} dengan {browser} di {device} {os}", - "message.visitors-dropped-off": "Visitors dropped off" + "message.visitors-dropped-off": "Pengunjung yang meninggalkan situs web" } From 767d5999727c8d48d5a0e81ff9e890282b19ce02 Mon Sep 17 00:00:00 2001 From: Dexalt142 <31777261+Dexalt142@users.noreply.github.com> Date: Fri, 21 Feb 2025 23:27:43 +0700 Subject: [PATCH 03/28] feat: update indonesian translation --- src/lang/id-ID.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/id-ID.json b/src/lang/id-ID.json index a3f80a0a9..b6eb83ce1 100644 --- a/src/lang/id-ID.json +++ b/src/lang/id-ID.json @@ -65,8 +65,8 @@ "label.edit-dashboard": "Sunting dasbor", "label.edit-member": "Sunting anggota", "label.enable-share-url": "Aktifkan URL berbagi", - "label.end-step": "End Step", - "label.entry": "Entry URL", + "label.end-step": "Langkah akhir", + "label.entry": "URL masuk", "label.event": "Peristiwa", "label.event-data": "Data peristiwa", "label.events": "Peristiwa", @@ -178,7 +178,7 @@ "label.settings": "Pengaturan", "label.share-url": "Bagikan URL", "label.single-day": "Sehari", - "label.start-step": "Start Step", + "label.start-step": "Langkah awal", "label.steps": "Langkah", "label.sum": "Sum", "label.tablet": "Tablet", @@ -235,7 +235,7 @@ "label.websites": "Situs web", "label.window": "Window", "label.yesterday": "Kemarin", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", + "message.action-confirmation": "Ketik {confirmation} pada kotak di bawah untuk mengonfirmasi.", "message.active-users": "{x} pengunjung saat ini", "message.collected-data": "Data dikumpulkan", "message.confirm-delete": "Apakah kamu yakin ingin menghapus {target}?", From 4d0bd03b4306222a8b099c1edbbc4fa144b76268 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 21 Feb 2025 17:47:39 -0800 Subject: [PATCH 04/28] Updated language files. --- public/intl/messages/id-ID.json | 332 ++++++++++++++++---------------- public/intl/messages/mn-MN.json | 132 ++++++------- 2 files changed, 230 insertions(+), 234 deletions(-) diff --git a/public/intl/messages/id-ID.json b/public/intl/messages/id-ID.json index c6bb04a28..b0fd0239f 100644 --- a/public/intl/messages/id-ID.json +++ b/public/intl/messages/id-ID.json @@ -2,7 +2,7 @@ "label.access-code": [ { "type": 0, - "value": "Access code" + "value": "Kode akses" } ], "label.actions": [ @@ -14,31 +14,31 @@ "label.activity": [ { "type": 0, - "value": "Activity log" + "value": "Catatan aktivitas" } ], "label.add": [ { "type": 0, - "value": "Add" + "value": "Tambah" } ], "label.add-description": [ { "type": 0, - "value": "Add description" + "value": "Tambah deskripsi" } ], "label.add-member": [ { "type": 0, - "value": "Add member" + "value": "Tambah anggota" } ], "label.add-step": [ { "type": 0, - "value": "Add step" + "value": "Tambah langkah" } ], "label.add-website": [ @@ -56,7 +56,7 @@ "label.after": [ { "type": 0, - "value": "After" + "value": "Setelah" } ], "label.all": [ @@ -74,13 +74,13 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Analitik" } ], "label.average": [ { "type": 0, - "value": "Average" + "value": "Rata-rata" } ], "label.back": [ @@ -92,7 +92,7 @@ "label.before": [ { "type": 0, - "value": "Before" + "value": "Sebelum" } ], "label.bounce-rate": [ @@ -104,13 +104,13 @@ "label.breakdown": [ { "type": 0, - "value": "Breakdown" + "value": "Rincian" } ], "label.browser": [ { "type": 0, - "value": "Browser" + "value": "Peramban" } ], "label.browsers": [ @@ -134,31 +134,31 @@ "label.cities": [ { "type": 0, - "value": "Cities" + "value": "Kota" } ], "label.city": [ { "type": 0, - "value": "City" + "value": "Kota" } ], "label.clear-all": [ { "type": 0, - "value": "Clear all" + "value": "Hapus semua" } ], "label.compare": [ { "type": 0, - "value": "Compare" + "value": "Bandingkan" } ], "label.confirm": [ { "type": 0, - "value": "Confirm" + "value": "Konfirmasi" } ], "label.confirm-password": [ @@ -170,19 +170,19 @@ "label.contains": [ { "type": 0, - "value": "Contains" + "value": "Mengandung" } ], "label.continue": [ { "type": 0, - "value": "Continue" + "value": "Lanjutkan" } ], "label.count": [ { "type": 0, - "value": "Count" + "value": "Jumlah" } ], "label.countries": [ @@ -194,49 +194,49 @@ "label.country": [ { "type": 0, - "value": "Country" + "value": "Negara" } ], "label.create": [ { "type": 0, - "value": "Create" + "value": "Buat" } ], "label.create-report": [ { "type": 0, - "value": "Create report" + "value": "Buat laporan" } ], "label.create-team": [ { "type": 0, - "value": "Create team" + "value": "Buat tim" } ], "label.create-user": [ { "type": 0, - "value": "Create user" + "value": "Buat pengguna" } ], "label.created": [ { "type": 0, - "value": "Created" + "value": "Dibuat" } ], "label.created-by": [ { "type": 0, - "value": "Created By" + "value": "Dibuat oleh" } ], "label.current": [ { "type": 0, - "value": "Current" + "value": "Saat ini" } ], "label.current-password": [ @@ -266,7 +266,7 @@ "label.date": [ { "type": 0, - "value": "Date" + "value": "Tanggal" } ], "label.date-range": [ @@ -278,7 +278,7 @@ "label.day": [ { "type": 0, - "value": "Day" + "value": "Hari" } ], "label.default-date-range": [ @@ -296,19 +296,19 @@ "label.delete-report": [ { "type": 0, - "value": "Delete report" + "value": "Hapus laporan" } ], "label.delete-team": [ { "type": 0, - "value": "Delete team" + "value": "Hapus tim" } ], "label.delete-user": [ { "type": 0, - "value": "Delete user" + "value": "Hapus pengguna" } ], "label.delete-website": [ @@ -320,7 +320,7 @@ "label.description": [ { "type": 0, - "value": "Description" + "value": "Deskripsi" } ], "label.desktop": [ @@ -332,13 +332,13 @@ "label.details": [ { "type": 0, - "value": "Details" + "value": "Detail" } ], "label.device": [ { "type": 0, - "value": "Device" + "value": "Perangkat" } ], "label.devices": [ @@ -356,7 +356,7 @@ "label.does-not-contain": [ { "type": 0, - "value": "Does not contain" + "value": "Tidak mengandung" } ], "label.domain": [ @@ -368,7 +368,7 @@ "label.dropoff": [ { "type": 0, - "value": "Dropoff" + "value": "Penurunan" } ], "label.edit": [ @@ -380,13 +380,13 @@ "label.edit-dashboard": [ { "type": 0, - "value": "Edit dashboard" + "value": "Sunting dasbor" } ], "label.edit-member": [ { "type": 0, - "value": "Edit member" + "value": "Sunting anggota" } ], "label.enable-share-url": [ @@ -398,31 +398,31 @@ "label.end-step": [ { "type": 0, - "value": "End Step" + "value": "Langkah akhir" } ], "label.entry": [ { "type": 0, - "value": "Entry URL" + "value": "URL masuk" } ], "label.event": [ { "type": 0, - "value": "Event" + "value": "Peristiwa" } ], "label.event-data": [ { "type": 0, - "value": "Event data" + "value": "Data peristiwa" } ], "label.events": [ { "type": 0, - "value": "Perihal" + "value": "Peristiwa" } ], "label.exit": [ @@ -434,19 +434,19 @@ "label.false": [ { "type": 0, - "value": "False" + "value": "Salah" } ], "label.field": [ { "type": 0, - "value": "Field" + "value": "Kolom" } ], "label.fields": [ { "type": 0, - "value": "Fields" + "value": "Kolom" } ], "label.filter": [ @@ -476,7 +476,7 @@ "label.first-seen": [ { "type": 0, - "value": "First seen" + "value": "Pertama kali dilihat" } ], "label.funnel": [ @@ -488,37 +488,37 @@ "label.funnel-description": [ { "type": 0, - "value": "Understand the conversion and drop-off rate of users." + "value": "Pahami tingkat konversi dan penurunan pengguna." } ], "label.goal": [ { "type": 0, - "value": "Goal" + "value": "Tujuan" } ], "label.goals": [ { "type": 0, - "value": "Goals" + "value": "Tujuan" } ], "label.goals-description": [ { "type": 0, - "value": "Track your goals for pageviews and events." + "value": "Lacak tujuan Anda untuk tampilan halaman dan peristiwa." } ], "label.greater-than": [ { "type": 0, - "value": "Greater than" + "value": "Lebih dari" } ], "label.greater-than-equals": [ { "type": 0, - "value": "Greater than or equals" + "value": "Lebih dari atau sama dengan" } ], "label.host": [ @@ -536,61 +536,61 @@ "label.insights": [ { "type": 0, - "value": "Insights" + "value": "Wawasan" } ], "label.insights-description": [ { "type": 0, - "value": "Dive deeper into your data by using segments and filters." + "value": "Jelajahi data Anda lebih dalam dengan menggunakan segmen dan filter." } ], "label.is": [ { "type": 0, - "value": "Is" + "value": "Adalah" } ], "label.is-not": [ { "type": 0, - "value": "Is not" + "value": "Bukan" } ], "label.is-not-set": [ { "type": 0, - "value": "Is not set" + "value": "Tidak diatur" } ], "label.is-set": [ { "type": 0, - "value": "Is set" + "value": "Diatur" } ], "label.join": [ { "type": 0, - "value": "Join" + "value": "Gabung" } ], "label.join-team": [ { "type": 0, - "value": "Join team" + "value": "Gabung tim" } ], "label.journey": [ { "type": 0, - "value": "Journey" + "value": "Perjalanan" } ], "label.journey-description": [ { "type": 0, - "value": "Understand how users navigate through your website." + "value": "Pahami bagaimana pengguna menavigasi situs web Anda." } ], "label.language": [ @@ -632,47 +632,43 @@ } ], "label.last-months": [ - { - "type": 0, - "value": "Last " - }, { "type": 1, "value": "x" }, { "type": 0, - "value": " months" + "value": " bulan terakhir" } ], "label.last-seen": [ { "type": 0, - "value": "Last seen" + "value": "Terakhir kali dilihat" } ], "label.leave": [ { "type": 0, - "value": "Leave" + "value": "Keluar" } ], "label.leave-team": [ { "type": 0, - "value": "Leave team" + "value": "Keluar dari tim" } ], "label.less-than": [ { "type": 0, - "value": "Less than" + "value": "Kurang dari" } ], "label.less-than-equals": [ { "type": 0, - "value": "Less than or equals" + "value": "Kurang dari atau sama dengan" } ], "label.login": [ @@ -690,31 +686,31 @@ "label.manage": [ { "type": 0, - "value": "Manage" + "value": "Kelola" } ], "label.manager": [ { "type": 0, - "value": "Manager" + "value": "Pengelola" } ], "label.max": [ { "type": 0, - "value": "Max" + "value": "Maks" } ], "label.member": [ { "type": 0, - "value": "Member" + "value": "Anggota" } ], "label.members": [ { "type": 0, - "value": "Members" + "value": "Anggota" } ], "label.min": [ @@ -738,13 +734,13 @@ "label.my-account": [ { "type": 0, - "value": "My account" + "value": "Akun saya" } ], "label.my-websites": [ { "type": 0, - "value": "My websites" + "value": "Situs web saya" } ], "label.name": [ @@ -762,7 +758,7 @@ "label.none": [ { "type": 0, - "value": "None" + "value": "Tidak ada" } ], "label.number-of-records": [ @@ -814,7 +810,7 @@ "label.overview": [ { "type": 0, - "value": "Overview" + "value": "Tinjauan umum" } ], "label.owner": [ @@ -826,7 +822,7 @@ "label.page-of": [ { "type": 0, - "value": "Page " + "value": "Halaman " }, { "type": 1, @@ -834,7 +830,7 @@ }, { "type": 0, - "value": " of " + "value": " dari " }, { "type": 1, @@ -850,7 +846,7 @@ "label.pageTitle": [ { "type": 0, - "value": "Page title" + "value": "Judul halaman" } ], "label.pages": [ @@ -890,19 +886,19 @@ "label.previous": [ { "type": 0, - "value": "Previous" + "value": "Sebelumnya" } ], "label.previous-period": [ { "type": 0, - "value": "Previous period" + "value": "Periode sebelumnya" } ], "label.previous-year": [ { "type": 0, - "value": "Previous year" + "value": "Tahun lalu" } ], "label.profile": [ @@ -950,7 +946,7 @@ "label.referrer": [ { "type": 0, - "value": "Referrer" + "value": "Perujuk" } ], "label.referrers": [ @@ -968,37 +964,37 @@ "label.regenerate": [ { "type": 0, - "value": "Regenerate" + "value": "Buat ulang" } ], "label.region": [ { "type": 0, - "value": "Region" + "value": "Wilayah" } ], "label.regions": [ { "type": 0, - "value": "Regions" + "value": "Wilayah" } ], "label.remove": [ { "type": 0, - "value": "Remove" + "value": "Hapus" } ], "label.remove-member": [ { "type": 0, - "value": "Remove member" + "value": "Hapus anggota" } ], "label.reports": [ { "type": 0, - "value": "Reports" + "value": "Laporan" } ], "label.required": [ @@ -1022,31 +1018,31 @@ "label.retention": [ { "type": 0, - "value": "Retention" + "value": "Retensi" } ], "label.retention-description": [ { "type": 0, - "value": "Measure your website stickiness by tracking how often users return." + "value": "Ukur daya tarik situs web Anda dengan melacak seberapa sering pengguna kembali." } ], "label.revenue": [ { "type": 0, - "value": "Revenue" + "value": "Pendapatan" } ], "label.revenue-description": [ { "type": 0, - "value": "Look into your revenue across time." + "value": "Lihat pendapatan Anda seiring waktu." } ], "label.revenue-property": [ { "type": 0, - "value": "Revenue Property" + "value": "Properti pendapatan" } ], "label.role": [ @@ -1076,43 +1072,43 @@ "label.search": [ { "type": 0, - "value": "Search" + "value": "Cari" } ], "label.select": [ { "type": 0, - "value": "Select" + "value": "Pilih" } ], "label.select-date": [ { "type": 0, - "value": "Select date" + "value": "Pilih tanggal" } ], "label.select-role": [ { "type": 0, - "value": "Select role" + "value": "Pilih role" } ], "label.select-website": [ { "type": 0, - "value": "Select website" + "value": "Pilih situs web" } ], "label.session": [ { "type": 0, - "value": "Session" + "value": "Sesi" } ], "label.sessions": [ { "type": 0, - "value": "Sessions" + "value": "Sesi" } ], "label.settings": [ @@ -1136,13 +1132,13 @@ "label.start-step": [ { "type": 0, - "value": "Start Step" + "value": "Langkah awal" } ], "label.steps": [ { "type": 0, - "value": "Steps" + "value": "Langkah" } ], "label.sum": [ @@ -1160,37 +1156,37 @@ "label.team": [ { "type": 0, - "value": "Team" + "value": "Tim" } ], "label.team-id": [ { "type": 0, - "value": "Team ID" + "value": "ID tim" } ], "label.team-manager": [ { "type": 0, - "value": "Team manager" + "value": "Pengelola tim" } ], "label.team-member": [ { "type": 0, - "value": "Team member" + "value": "Anggota tim" } ], "label.team-name": [ { "type": 0, - "value": "Team name" + "value": "Nama tim" } ], "label.team-owner": [ { "type": 0, - "value": "Team owner" + "value": "Pemilik tim" } ], "label.team-view-only": [ @@ -1202,13 +1198,13 @@ "label.team-websites": [ { "type": 0, - "value": "Team websites" + "value": "Situs web tim" } ], "label.teams": [ { "type": 0, - "value": "Teams" + "value": "Tim" } ], "label.theme": [ @@ -1244,7 +1240,7 @@ "label.title": [ { "type": 0, - "value": "Title" + "value": "Judul" } ], "label.today": [ @@ -1268,7 +1264,7 @@ "label.total-records": [ { "type": 0, - "value": "Total records" + "value": "Total baris" } ], "label.tracking-code": [ @@ -1280,7 +1276,7 @@ "label.transactions": [ { "type": 0, - "value": "Transactions" + "value": "Transaksi" } ], "label.transfer": [ @@ -1292,25 +1288,25 @@ "label.transfer-website": [ { "type": 0, - "value": "Transfer website" + "value": "Transfer situs web" } ], "label.true": [ { "type": 0, - "value": "True" + "value": "Benar" } ], "label.type": [ { "type": 0, - "value": "Type" + "value": "Tipe" } ], "label.unique": [ { "type": 0, - "value": "Unique" + "value": "Unik" } ], "label.unique-visitors": [ @@ -1322,7 +1318,7 @@ "label.uniqueCustomers": [ { "type": 0, - "value": "Unique Customers" + "value": "Kustomer unik" } ], "label.unknown": [ @@ -1334,13 +1330,13 @@ "label.untitled": [ { "type": 0, - "value": "Untitled" + "value": "Tanpa judul" } ], "label.update": [ { "type": 0, - "value": "Update" + "value": "Perbarui" } ], "label.url": [ @@ -1358,7 +1354,7 @@ "label.user": [ { "type": 0, - "value": "User" + "value": "Pengguna" } ], "label.user-property": [ @@ -1376,7 +1372,7 @@ "label.users": [ { "type": 0, - "value": "Users" + "value": "Pengguna" } ], "label.utm": [ @@ -1388,19 +1384,19 @@ "label.utm-description": [ { "type": 0, - "value": "Track your campaigns through UTM parameters." + "value": "Lacak kampanye Anda melalui parameter UTM." } ], "label.value": [ { "type": 0, - "value": "Value" + "value": "Nilai" } ], "label.view": [ { "type": 0, - "value": "View" + "value": "Lihat" } ], "label.view-details": [ @@ -1412,7 +1408,7 @@ "label.view-only": [ { "type": 0, - "value": "View only" + "value": "Hanya melihat" } ], "label.views": [ @@ -1424,7 +1420,7 @@ "label.views-per-visit": [ { "type": 0, - "value": "Views per visit" + "value": "Tampilan per kunjungan" } ], "label.visit-duration": [ @@ -1442,19 +1438,19 @@ "label.visits": [ { "type": 0, - "value": "Visits" + "value": "Kunjungan" } ], "label.website": [ { "type": 0, - "value": "Website" + "value": "Situs web" } ], "label.website-id": [ { "type": 0, - "value": "Website ID" + "value": "ID situs web" } ], "label.websites": [ @@ -1472,13 +1468,13 @@ "label.yesterday": [ { "type": 0, - "value": "Yesterday" + "value": "Kemarin" } ], "message.action-confirmation": [ { "type": 0, - "value": "Type " + "value": "Ketik " }, { "type": 1, @@ -1486,7 +1482,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " pada kotak di bawah untuk mengonfirmasi." } ], "message.active-users": [ @@ -1502,7 +1498,7 @@ "message.collected-data": [ { "type": 0, - "value": "Collected data" + "value": "Data dikumpulkan" } ], "message.confirm-delete": [ @@ -1522,7 +1518,7 @@ "message.confirm-leave": [ { "type": 0, - "value": "Are you sure you want to leave " + "value": "Apakah Anda yakin ingin meninggalkan " }, { "type": 1, @@ -1536,7 +1532,7 @@ "message.confirm-remove": [ { "type": 0, - "value": "Are you sure you want to remove " + "value": "Apakah Anda yakin ingin menghapus " }, { "type": 1, @@ -1564,7 +1560,7 @@ "message.delete-team-warning": [ { "type": 0, - "value": "Deleting a team will also delete all team websites." + "value": "Menghapus tim juga akan menghapus semua situs web yang terkait." } ], "message.delete-website-warning": [ @@ -1614,7 +1610,7 @@ "message.min-password-length": [ { "type": 0, - "value": "Minimum length of " + "value": "Minimal " }, { "type": 1, @@ -1622,13 +1618,13 @@ }, { "type": 0, - "value": " characters" + "value": " karakter" } ], "message.new-version-available": [ { "type": 0, - "value": "A new version of Umami " + "value": "Versi baru dari Umami " }, { "type": 1, @@ -1636,7 +1632,7 @@ }, { "type": 0, - "value": " is available!" + "value": " telah tersedia!" } ], "message.no-data-available": [ @@ -1648,7 +1644,7 @@ "message.no-event-data": [ { "type": 0, - "value": "No event data is available." + "value": "Tidak ada data peristiwa" } ], "message.no-match-password": [ @@ -1660,25 +1656,25 @@ "message.no-results-found": [ { "type": 0, - "value": "No results were found." + "value": "Tidak ada hasil yang ditemukan." } ], "message.no-team-websites": [ { "type": 0, - "value": "This team does not have any websites." + "value": "Tim ini tidak memiliki situs web." } ], "message.no-teams": [ { "type": 0, - "value": "You have not created any teams." + "value": "Anda belum membuat tim." } ], "message.no-users": [ { "type": 0, - "value": "There are no users." + "value": "Tidak ada pengguna." } ], "message.no-websites-configured": [ @@ -1696,7 +1692,7 @@ "message.reset-website": [ { "type": 0, - "value": "To reset this website, type " + "value": "Untuk mengatur ulang situs web ini, ketik " }, { "type": 1, @@ -1704,13 +1700,13 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " pada kotak di bawah untuk mengonfirmasi." } ], "message.reset-website-warning": [ { "type": 0, - "value": "Semua statistik pada website ini akan dihapus, tetapi kode lacak akan tetap terpasang" + "value": "Semua statistik pada situs web ini akan dihapus, tetapi kode lacak akan tetap terpasang" } ], "message.saved": [ @@ -1736,19 +1732,19 @@ "message.team-already-member": [ { "type": 0, - "value": "You are already a member of the team." + "value": "Anda sudah menjadi anggota tim ini." } ], "message.team-not-found": [ { "type": 0, - "value": "Team not found." + "value": "Tim tidak ditemukan." } ], "message.team-websites-info": [ { "type": 0, - "value": "Websites can be viewed by anyone on the team." + "value": "Situs web dapat dilihat oleh semua anggota tim." } ], "message.tracking-code": [ @@ -1760,37 +1756,37 @@ "message.transfer-team-website-to-user": [ { "type": 0, - "value": "Transfer this website to your account?" + "value": "Transfer situs web ini ke akun Anda?" } ], "message.transfer-user-website-to-team": [ { "type": 0, - "value": "Select the team to transfer this website to." + "value": "Pilih tim tujuan untuk mentransfer situs web ini." } ], "message.transfer-website": [ { "type": 0, - "value": "Transfer website ownership to your account or another team." + "value": "Transfer kepemilikan situs web ke akun Anda atau tim lain" } ], "message.triggered-event": [ { "type": 0, - "value": "Triggered event" + "value": "Peristiwa terjadi" } ], "message.user-deleted": [ { "type": 0, - "value": "User deleted." + "value": "Pengguna telah dihapus." } ], "message.viewed-page": [ { "type": 0, - "value": "Viewed page" + "value": "Halaman dilihat" } ], "message.visitor-log": [ @@ -1830,7 +1826,7 @@ "message.visitors-dropped-off": [ { "type": 0, - "value": "Visitors dropped off" + "value": "Pengunjung yang meninggalkan situs web" } ] } diff --git a/public/intl/messages/mn-MN.json b/public/intl/messages/mn-MN.json index f1a76b9b8..85527896f 100644 --- a/public/intl/messages/mn-MN.json +++ b/public/intl/messages/mn-MN.json @@ -32,13 +32,13 @@ "label.add-member": [ { "type": 0, - "value": "Add member" + "value": "Гишүүн нэмэх" } ], "label.add-step": [ { "type": 0, - "value": "Add step" + "value": "Алхам нэмэх" } ], "label.add-website": [ @@ -74,7 +74,7 @@ "label.analytics": [ { "type": 0, - "value": "Analytics" + "value": "Аналитик" } ], "label.average": [ @@ -152,7 +152,7 @@ "label.compare": [ { "type": 0, - "value": "Compare" + "value": "Харьцуулах" } ], "label.confirm": [ @@ -182,7 +182,7 @@ "label.count": [ { "type": 0, - "value": "Count" + "value": "Тоо" } ], "label.countries": [ @@ -230,13 +230,13 @@ "label.created-by": [ { "type": 0, - "value": "Created By" + "value": "Үүсгэсэн" } ], "label.current": [ { "type": 0, - "value": "Current" + "value": "Одоогийн" } ], "label.current-password": [ @@ -296,7 +296,7 @@ "label.delete-report": [ { "type": 0, - "value": "Delete report" + "value": "Тайлан устгах" } ], "label.delete-team": [ @@ -386,7 +386,7 @@ "label.edit-member": [ { "type": 0, - "value": "Edit member" + "value": "Гишүүн засах" } ], "label.enable-share-url": [ @@ -398,13 +398,13 @@ "label.end-step": [ { "type": 0, - "value": "End Step" + "value": "Төгсгөлийн алхам" } ], "label.entry": [ { "type": 0, - "value": "Entry URL" + "value": "Орох зам" } ], "label.event": [ @@ -428,7 +428,7 @@ "label.exit": [ { "type": 0, - "value": "Exit URL" + "value": "Гарах зам" } ], "label.false": [ @@ -476,7 +476,7 @@ "label.first-seen": [ { "type": 0, - "value": "First seen" + "value": "Анх харсан" } ], "label.funnel": [ @@ -494,19 +494,19 @@ "label.goal": [ { "type": 0, - "value": "Goal" + "value": "Зорилго" } ], "label.goals": [ { "type": 0, - "value": "Goals" + "value": "Зорилго" } ], "label.goals-description": [ { "type": 0, - "value": "Track your goals for pageviews and events." + "value": "Хуудас үзсэн болон үйлдлийн зорилгыг мөрдөх." } ], "label.greater-than": [ @@ -524,13 +524,13 @@ "label.host": [ { "type": 0, - "value": "Host" + "value": "Хост" } ], "label.hosts": [ { "type": 0, - "value": "Hosts" + "value": "Хост" } ], "label.insights": [ @@ -584,13 +584,13 @@ "label.journey": [ { "type": 0, - "value": "Journey" + "value": "Аялал" } ], "label.journey-description": [ { "type": 0, - "value": "Understand how users navigate through your website." + "value": "Хэрэглэгчид таны цахим хуудсаар хэрхэн шилжиж явсныг шинжлэх." } ], "label.language": [ @@ -642,7 +642,7 @@ "label.last-months": [ { "type": 0, - "value": "Last " + "value": "Сүүлийн " }, { "type": 1, @@ -650,13 +650,13 @@ }, { "type": 0, - "value": " months" + "value": " сар" } ], "label.last-seen": [ { "type": 0, - "value": "Last seen" + "value": "Сүүлд харагдсан" } ], "label.leave": [ @@ -698,13 +698,13 @@ "label.manage": [ { "type": 0, - "value": "Manage" + "value": "Удирдах" } ], "label.manager": [ { "type": 0, - "value": "Manager" + "value": "Удирдагч" } ], "label.max": [ @@ -716,7 +716,7 @@ "label.member": [ { "type": 0, - "value": "Member" + "value": "Гишүүн" } ], "label.members": [ @@ -746,7 +746,7 @@ "label.my-account": [ { "type": 0, - "value": "My account" + "value": "Миний бүртгэл" } ], "label.my-websites": [ @@ -789,7 +789,7 @@ "value": [ { "type": 0, - "value": "record" + "value": "бичлэг" } ] }, @@ -797,7 +797,7 @@ "value": [ { "type": 0, - "value": "records" + "value": "бичлэг" } ] } @@ -810,7 +810,7 @@ "label.ok": [ { "type": 0, - "value": "OK" + "value": "ЗА" } ], "label.os": [ @@ -876,13 +876,13 @@ "label.path": [ { "type": 0, - "value": "Path" + "value": "Зам" } ], "label.paths": [ { "type": 0, - "value": "Paths" + "value": "Зам" } ], "label.powered-by": [ @@ -898,19 +898,19 @@ "label.previous": [ { "type": 0, - "value": "Previous" + "value": "Өмнөх" } ], "label.previous-period": [ { "type": 0, - "value": "Previous period" + "value": "Өмнөх үе" } ], "label.previous-year": [ { "type": 0, - "value": "Previous year" + "value": "Өмнөх жил" } ], "label.profile": [ @@ -922,13 +922,13 @@ "label.properties": [ { "type": 0, - "value": "Properties" + "value": "Шинж чанар" } ], "label.property": [ { "type": 0, - "value": "Property" + "value": "Шинж чанар" } ], "label.queries": [ @@ -1000,7 +1000,7 @@ "label.remove-member": [ { "type": 0, - "value": "Remove member" + "value": "Гишүүн хасах" } ], "label.reports": [ @@ -1042,19 +1042,19 @@ "label.revenue": [ { "type": 0, - "value": "Revenue" + "value": "Орлого" } ], "label.revenue-description": [ { "type": 0, - "value": "Look into your revenue across time." + "value": "Цаг хугацааны туршид орлогын өөрчлөлтийг харах." } ], "label.revenue-property": [ { "type": 0, - "value": "Revenue Property" + "value": "Орлогын шинж чанар" } ], "label.role": [ @@ -1090,7 +1090,7 @@ "label.select": [ { "type": 0, - "value": "Select" + "value": "Сонгох" } ], "label.select-date": [ @@ -1144,13 +1144,13 @@ "label.start-step": [ { "type": 0, - "value": "Start Step" + "value": "Эхлэх алхам" } ], "label.steps": [ { "type": 0, - "value": "Steps" + "value": "Алхам" } ], "label.sum": [ @@ -1180,7 +1180,7 @@ "label.team-manager": [ { "type": 0, - "value": "Team manager" + "value": "Багийн удирдагч" } ], "label.team-member": [ @@ -1294,13 +1294,13 @@ "label.transfer": [ { "type": 0, - "value": "Transfer" + "value": "Шилжүүлэх" } ], "label.transfer-website": [ { "type": 0, - "value": "Transfer website" + "value": "Вебийг шилжүүлэх" } ], "label.true": [ @@ -1330,7 +1330,7 @@ "label.uniqueCustomers": [ { "type": 0, - "value": "Unique Customers" + "value": "Давтагдаагүй зочин" } ], "label.unknown": [ @@ -1348,7 +1348,7 @@ "label.update": [ { "type": 0, - "value": "Update" + "value": "Шинэчлэх" } ], "label.url": [ @@ -1360,7 +1360,7 @@ "label.urls": [ { "type": 0, - "value": "URLs" + "value": "URL-ууд" } ], "label.user": [ @@ -1372,7 +1372,7 @@ "label.user-property": [ { "type": 0, - "value": "User Property" + "value": "Хэрэглэгчийн шинж" } ], "label.username": [ @@ -1396,7 +1396,7 @@ "label.utm-description": [ { "type": 0, - "value": "Track your campaigns through UTM parameters." + "value": "UTM параметраар кампанит ажлаа мөрдөх." } ], "label.value": [ @@ -1432,7 +1432,7 @@ "label.views-per-visit": [ { "type": 0, - "value": "Views per visit" + "value": "Зочдын хуудас үзсэн тоо" } ], "label.visit-duration": [ @@ -1450,7 +1450,7 @@ "label.visits": [ { "type": 0, - "value": "Visits" + "value": "Зочилсон" } ], "label.website": [ @@ -1486,7 +1486,7 @@ "message.action-confirmation": [ { "type": 0, - "value": "Type " + "value": "Доорх хэсэгт " }, { "type": 1, @@ -1494,7 +1494,7 @@ }, { "type": 0, - "value": " in the box below to confirm." + "value": " гэж бичин баталгаажуулна уу." } ], "message.active-users": [ @@ -1542,7 +1542,7 @@ "message.collected-data": [ { "type": 0, - "value": "Collected data" + "value": "Цуглуулсан өгөгдөл" } ], "message.confirm-delete": [ @@ -1576,7 +1576,7 @@ "message.confirm-remove": [ { "type": 0, - "value": "Are you sure you want to remove " + "value": "Та " }, { "type": 1, @@ -1584,7 +1584,7 @@ }, { "type": 0, - "value": "?" + "value": "-г устгахдаа итгэлтэй байна уу?" } ], "message.confirm-reset": [ @@ -1604,7 +1604,7 @@ "message.delete-team-warning": [ { "type": 0, - "value": "Deleting a team will also delete all team websites." + "value": "Баг устгах нь мөн түүнд харъяалагдах вебүүдийг устгах болно." } ], "message.delete-website-warning": [ @@ -1806,25 +1806,25 @@ "message.transfer-team-website-to-user": [ { "type": 0, - "value": "Transfer this website to your account?" + "value": "Энэ вебийг өөрийн бүртгэл рүү шилжүүлэх үү?" } ], "message.transfer-user-website-to-team": [ { "type": 0, - "value": "Select the team to transfer this website to." + "value": "Энэ вебийг шилжүүлж авах багийг сонгоно уу." } ], "message.transfer-website": [ { "type": 0, - "value": "Transfer website ownership to your account or another team." + "value": "Энэ вебийг өөрийн бүртгэл рүү эсвэл багт шилжүүлж авах." } ], "message.triggered-event": [ { "type": 0, - "value": "Triggered event" + "value": "Өдөөсөн үйлдэл" } ], "message.user-deleted": [ @@ -1836,7 +1836,7 @@ "message.viewed-page": [ { "type": 0, - "value": "Viewed page" + "value": "Үзсэн хуудас" } ], "message.visitor-log": [ @@ -1876,7 +1876,7 @@ "message.visitors-dropped-off": [ { "type": 0, - "value": "Visitors dropped off" + "value": "Зочдын уналт" } ] } From dc084b78a875d0bcb875a856f5736fefb8046241 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 21 Feb 2025 17:47:59 -0800 Subject: [PATCH 05/28] Fixed user create. --- package.json | 2 +- src/app/api/users/route.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 275f1408f..870769364 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.16.0", + "version": "2.16.1", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 320f72bd7..f6b32fe7e 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -10,7 +10,6 @@ export async function POST(request: Request) { const schema = z.object({ username: z.string().max(255), password: z.string(), - id: z.string().uuid(), role: z.string().regex(/admin|user|view-only/i), }); @@ -24,7 +23,7 @@ export async function POST(request: Request) { return unauthorized(); } - const { username, password, role, id } = body; + const { username, password, role } = body; const existingUser = await getUserByUsername(username, { showDeleted: true }); @@ -33,7 +32,7 @@ export async function POST(request: Request) { } const user = await createUser({ - id: id || uuid(), + id: uuid(), username, password: hashPassword(password), role: role ?? ROLES.user, From 0aad3d8e05a1bf1620c2013eaf51adb87b176e15 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 21 Feb 2025 18:11:52 -0800 Subject: [PATCH 06/28] Lookup location for payload IPs. Removed hostname from session id. --- src/app/api/send/route.ts | 2 +- src/lib/detect.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 2d220f38c..933ef78e2 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -87,7 +87,7 @@ export async function POST(request: Request) { return forbidden(); } - const sessionId = uuid(websiteId, hostname, ip, userAgent); + const sessionId = uuid(websiteId, ip, userAgent); // Find session if (!clickhouse.enabled && !cache?.sessionId) { diff --git a/src/lib/detect.ts b/src/lib/detect.ts index cd91069e4..9d9fd7db1 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -86,13 +86,13 @@ function decodeHeader(s: string | undefined | null): string | undefined | null { return Buffer.from(s, 'latin1').toString('utf-8'); } -export async function getLocation(ip: string = '', headers: Headers) { +export async function getLocation(ip: string = '', headers: Headers, hasPayloadIP: boolean) { // Ignore local ips if (await isLocalhost(ip)) { return; } - if (!process.env.SKIP_LOCATION_HEADERS) { + if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) { // Cloudflare headers if (headers.get('cf-ipcountry')) { const country = decodeHeader(headers.get('cf-ipcountry')); @@ -147,7 +147,7 @@ export async function getLocation(ip: string = '', headers: Headers) { export async function getClientInfo(request: Request, payload: Record) { const userAgent = payload?.userAgent || request.headers.get('user-agent'); const ip = payload?.ip || getIpAddress(request.headers); - const location = await getLocation(ip, request.headers); + const location = await getLocation(ip, request.headers, !!payload?.ip); const country = payload?.userAgent || location?.country; const subdivision1 = location?.subdivision1; const subdivision2 = location?.subdivision2; From a7ded80fb2073b288695351b666cf18a3e2ccafb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 21 Feb 2025 18:33:42 -0800 Subject: [PATCH 07/28] Fixed channels query. Closes #3245 --- src/app/api/websites/[websiteId]/metrics/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index c09587394..1c3c804c7 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -125,7 +125,7 @@ function getChannels(data: { domain: string; query: string; visitors: number }[] const match = (value: string) => { return (str: string | RegExp) => { - return typeof str === 'string' ? value.includes(str) : (str as RegExp).test(value); + return typeof str === 'string' ? value?.includes(str) : (str as RegExp).test(value); }; }; From 75b0b2e67753656ba1a6c266c235fbf294495501 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 22 Feb 2025 08:09:01 -0800 Subject: [PATCH 08/28] Make user id optional. --- src/app/api/users/route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index f6b32fe7e..c5896f892 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -8,6 +8,7 @@ import { createUser, getUserByUsername } from '@/queries'; export async function POST(request: Request) { const schema = z.object({ + id: z.string().uuid().optional(), username: z.string().max(255), password: z.string(), role: z.string().regex(/admin|user|view-only/i), @@ -23,7 +24,7 @@ export async function POST(request: Request) { return unauthorized(); } - const { username, password, role } = body; + const { id, username, password, role } = body; const existingUser = await getUserByUsername(username, { showDeleted: true }); @@ -32,7 +33,7 @@ export async function POST(request: Request) { } const user = await createUser({ - id: uuid(), + id: id || uuid(), username, password: hashPassword(password), role: role ?? ROLES.user, From bdeaa9e5c667a30d75ba827faede8538426cb945 Mon Sep 17 00:00:00 2001 From: Harry Oosterveen Date: Tue, 25 Feb 2025 12:33:02 +0100 Subject: [PATCH 09/28] Fix duplicate key errors --- src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx | 3 ++- .../(main)/websites/[websiteId]/sessions/SessionsWeekly.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index bb0225cc7..26c921e43 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -71,9 +71,10 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { if (__type === TYPE_EVENT) { return formatMessage(messages.eventLog, { - event: {eventName || formatMessage(labels.unknown)}, + event: {eventName || formatMessage(labels.unknown)}, url: ( {format(getDayOfWeekAsDate(index), 'EEE', { locale: dateLocale })} - {day?.map((hour: number) => { + {day?.map((hour: number, j) => { const pct = hour / max; return ( -
+
{hour > 0 && ( Date: Tue, 25 Feb 2025 13:17:53 +0100 Subject: [PATCH 10/28] Add keys for RealTime Session events --- .../(main)/websites/[websiteId]/realtime/RealtimeLog.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx index 26c921e43..6a2b3c25c 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeLog.tsx @@ -101,10 +101,10 @@ export function RealtimeLog({ data }: { data: RealtimeData }) { if (__type === TYPE_SESSION) { return formatMessage(messages.visitorLog, { - country: {countryNames[country] || formatMessage(labels.unknown)}, - browser: {BROWSERS[browser]}, - os: {OS_NAMES[os] || os}, - device: {formatMessage(labels[device] || labels.unknown)}, + country: {countryNames[country] || formatMessage(labels.unknown)}, + browser: {BROWSERS[browser]}, + os: {OS_NAMES[os] || os}, + device: {formatMessage(labels[device] || labels.unknown)}, }); } }; From 796f6d448c835e06c993a0a2d2d8f8396fc4a0b9 Mon Sep 17 00:00:00 2001 From: Shrutesh Date: Wed, 26 Feb 2025 11:49:33 +0530 Subject: [PATCH 11/28] Update batch.ts --- src/pages/api/batch.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/api/batch.ts b/src/pages/api/batch.ts index 93632d114..0557d4da3 100644 --- a/src/pages/api/batch.ts +++ b/src/pages/api/batch.ts @@ -22,7 +22,13 @@ export default async function handler(req, res) { const mockRes = { ...res, - end: () => {}, // Prevent premature response closure + status: (code) => { + res.status(code); + return mockRes; + }, + json: (data) => res.json(data), + setHeader: (key, value) => res.setHeader(key, value), + end: () => {}, }; await sendHandler(mockReq, mockRes); From 4c45285010e4ac9ff0bf2baa11ce6f07871e64c3 Mon Sep 17 00:00:00 2001 From: Harry Oosterveen Date: Thu, 27 Feb 2025 14:42:04 +0100 Subject: [PATCH 12/28] Fix https://github.com/umami-software/umami/issues/3255 --- src/lib/prisma.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index e2f50a6c7..c8286082e 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -192,7 +192,9 @@ async function parseFilters( options: QueryOptions = {}, ) { const website = await fetchWebsite(websiteId); - const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key)); + const joinSession = Object.keys(filters).find(key => + ['referrer', ...SESSION_COLUMNS].includes(key), + ); return { joinSession: From 0d153a27dcab298fa5ec586543288d1b0b9ac6cb Mon Sep 17 00:00:00 2001 From: Louis Vallat Date: Sat, 1 Mar 2025 00:00:50 +0100 Subject: [PATCH 13/28] feat: add CORS headers to any value of COLLECT_API_ENDPOINT in addition to /api/* endpoints Signed-off-by: Louis Vallat --- next.config.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/next.config.js b/next.config.js index 7a65c4727..590d7121b 100644 --- a/next.config.js +++ b/next.config.js @@ -59,15 +59,29 @@ const trackerHeaders = [ }, ]; +const apiHeaders = [ + { + key: 'Access-Control-Allow-Origin', + value: '*' + }, + { + key: 'Access-Control-Allow-Headers', + value: '*' + }, + { + key: 'Access-Control-Allow-Methods', + value: 'GET, DELETE, POST, PUT' + }, + { + key: 'Access-Control-Max-Age', + value: corsMaxAge || '86400' + }, +]; + const headers = [ { source: '/api/:path*', - headers: [ - { key: 'Access-Control-Allow-Origin', value: '*' }, - { key: 'Access-Control-Allow-Headers', value: '*' }, - { key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' }, - { key: 'Access-Control-Max-Age', value: corsMaxAge || '86400' }, - ], + headers: apiHeaders }, { source: '/:path*', @@ -89,6 +103,11 @@ if (trackerScriptURL) { } if (collectApiEndpoint) { + headers.push({ + source: collectApiEndpoint, + headers: apiHeaders, + }); + rewrites.push({ source: collectApiEndpoint, destination: '/api/send', From a8835f385e69fcb208feb8e0741697430cf84329 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 28 Feb 2025 16:58:57 -0800 Subject: [PATCH 14/28] Refactored batch route. --- src/app/api/batch/route.ts | 39 ++++++++++++++++++++++++++++++++++++ src/app/api/send/route.ts | 2 +- src/lib/request.ts | 4 ++-- src/pages/api/batch.ts | 41 -------------------------------------- 4 files changed, 42 insertions(+), 44 deletions(-) create mode 100644 src/app/api/batch/route.ts delete mode 100644 src/pages/api/batch.ts diff --git a/src/app/api/batch/route.ts b/src/app/api/batch/route.ts new file mode 100644 index 000000000..87e04110d --- /dev/null +++ b/src/app/api/batch/route.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import * as send from '@/app/api/send/route'; +import { parseRequest } from '@/lib/request'; +import { json, serverError } from '@/lib/response'; + +const schema = z.array(z.object({}).passthrough()); + +export async function POST(request: Request) { + try { + const { body, error } = await parseRequest(request, schema, { skipAuth: true }); + + if (error) { + return error(); + } + + const errors = []; + + let index = 0; + for (const data of body) { + const newRequest = new Request(request, { body: JSON.stringify(data) }); + const response = await send.POST(newRequest); + + if (!response.ok) { + errors.push({ index, response: await response.json() }); + } + + index++; + } + + return json({ + size: body.length, + processed: body.length - errors.length, + errors: errors.length, + details: errors, + }); + } catch (e) { + return serverError(e); + } +} diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 933ef78e2..80db8f96d 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -21,7 +21,7 @@ const schema = z.object({ referrer: urlOrPathParam.optional(), screen: z.string().max(11).optional(), title: z.string().optional(), - url: urlOrPathParam, + url: urlOrPathParam.optional(), name: z.string().max(50).optional(), tag: z.string().max(50).optional(), ip: z.string().ip().optional(), diff --git a/src/lib/request.ts b/src/lib/request.ts index 9d32f89b3..0c71537ae 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,4 +1,4 @@ -import { ZodObject } from 'zod'; +import { ZodSchema } from 'zod'; import { FILTER_COLUMNS } from '@/lib/constants'; import { badRequest, unauthorized } from '@/lib/response'; import { getAllowedUnits, getMinimumUnit } from '@/lib/date'; @@ -15,7 +15,7 @@ export async function getJsonBody(request: Request) { export async function parseRequest( request: Request, - schema?: ZodObject, + schema?: ZodSchema, options?: { skipAuth: boolean }, ): Promise { const url = new URL(request.url); diff --git a/src/pages/api/batch.ts b/src/pages/api/batch.ts deleted file mode 100644 index 0557d4da3..000000000 --- a/src/pages/api/batch.ts +++ /dev/null @@ -1,41 +0,0 @@ -import sendHandler from './send'; - -export default async function handler(req, res) { - if (req.method !== 'POST') { - res.setHeader('Allow', ['POST']); - return res.status(405).end(`Method ${req.method} Not Allowed`); - } - - const events = req.body; - - if (!Array.isArray(events)) { - return res.status(400).json({ error: 'Invalid payload, expected an array.' }); - } - - try { - for (const event of events) { - const mockReq = { - ...req, - body: event, - headers: { ...req.headers, origin: req.headers.origin || 'http://localhost:3000' }, - }; - - const mockRes = { - ...res, - status: (code) => { - res.status(code); - return mockRes; - }, - json: (data) => res.json(data), - setHeader: (key, value) => res.setHeader(key, value), - end: () => {}, - }; - - await sendHandler(mockReq, mockRes); - } - - return res.status(200).json({ success: true, message: `${events.length} events processed.` }); - } catch (error) { - return res.status(500).json({ error: 'Internal Server Error' }); - } -} From 05db1a8ba24b90ba9b3feb257c8e1ba23ec51d67 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 28 Feb 2025 17:37:56 -0800 Subject: [PATCH 15/28] Removed css rule. Fixes #3272 --- .eslintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.json b/.eslintrc.json index 82f6a122d..324e291cd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,7 @@ "react/prop-types": "off", "import/no-anonymous-default-export": "off", "import/no-named-as-default": "off", + "css-modules/no-unused-class": "off", "@next/next/no-img-element": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", From 9a87442870dec6314586b9571030464fef2926b3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 28 Feb 2025 21:04:53 -0800 Subject: [PATCH 16/28] Added SKIP_DB_MIGRATION var. --- scripts/check-db.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/check-db.js b/scripts/check-db.js index cdfeafa32..ca0fca31c 100644 --- a/scripts/check-db.js +++ b/scripts/check-db.js @@ -82,9 +82,11 @@ async function checkV1Tables() { } async function applyMigration() { - console.log(execSync('prisma migrate deploy').toString()); + if (!process.env.SKIP_DB_MIGRATION) { + console.log(execSync('prisma migrate deploy').toString()); - success('Database is up to date.'); + success('Database is up to date.'); + } } (async () => { From 30b28793cf524ef50d29a4859e4c034c8f5f4b43 Mon Sep 17 00:00:00 2001 From: David Ventura Date: Mon, 6 Jan 2025 13:46:38 +0100 Subject: [PATCH 17/28] Allow populating event's createdAt on the send endpoint --- src/app/api/send/route.ts | 11 +++++++---- src/queries/sql/events/saveEvent.ts | 12 +++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 80db8f96d..4189d7d91 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -26,6 +26,7 @@ const schema = z.object({ tag: z.string().max(50).optional(), ip: z.string().ip().optional(), userAgent: z.string().optional(), + createdAt: yup.number().optional(), }), }); @@ -55,6 +56,7 @@ export async function POST(request: Request) { data, title, tag, + reqCreatedAt, } = payload; // Cache check @@ -119,14 +121,14 @@ export async function POST(request: Request) { } // Visit info - const now = Math.floor(new Date().getTime() / 1000); + const createdAt = Math.floor((reqCreatedAt || new Date()).getTime() / 1000); let visitId = cache?.visitId || uuid(sessionId, visitSalt()); - let iat = cache?.iat || now; + let iat = cache?.iat || createdAt; // Expire visit after 30 minutes - if (now - iat > 1800) { + if (createdAt - iat > 1800) { visitId = uuid(sessionId, visitSalt()); - iat = now; + iat = createdAt; } if (type === COLLECTION_TYPE.event) { @@ -179,6 +181,7 @@ export async function POST(request: Request) { subdivision2, city, tag, + createdAt, }); } diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 65ee1175b..3b3f3a99d 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -29,6 +29,7 @@ export async function saveEvent(args: { subdivision2?: string; city?: string; tag?: string; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(args), @@ -49,6 +50,7 @@ async function relationalQuery(data: { eventName?: string; eventData?: any; tag?: string; + createdAt?: Date; }) { const { websiteId, @@ -63,6 +65,7 @@ async function relationalQuery(data: { eventData, pageTitle, tag, + createdAt, } = data; const websiteEventId = uuid(); @@ -80,6 +83,7 @@ async function relationalQuery(data: { pageTitle: pageTitle?.substring(0, PAGE_TITLE_LENGTH), eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, + createdAt, tag, }, }); @@ -121,6 +125,7 @@ async function clickhouseQuery(data: { subdivision2?: string; city?: string; tag?: string; + createdAt?: Date; }) { const { websiteId, @@ -139,12 +144,13 @@ async function clickhouseQuery(data: { subdivision2, city, tag, + createdAt, ...args } = data; const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; const eventId = uuid(); - const createdAt = getUTCString(); + const createdAtUTC = getUTCString(createdAt); const message = { ...args, @@ -170,7 +176,7 @@ async function clickhouseQuery(data: { event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag: tag, - created_at: createdAt, + created_at: createdAtUTC, }; if (kafka.enabled) { @@ -187,7 +193,7 @@ async function clickhouseQuery(data: { urlPath: urlPath?.substring(0, URL_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventData, - createdAt, + createdAt: createdAtUTC, }); } From 65f18d12ab91bb12870551a8bfbfcdaa467d5236 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Mar 2025 14:40:37 -0800 Subject: [PATCH 18/28] Added timestamp property to payload. --- next-env.d.ts | 2 +- src/app/api/send/route.ts | 13 +++++++++---- src/lib/schema.ts | 2 ++ src/queries/sql/events/saveEvent.ts | 10 ++++++++-- src/queries/sql/events/saveEventData.ts | 10 ++++++---- src/queries/sql/sessions/saveSessionData.ts | 11 +++++++---- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 40c3d6809..1b3be0840 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 80db8f96d..cc3b03cb9 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -7,16 +7,16 @@ import { badRequest, json, forbidden, serverError } from '@/lib/response'; import { fetchSession, fetchWebsite } from '@/lib/load'; import { getClientInfo, hasBlockedIp } from '@/lib/detect'; import { secret, uuid, visitSalt } from '@/lib/crypto'; -import { COLLECTION_TYPE, DOMAIN_REGEX } from '@/lib/constants'; +import { COLLECTION_TYPE } from '@/lib/constants'; +import { anyObjectParam, urlOrPathParam } from '@/lib/schema'; import { createSession, saveEvent, saveSessionData } from '@/queries'; -import { urlOrPathParam } from '@/lib/schema'; const schema = z.object({ type: z.enum(['event', 'identify']), payload: z.object({ website: z.string().uuid(), - data: z.object({}).passthrough().optional(), - hostname: z.string().regex(DOMAIN_REGEX).max(100).optional(), + data: anyObjectParam.optional(), + hostname: z.string().max(100).optional(), language: z.string().max(35).optional(), referrer: urlOrPathParam.optional(), screen: z.string().max(11).optional(), @@ -26,6 +26,7 @@ const schema = z.object({ tag: z.string().max(50).optional(), ip: z.string().ip().optional(), userAgent: z.string().optional(), + timestamp: z.coerce.number().int().optional(), }), }); @@ -55,6 +56,7 @@ export async function POST(request: Request) { data, title, tag, + timestamp, } = payload; // Cache check @@ -88,6 +90,7 @@ export async function POST(request: Request) { } const sessionId = uuid(websiteId, ip, userAgent); + const createdAt = timestamp ? new Date(timestamp * 1000) : new Date(); // Find session if (!clickhouse.enabled && !cache?.sessionId) { @@ -179,6 +182,7 @@ export async function POST(request: Request) { subdivision2, city, tag, + createdAt, }); } @@ -191,6 +195,7 @@ export async function POST(request: Request) { websiteId, sessionId, sessionData: data, + createdAt, }); } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 8df7be9fa..4e2b3e4a3 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -36,6 +36,8 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); +export const anyObjectParam = z.object({}).passthrough(); + export const urlOrPathParam = z.string().refine( value => { try { diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 65ee1175b..148b03f33 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -29,6 +29,7 @@ export async function saveEvent(args: { subdivision2?: string; city?: string; tag?: string; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(args), @@ -49,6 +50,7 @@ async function relationalQuery(data: { eventName?: string; eventData?: any; tag?: string; + createdAt?: Date; }) { const { websiteId, @@ -63,6 +65,7 @@ async function relationalQuery(data: { eventData, pageTitle, tag, + createdAt, } = data; const websiteEventId = uuid(); @@ -81,6 +84,7 @@ async function relationalQuery(data: { eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag, + createdAt, }, }); @@ -92,6 +96,7 @@ async function relationalQuery(data: { urlPath: urlPath?.substring(0, URL_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventData, + createdAt, }); } @@ -121,6 +126,7 @@ async function clickhouseQuery(data: { subdivision2?: string; city?: string; tag?: string; + createdAt?: Date; }) { const { websiteId, @@ -139,12 +145,12 @@ async function clickhouseQuery(data: { subdivision2, city, tag, + createdAt, ...args } = data; const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; const eventId = uuid(); - const createdAt = getUTCString(); const message = { ...args, @@ -170,7 +176,7 @@ async function clickhouseQuery(data: { event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag: tag, - created_at: createdAt, + created_at: getUTCString(createdAt), }; if (kafka.enabled) { diff --git a/src/queries/sql/events/saveEventData.ts b/src/queries/sql/events/saveEventData.ts index 7c158da40..16a5cab10 100644 --- a/src/queries/sql/events/saveEventData.ts +++ b/src/queries/sql/events/saveEventData.ts @@ -15,7 +15,7 @@ export async function saveEventData(data: { urlPath?: string; eventName?: string; eventData: DynamicData; - createdAt?: string; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(data), @@ -27,8 +27,9 @@ async function relationalQuery(data: { websiteId: string; eventId: string; eventData: DynamicData; + createdAt?: Date; }): Promise { - const { websiteId, eventId, eventData } = data; + const { websiteId, eventId, eventData, createdAt } = data; const jsonKeys = flattenJSON(eventData); @@ -42,6 +43,7 @@ async function relationalQuery(data: { numberValue: a.dataType === DATA_TYPE.number ? a.value : null, dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dataType, + createdAt, })); return prisma.client.eventData.createMany({ @@ -56,7 +58,7 @@ async function clickhouseQuery(data: { urlPath?: string; eventName?: string; eventData: DynamicData; - createdAt?: string; + createdAt?: Date; }) { const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data; @@ -77,7 +79,7 @@ async function clickhouseQuery(data: { string_value: getStringValue(value, dataType), number_value: dataType === DATA_TYPE.number ? value : null, date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null, - created_at: createdAt, + created_at: getUTCString(createdAt), }; }); diff --git a/src/queries/sql/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts index 35f0c7126..a060e9a84 100644 --- a/src/queries/sql/sessions/saveSessionData.ts +++ b/src/queries/sql/sessions/saveSessionData.ts @@ -11,6 +11,7 @@ export async function saveSessionData(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(data), @@ -22,9 +23,10 @@ export async function relationalQuery(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { const { client } = prisma; - const { websiteId, sessionId, sessionData } = data; + const { websiteId, sessionId, sessionData, createdAt } = data; const jsonKeys = flattenJSON(sessionData); @@ -37,6 +39,7 @@ export async function relationalQuery(data: { numberValue: a.dataType === DATA_TYPE.number ? a.value : null, dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dataType, + createdAt, })); const existing = await client.sessionData.findMany({ @@ -77,12 +80,12 @@ async function clickhouseQuery(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { - const { websiteId, sessionId, sessionData } = data; + const { websiteId, sessionId, sessionData, createdAt } = data; const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; - const createdAt = getUTCString(); const jsonKeys = flattenJSON(sessionData); @@ -95,7 +98,7 @@ async function clickhouseQuery(data: { string_value: getStringValue(value, dataType), number_value: dataType === DATA_TYPE.number ? value : null, date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null, - created_at: createdAt, + created_at: getUTCString(createdAt), }; }); From 925c7562153a0ef656cf5199e892e8d4e7616804 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Mar 2025 16:29:35 -0800 Subject: [PATCH 19/28] Updated salt methods. --- next-env.d.ts | 2 +- src/app/api/send/route.ts | 25 +++++++++++++++---------- src/lib/crypto.ts | 15 +-------------- src/queries/sql/events/saveEvent.ts | 3 +-- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 1b3be0840..40c3d6809 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index b9556ddd6..8519a73e1 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -1,12 +1,13 @@ import { z } from 'zod'; import { isbot } from 'isbot'; -import { createToken, parseToken } from '@/lib/jwt'; +import { startOfHour, startOfMonth } from 'date-fns'; import clickhouse from '@/lib/clickhouse'; import { parseRequest } from '@/lib/request'; import { badRequest, json, forbidden, serverError } from '@/lib/response'; import { fetchSession, fetchWebsite } from '@/lib/load'; import { getClientInfo, hasBlockedIp } from '@/lib/detect'; -import { secret, uuid, visitSalt } from '@/lib/crypto'; +import { createToken, parseToken } from '@/lib/jwt'; +import { secret, uuid, hash } from '@/lib/crypto'; import { COLLECTION_TYPE } from '@/lib/constants'; import { anyObjectParam, urlOrPathParam } from '@/lib/schema'; import { createSession, saveEvent, saveSessionData } from '@/queries'; @@ -89,8 +90,13 @@ export async function POST(request: Request) { return forbidden(); } - const sessionId = uuid(websiteId, ip, userAgent); const createdAt = timestamp ? new Date(timestamp * 1000) : new Date(); + const now = Math.floor(new Date().getTime() / 1000); + + const sessionSalt = hash(startOfMonth(createdAt).toUTCString()); + const visitSalt = hash(startOfHour(createdAt).toUTCString()); + + const sessionId = uuid(websiteId, ip, userAgent, sessionSalt); // Find session if (!clickhouse.enabled && !cache?.sessionId) { @@ -122,14 +128,13 @@ export async function POST(request: Request) { } // Visit info - const createdAt = Math.floor((reqCreatedAt || new Date()).getTime() / 1000); - let visitId = cache?.visitId || uuid(sessionId, visitSalt()); - let iat = cache?.iat || createdAt; + let visitId = cache?.visitId || uuid(sessionId, visitSalt); + let iat = cache?.iat || now; // Expire visit after 30 minutes - if (createdAt - iat > 1800) { - visitId = uuid(sessionId, visitSalt()); - iat = createdAt; + if (!timestamp && now - iat > 1800) { + visitId = uuid(sessionId, visitSalt); + iat = now; } if (type === COLLECTION_TYPE.event) { @@ -201,7 +206,7 @@ export async function POST(request: Request) { const token = createToken({ websiteId, sessionId, visitId, iat }, secret()); - return json({ cache: token }); + return json({ cache: token, sessionId, visitId }); } catch (e) { return serverError(e); } diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index a4ff3a526..d22bad091 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,5 +1,4 @@ import crypto from 'crypto'; -import { startOfHour, startOfMonth } from 'date-fns'; import prand from 'pure-rand'; import { v4, v5 } from 'uuid'; @@ -77,20 +76,8 @@ export function secret() { return hash(process.env.APP_SECRET || process.env.DATABASE_URL); } -export function salt() { - const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString()); - - return hash(secret(), ROTATING_SALT); -} - -export function visitSalt() { - const ROTATING_SALT = hash(startOfHour(new Date()).toUTCString()); - - return hash(secret(), ROTATING_SALT); -} - export function uuid(...args: any) { if (!args.length) return v4(); - return v5(hash(...args, salt()), v5.DNS); + return v5(hash(...args, secret()), v5.DNS); } diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 5df276e18..148b03f33 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -83,7 +83,6 @@ async function relationalQuery(data: { pageTitle: pageTitle?.substring(0, PAGE_TITLE_LENGTH), eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, - createdAt, tag, createdAt, }, @@ -194,7 +193,7 @@ async function clickhouseQuery(data: { urlPath: urlPath?.substring(0, URL_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventData, - createdAt: createdAtUTC, + createdAt, }); } From cb7eef200cb90a2447319558ecf2dfbeb4f3e718 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Mar 2025 17:18:46 -0800 Subject: [PATCH 20/28] Added check for do not track to tracker. --- src/tracker/index.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index dbd47b7c6..c423a66b5 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -1,11 +1,12 @@ (window => { const { screen: { width, height }, - navigator: { language }, + navigator: { language, doNotTrack: ndnt, msDoNotTrack: msdnt }, location, document, history, top, + doNotTrack, } = window; const { hostname, href, origin } = location; const { currentScript, referrer } = document; @@ -21,6 +22,7 @@ const hostUrl = attr(_data + 'host-url'); const tag = attr(_data + 'tag'); const autoTrack = attr(_data + 'auto-track') !== _false; + const dnt = attr(_data + 'do-not-track') === _true; const excludeSearch = attr(_data + 'exclude-search') === _true; const excludeHash = attr(_data + 'exclude-hash') === _true; const domain = attr(_data + 'domains') || ''; @@ -46,6 +48,11 @@ tag: tag ? tag : undefined, }); + const hasDoNotTrack = () => { + const dnt = doNotTrack || ndnt || msdnt; + return dnt === 1 || dnt === '1' || dnt === 'yes'; + }; + /* Event handlers */ const handlePush = (state, title, url) => { @@ -182,7 +189,8 @@ disabled || !website || (localStorage && localStorage.getItem('umami.disabled')) || - (domain && !domains.includes(hostname)); + (domain && !domains.includes(hostname)) || + (dnt && hasDoNotTrack()); const send = async (payload, type = 'event') => { if (trackingDisabled()) return; From c52774c787cbee06cdd2e00f880067f1d11f1297 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 1 Mar 2025 20:27:31 -0800 Subject: [PATCH 21/28] Bump version 2.17.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 870769364..db146e9a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "2.16.1", + "version": "2.17.0", "description": "A simple, fast, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", From 72ac97c5d94edafa7c936ad1ef9ed66b46ba3eeb Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 3 Mar 2025 12:24:54 -0800 Subject: [PATCH 22/28] fix unique key prop error on forms --- src/app/(main)/reports/ReportDeleteButton.tsx | 4 +++- src/app/(main)/settings/teams/TeamLeaveForm.tsx | 4 +++- src/app/(main)/settings/users/UserDeleteForm.tsx | 4 +++- .../[teamId]/settings/members/TeamMemberRemoveButton.tsx | 4 +++- src/components/common/TypeConfirmationForm.tsx | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/app/(main)/reports/ReportDeleteButton.tsx b/src/app/(main)/reports/ReportDeleteButton.tsx index efd1da3c7..ca096675f 100644 --- a/src/app/(main)/reports/ReportDeleteButton.tsx +++ b/src/app/(main)/reports/ReportDeleteButton.tsx @@ -39,7 +39,9 @@ export function ReportDeleteButton({ {(close: () => void) => ( {reportName} })} + message={formatMessage(messages.confirmDelete, { + target: {reportName}, + })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/app/(main)/settings/teams/TeamLeaveForm.tsx b/src/app/(main)/settings/teams/TeamLeaveForm.tsx index daf464341..389ba4ea7 100644 --- a/src/app/(main)/settings/teams/TeamLeaveForm.tsx +++ b/src/app/(main)/settings/teams/TeamLeaveForm.tsx @@ -34,7 +34,9 @@ export function TeamLeaveForm({ return ( {teamName} })} + message={formatMessage(messages.confirmLeave, { + target: {teamName}, + })} onConfirm={handleConfirm} onClose={onClose} isLoading={isPending} diff --git a/src/app/(main)/settings/users/UserDeleteForm.tsx b/src/app/(main)/settings/users/UserDeleteForm.tsx index 3ac7c1186..5c307cdcc 100644 --- a/src/app/(main)/settings/users/UserDeleteForm.tsx +++ b/src/app/(main)/settings/users/UserDeleteForm.tsx @@ -19,7 +19,9 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) { return ( {username} })} + message={formatMessage(messages.confirmDelete, { + target: {username}, + })} onConfirm={handleConfirm} onClose={onClose} buttonLabel={formatMessage(labels.delete)} diff --git a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx index 931390c7b..0dfe758b2 100644 --- a/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx +++ b/src/app/(main)/teams/[teamId]/settings/members/TeamMemberRemoveButton.tsx @@ -43,7 +43,9 @@ export function TeamMemberRemoveButton({ {(close: () => void) => ( {userName} })} + message={formatMessage(messages.confirmRemove, { + target: {userName}, + })} isLoading={isPending} error={error} onConfirm={handleConfirm.bind(null, close)} diff --git a/src/components/common/TypeConfirmationForm.tsx b/src/components/common/TypeConfirmationForm.tsx index baf5949f2..9ef5b30a0 100644 --- a/src/components/common/TypeConfirmationForm.tsx +++ b/src/components/common/TypeConfirmationForm.tsx @@ -35,7 +35,9 @@ export function TypeConfirmationForm({ return (

- {formatMessage(messages.actionConfirmation, { confirmation: {confirmationValue} })} + {formatMessage(messages.actionConfirmation, { + confirmation: {confirmationValue}, + })}

value === confirmationValue }}> From 51f2a1c43165b1b78e4fdae7642142e604b5395c Mon Sep 17 00:00:00 2001 From: Maxime-J Date: Thu, 6 Mar 2025 15:41:27 +0100 Subject: [PATCH 23/28] Make journey report steps optional --- src/app/api/reports/journey/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts index a1bc62901..19ad98fa7 100644 --- a/src/app/api/reports/journey/route.ts +++ b/src/app/api/reports/journey/route.ts @@ -9,8 +9,8 @@ export async function POST(request: Request) { const schema = z.object({ ...reportParms, steps: z.coerce.number().min(3).max(7), - startStep: z.string(), - endStep: z.string(), + startStep: z.string().optional(), + endStep: z.string().optional(), }); const { auth, body, error } = await parseRequest(request, schema); From b1901c7278c99c90167d7303e333fd1d94c30258 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 7 Mar 2025 13:06:38 -0800 Subject: [PATCH 24/28] update cypress tests, update zod validation error messaging to UI --- .eslintrc.json | 5 +- cypress/e2e/api.cy.ts | 29 +++++++++ cypress/e2e/login.cy.ts | 30 ++++++--- cypress/e2e/user.cy.ts | 65 +++++++++++++++++++ cypress/e2e/website.cy.ts | 10 +-- cypress/fixtures/users.json | 17 +++++ cypress/support/e2e.ts | 6 ++ cypress/support/index.d.ts | 6 ++ .../(main)/settings/users/UserAddButton.tsx | 2 +- src/app/(main)/settings/users/UserAddForm.tsx | 32 +++++++-- .../settings/users/UserDeleteButton.tsx | 2 +- src/app/(main)/settings/users/UsersTable.tsx | 2 +- .../settings/users/[userId]/UserEditForm.tsx | 19 ++++-- src/app/api/auth/login/route.ts | 2 +- src/app/api/users/[userId]/route.ts | 11 ++-- src/app/login/LoginForm.tsx | 4 +- src/components/common/ConfirmationForm.tsx | 7 +- src/lib/request.ts | 13 +++- 18 files changed, 221 insertions(+), 41 deletions(-) create mode 100644 cypress/e2e/api.cy.ts create mode 100644 cypress/e2e/user.cy.ts create mode 100644 cypress/fixtures/users.json diff --git a/.eslintrc.json b/.eslintrc.json index 324e291cd..9cbbd586a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,7 @@ "browser": true, "es2020": true, "node": true, + "jquery": true, "jest": true }, "parser": "@typescript-eslint/parser", @@ -14,6 +15,7 @@ "sourceType": "module" }, "extends": [ + "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "eslint:recommended", "plugin:prettier/recommended", @@ -39,7 +41,8 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] + "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], + "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }] }, "globals": { "React": "writable" diff --git a/cypress/e2e/api.cy.ts b/cypress/e2e/api.cy.ts new file mode 100644 index 000000000..e69b5dff6 --- /dev/null +++ b/cypress/e2e/api.cy.ts @@ -0,0 +1,29 @@ +describe('Website tests', () => { + Cypress.session.clearAllSavedSessions(); + + beforeEach(() => { + cy.login(Cypress.env('umami_user'), Cypress.env('umami_password')); + }); + + //let userId; + + it('creates a user.', () => { + cy.fixture('users').then(data => { + const userPost = data.userPost; + cy.request({ + method: 'POST', + url: '/api/users', + headers: { + 'Content-Type': 'application/json', + Authorization: Cypress.env('authorization'), + }, + body: userPost, + }).then(response => { + //userId = response.body.id; + expect(response.status).to.eq(200); + expect(response.body).to.have.property('username', 'cypress1'); + expect(response.body).to.have.property('role', 'User'); + }); + }); + }); +}); diff --git a/cypress/e2e/login.cy.ts b/cypress/e2e/login.cy.ts index 5831c81d6..507b1b580 100644 --- a/cypress/e2e/login.cy.ts +++ b/cypress/e2e/login.cy.ts @@ -1,22 +1,36 @@ describe('Login tests', () => { + beforeEach(() => { + cy.visit('/login'); + }); + it( 'logs user in with correct credentials and logs user out', { defaultCommandTimeout: 10000, }, () => { - cy.visit('/login'); - cy.getDataTest('input-username').find('input').click(); - cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 }); - cy.getDataTest('input-password').find('input').click(); + cy.getDataTest('input-username').find('input').as('inputUsername').click(); + cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); + cy.get('@inputUsername').click(); cy.getDataTest('input-password') .find('input') - .type(Cypress.env('umami_password'), { delay: 50 }); + .type(Cypress.env('umami_password'), { delay: 0 }); cy.getDataTest('button-submit').click(); cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); - cy.getDataTest('button-profile').click(); - cy.getDataTest('item-logout').click(); - cy.url().should('eq', Cypress.config().baseUrl + '/login'); + cy.logout(); }, ); + + it('login with blank inputs or incorrect credentials', () => { + cy.getDataTest('button-submit').click(); + cy.contains(/Required/i).should('be.visible'); + + cy.getDataTest('input-username').find('input').as('inputUsername'); + cy.get('@inputUsername').click(); + cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 }); + cy.get('@inputUsername').click(); + cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 }); + cy.getDataTest('button-submit').click(); + cy.contains(/Incorrect username and\/or password./i).should('be.visible'); + }); }); diff --git a/cypress/e2e/user.cy.ts b/cypress/e2e/user.cy.ts new file mode 100644 index 000000000..9f432f16a --- /dev/null +++ b/cypress/e2e/user.cy.ts @@ -0,0 +1,65 @@ +describe('Website tests', () => { + Cypress.session.clearAllSavedSessions(); + + beforeEach(() => { + cy.login(Cypress.env('umami_user'), Cypress.env('umami_password')); + cy.visit('/settings/users'); + }); + + it('Add a User', () => { + // add user + cy.contains(/Create user/i).should('be.visible'); + cy.getDataTest('button-create-user').click(); + cy.getDataTest('input-username').find('input').as('inputName').click(); + cy.get('@inputName').type('Test-user', { delay: 0 }); + cy.getDataTest('input-password').find('input').as('inputPassword').click(); + cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 }); + cy.getDataTest('dropdown-role').click(); + cy.getDataTest('dropdown-item-user').click(); + cy.getDataTest('button-submit').click(); + cy.get('td[label="Username"]').should('contain.text', 'Test-user'); + cy.get('td[label="Role"]').should('contain.text', 'User'); + }); + + it('Edit a User role and password', () => { + // edit user + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .within(() => { + cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row + }); + cy.getDataTest('input-password').find('input').as('inputPassword').click(); + cy.get('@inputPassword').type('newPassword', { delay: 0 }); + cy.getDataTest('dropdown-role').click(); + cy.getDataTest('dropdown-item-viewOnly').click(); + cy.getDataTest('button-submit').click(); + + cy.visit('/settings/users'); + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .should('contain.text', 'View only'); + + cy.logout(); + cy.url().should('eq', Cypress.config().baseUrl + '/login'); + cy.getDataTest('input-username').find('input').as('inputUsername').click(); + cy.get('@inputUsername').type('Test-user', { delay: 0 }); + cy.get('@inputUsername').click(); + cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 }); + cy.getDataTest('button-submit').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); + }); + + it('Delete a website', () => { + // delete user + cy.get('table tbody tr') + .contains('td', /Test-user/i) + .parent() + .within(() => { + cy.getDataTest('button-delete').click(); // Clicks the button inside the row + }); + cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible'); + cy.getDataTest('button-confirm').click(); + }); +}); diff --git a/cypress/e2e/website.cy.ts b/cypress/e2e/website.cy.ts index b60d8e7a4..2dcd60273 100644 --- a/cypress/e2e/website.cy.ts +++ b/cypress/e2e/website.cy.ts @@ -10,10 +10,10 @@ describe('Website tests', () => { cy.visit('/settings/websites'); cy.getDataTest('button-website-add').click(); cy.contains(/Add website/i).should('be.visible'); - cy.getDataTest('input-name').find('input').click(); - cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 }); + cy.getDataTest('input-name').find('input').as('inputUsername').click(); + cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 }); cy.getDataTest('input-domain').find('input').click(); - cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 }); + cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 }); cy.getDataTest('button-submit').click(); cy.get('td[label="Name"]').should('contain.text', 'Add test'); cy.get('td[label="Domain"]').should('contain.text', 'addtest.com'); @@ -41,10 +41,10 @@ describe('Website tests', () => { cy.contains(/Details/i).should('be.visible'); cy.getDataTest('input-name').find('input').click(); cy.getDataTest('input-name').find('input').clear(); - cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 }); + cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 }); cy.getDataTest('input-domain').find('input').click(); cy.getDataTest('input-domain').find('input').clear(); - cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 }); + cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 }); cy.getDataTest('button-submit').click({ force: true }); cy.getDataTest('input-name').find('input').should('have.value', 'Updated website'); cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com'); diff --git a/cypress/fixtures/users.json b/cypress/fixtures/users.json new file mode 100644 index 000000000..420a71c39 --- /dev/null +++ b/cypress/fixtures/users.json @@ -0,0 +1,17 @@ +{ + "userGet": { + "name": "cypress", + "email": "password", + "role": "User" + }, + "userPost": { + "username": "cypress1", + "password": "password", + "role": "User" + }, + "userDelete": { + "name": "Charlie", + "email": "charlie@example.com", + "age": 35 + } +} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2c45142b3..a300b9691 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -5,6 +5,12 @@ Cypress.Commands.add('getDataTest', (value: string) => { return cy.get(`[data-test=${value}]`); }); +Cypress.Commands.add('logout', () => { + cy.getDataTest('button-profile').click(); + cy.getDataTest('item-logout').click(); + cy.url().should('eq', Cypress.config().baseUrl + '/login'); +}); + Cypress.Commands.add('login', (username: string, password: string) => { cy.session([username, password], () => { cy.request({ diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index 90cca19b2..e89b24dd8 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -1,4 +1,5 @@ /// +/* global JQuery */ declare namespace Cypress { interface Chainable { @@ -7,6 +8,11 @@ declare namespace Cypress { * @example cy.getDataTest('greeting') */ getDataTest(value: string): Chainable>; + /** + * Custom command to logout through UI. + * @example cy.logout() + */ + logout(): Chainable>; /** * Custom command to login user into the app. * @example cy.login('admin', 'password) diff --git a/src/app/(main)/settings/users/UserAddButton.tsx b/src/app/(main)/settings/users/UserAddButton.tsx index e1b048420..674771b6e 100644 --- a/src/app/(main)/settings/users/UserAddButton.tsx +++ b/src/app/(main)/settings/users/UserAddButton.tsx @@ -15,7 +15,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) { return ( - diff --git a/src/lib/request.ts b/src/lib/request.ts index 0c71537ae..374f1cbc1 100644 --- a/src/lib/request.ts +++ b/src/lib/request.ts @@ -1,4 +1,4 @@ -import { ZodSchema } from 'zod'; +import { z, ZodSchema } from 'zod'; import { FILTER_COLUMNS } from '@/lib/constants'; import { badRequest, unauthorized } from '@/lib/response'; import { getAllowedUnits, getMinimumUnit } from '@/lib/date'; @@ -24,12 +24,21 @@ export async function parseRequest( let error: () => void | undefined; let auth = null; + const getErrorMessages = (error: z.ZodError) => { + return Object.entries(error.format()) + .map(([key, value]) => { + const messages = (value as any)._errors; + return messages ? `${key}: ${messages.join(', ')}` : null; + }) + .filter(Boolean); + }; + if (schema) { const isGet = request.method === 'GET'; const result = schema.safeParse(isGet ? query : body); if (!result.success) { - error = () => badRequest(result.error); + error = () => badRequest(getErrorMessages(result.error)); } else if (isGet) { query = result.data; } else { From 1b21f264b093885ed5b80f8507b208c63224dd45 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 7 Mar 2025 13:37:19 -0800 Subject: [PATCH 25/28] Added more paid ad params. Closes #3270 --- src/lib/constants.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a64210eca..3eddefdcf 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -397,6 +397,14 @@ export const PAID_AD_PARAMS = [ 'epik=', 'ttclid=', 'scid=', + 'aid=', + 'pc_id=', + 'ad_id=', + 'rdt_cid=', + 'ob_click_id=', + 'utm_medium=cpc', + 'utm_medium=paid', + 'utm_medium=paid_social', ]; export const GROUPED_DOMAINS = [ From 833de1a1af2ef9efce23741f995043bf651d6a1e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 7 Mar 2025 18:42:15 -0800 Subject: [PATCH 26/28] Added decoding to URL elements. --- src/app/api/send/route.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 8519a73e1..bd255eaf8 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -10,6 +10,7 @@ import { createToken, parseToken } from '@/lib/jwt'; import { secret, uuid, hash } from '@/lib/crypto'; import { COLLECTION_TYPE } from '@/lib/constants'; import { anyObjectParam, urlOrPathParam } from '@/lib/schema'; +import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url'; import { createSession, saveEvent, saveSessionData } from '@/queries'; const schema = z.object({ @@ -168,12 +169,12 @@ export async function POST(request: Request) { websiteId, sessionId, visitId, - urlPath, + urlPath: safeDecodeURI(urlPath), urlQuery, - referrerPath, + referrerPath: safeDecodeURI(referrerPath), referrerQuery, referrerDomain, - pageTitle: title, + pageTitle: safeDecodeURIComponent(title), eventName: name, eventData: data, hostname: hostname || urlDomain, From 97c687ff05f19be3d3abd75c4d737c4ec6d18a09 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 7 Mar 2025 20:29:31 -0800 Subject: [PATCH 27/28] Fixed group referrers count. Closes #3257 --- src/app/layout.tsx | 2 +- src/components/metrics/ReferrersTable.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f88d8169c..ebe313e62 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -23,7 +23,7 @@ export default function ({ children }) { - + {children} diff --git a/src/components/metrics/ReferrersTable.tsx b/src/components/metrics/ReferrersTable.tsx index db40a6177..4d5a87c36 100644 --- a/src/components/metrics/ReferrersTable.tsx +++ b/src/components/metrics/ReferrersTable.tsx @@ -69,11 +69,10 @@ export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) { if (!groups[domain]) { groups[domain] = 0; } - groups[domain] += y; - } else { - groups._other += y; + groups[domain] += +y; } } + groups._other += +y; } return Object.keys(groups) From abde966647af38828095da1145e20caa2eab7542 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 7 Mar 2025 21:41:02 -0800 Subject: [PATCH 28/28] Fixed wrong country lookup. --- src/lib/detect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 9d9fd7db1..da2ca8a1b 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -148,7 +148,7 @@ export async function getClientInfo(request: Request, payload: Record