From 54845ade1e9647b784d921c2890332e705dcfe1d Mon Sep 17 00:00:00 2001 From: BM Cho Date: Tue, 4 Aug 2026 10:13:37 +0800 Subject: [PATCH 1/7] Support distinct ID tracker attribute --- src/tracker/index.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/tracker/index.ts | 7 ++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/tracker/index.test.ts diff --git a/src/tracker/index.test.ts b/src/tracker/index.test.ts new file mode 100644 index 000000000..2e2fab59e --- /dev/null +++ b/src/tracker/index.test.ts @@ -0,0 +1,38 @@ +import { afterEach, expect, test, vi } from 'vitest'; + +afterEach(() => { + delete (window as Window & { umami?: unknown }).umami; + delete (document as Document & { currentScript?: HTMLScriptElement }).currentScript; + delete (document as Document & { readyState?: DocumentReadyState }).readyState; + vi.unstubAllGlobals(); + vi.resetModules(); +}); + +test('identifies data-distinct-id before the initial page view', async () => { + const script = document.createElement('script'); + script.src = 'https://analytics.example.com/script.js'; + script.dataset.websiteId = 'website-id'; + script.dataset.distinctId = 'visitor-id'; + + Object.defineProperties(document, { + currentScript: { configurable: true, value: script }, + readyState: { configurable: true, value: 'complete' }, + }); + + const fetchMock = vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({}) }); + vi.stubGlobal('fetch', fetchMock); + + await import('./index'); + + await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)); + const requests = fetchMock.mock.calls.map(([, init]) => JSON.parse(init.body)); + + expect(requests[0]).toMatchObject({ + type: 'identify', + payload: { id: 'visitor-id', website: 'website-id' }, + }); + expect(requests[1]).toMatchObject({ + type: 'event', + payload: { id: 'visitor-id', website: 'website-id' }, + }); +}); diff --git a/src/tracker/index.ts b/src/tracker/index.ts index ab5d868e1..2fefed98d 100644 --- a/src/tracker/index.ts +++ b/src/tracker/index.ts @@ -241,6 +241,7 @@ type MetricEntry = PerformanceEntry & { const website = config('website-id'); const hostUrl = config('host-url'); const beforeSend = config('before-send'); + const distinctId = config('distinct-id') || undefined; const tag = config('tag') || undefined; const autoTrack = config('auto-track') !== _false; const dnt = config('do-not-track') === _true; @@ -636,9 +637,13 @@ type MetricEntry = PerformanceEntry & { let initialized = false; let disabled = false; let cache: string | undefined; - let identity: string | undefined; + let identity = distinctId; let flushPerformance: (() => void) | undefined; + if (distinctId) { + void identify(distinctId); + } + if (autoTrack && !trackingDisabled()) { if (document.readyState === 'complete') { init(); From 474cbccd490ab78281f03f4aecd13aba4c482fdc Mon Sep 17 00:00:00 2001 From: Flame Date: Wed, 12 Aug 2026 23:45:33 +0800 Subject: [PATCH 2/7] Add TWD (New Taiwan Dollar) to the revenue currency list --- src/lib/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 89e250e73..db2eb1d7f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -705,6 +705,7 @@ export const CURRENCIES = [ { id: 'MYR', name: 'Malaysian Ringgit' }, { id: 'INR', name: 'Indian Rupee' }, { id: 'KRW', name: 'South Korean Won' }, + { id: 'TWD', name: 'New Taiwan Dollar' }, { id: 'BRL', name: 'Brazilian Real' }, { id: 'TRY', name: 'Turkish Lira' }, { id: 'CZK', name: 'Czech Koruna' }, From 1976622494e8cb3d133580ce247763d916d621a3 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 29 Aug 2026 04:57:36 +0530 Subject: [PATCH 3/7] fix(filters): preserve commas in search values --- src/components/common/FilterRecord.test.tsx | 47 +++++++++++++++++++++ src/components/common/FilterRecord.tsx | 10 ++++- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/components/common/FilterRecord.test.tsx diff --git a/src/components/common/FilterRecord.test.tsx b/src/components/common/FilterRecord.test.tsx new file mode 100644 index 000000000..d9e4dbb9b --- /dev/null +++ b/src/components/common/FilterRecord.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/react'; +import type { ReactNode } from 'react'; +import { expect, test, vi } from 'vitest'; +import { FilterRecord } from './FilterRecord'; + +vi.mock('@umami/react-zen', () => ({ + Button: ({ children }: { children: ReactNode }) => , + Column: ({ children }: { children: ReactNode }) =>
{children}
, + Grid: ({ children }: { children: ReactNode }) =>
{children}
, + Icon: ({ children }: { children: ReactNode }) => {children}, + Label: ({ children }: { children: ReactNode }) => , + ListItem: ({ children }: { children: ReactNode }) =>
{children}
, + Loading: () =>
Loading
, + Select: ({ children }: { children: ReactNode }) =>
{children}
, + TextField: ({ value }: { value: string }) => ( + + ), +})); + +vi.mock('@/components/common/Empty', () => ({ Empty: () =>
Empty
})); +vi.mock('@/components/common/MultiSelect', () => ({ + MultiSelect: ({ children }: { children: ReactNode }) =>
{children}
, + MultiSelectItem: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock('@/components/hooks', () => ({ + useFilters: () => ({ fields: [], operators: [] }), + useFormat: () => ({ formatValue: (value: string) => value }), + useWebsiteValuesQuery: () => ({ data: [], isLoading: false }), +})); +vi.mock('@/components/icons', () => ({ X: () => null })); + +test('keeps commas in regex filter values', () => { + const value = '^[a-zA-Z0-9]{21,22}$'; + + render( + , + ); + + expect(screen.getByLabelText('Filter value')).toHaveValue(value); +}); diff --git a/src/components/common/FilterRecord.tsx b/src/components/common/FilterRecord.tsx index d9b52f4bd..b689b76bc 100644 --- a/src/components/common/FilterRecord.tsx +++ b/src/components/common/FilterRecord.tsx @@ -42,7 +42,14 @@ export function FilterRecord({ onChange, }: FilterRecordProps) { const { fields, operators } = useFilters(); - const initValues = Array.isArray(value) ? value : value ? value.split(',') : []; + const isSearch = isSearchOperator(operator); + const initValues = Array.isArray(value) + ? value + : value + ? isSearch + ? [value] + : value.split(',') + : []; const [selected, setSelected] = useState(initValues); const [search, setSearch] = useState(''); const { formatValue } = useFormat(); @@ -53,7 +60,6 @@ export function FilterRecord({ startDate, endDate, }); - const isSearch = isSearchOperator(operator); const items = data?.filter(({ value }) => value) || []; const handleSearch = (value: string) => { From 8928c5f1177fc2d1ef3b0afa7b8ab244760ac79d Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 29 Aug 2026 05:40:03 +0530 Subject: [PATCH 4/7] fix(filters): sync values when operators change --- src/components/common/FilterRecord.test.tsx | 16 ++++++++++++++++ src/components/common/FilterRecord.tsx | 21 ++++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/components/common/FilterRecord.test.tsx b/src/components/common/FilterRecord.test.tsx index d9e4dbb9b..a55cd48b0 100644 --- a/src/components/common/FilterRecord.test.tsx +++ b/src/components/common/FilterRecord.test.tsx @@ -45,3 +45,19 @@ test('keeps commas in regex filter values', () => { expect(screen.getByLabelText('Filter value')).toHaveValue(value); }); + +test('keeps commas when changing to a search operator', () => { + const value = '^[a-zA-Z0-9]{21,22}$'; + const props = { + type: 'path', + startDate: new Date('2026-08-01'), + endDate: new Date('2026-08-02'), + name: 'path', + value, + }; + const { rerender } = render(); + + rerender(); + + expect(screen.getByLabelText('Filter value')).toHaveValue(value); +}); diff --git a/src/components/common/FilterRecord.tsx b/src/components/common/FilterRecord.tsx index b689b76bc..cb2a1462d 100644 --- a/src/components/common/FilterRecord.tsx +++ b/src/components/common/FilterRecord.tsx @@ -9,7 +9,7 @@ import { Select, TextField, } from '@umami/react-zen'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Empty } from '@/components/common/Empty'; import { MultiSelect, MultiSelectItem } from '@/components/common/MultiSelect'; import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks'; @@ -29,6 +29,12 @@ export interface FilterRecordProps { onChange?: (name: string, value: string) => void; } +function getSelectedValues(value: string | string[], operator: string) { + if (Array.isArray(value)) return value; + if (!value) return []; + return isSearchOperator(operator) ? [value] : value.split(','); +} + export function FilterRecord({ websiteId, type, @@ -43,14 +49,7 @@ export function FilterRecord({ }: FilterRecordProps) { const { fields, operators } = useFilters(); const isSearch = isSearchOperator(operator); - const initValues = Array.isArray(value) - ? value - : value - ? isSearch - ? [value] - : value.split(',') - : []; - const [selected, setSelected] = useState(initValues); + const [selected, setSelected] = useState(() => getSelectedValues(value, operator)); const [search, setSearch] = useState(''); const { formatValue } = useFormat(); const { data, isLoading } = useWebsiteValuesQuery({ @@ -62,6 +61,10 @@ export function FilterRecord({ }); const items = data?.filter(({ value }) => value) || []; + useEffect(() => { + setSelected(getSelectedValues(value, operator)); + }, [operator, value]); + const handleSearch = (value: string) => { setSearch(value); }; From 2cfd5421eb468b7a3bbcce6f67cd148cfd8c6acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magn=C3=BAs=20Bergsson?= Date: Sat, 5 Sep 2026 15:05:51 +0000 Subject: [PATCH 5/7] =?UTF-8?q?Add=20ISK=20(Icelandic=20Kr=C3=B3na)=20to?= =?UTF-8?q?=20revenue=20currencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 89e250e73..57597f787 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -697,6 +697,7 @@ export const CURRENCIES = [ { id: 'PLN', name: 'Polish Złoty' }, { id: 'NOK', name: 'Norwegian Krone' }, { id: 'DKK', name: 'Danish Krone' }, + { id: 'ISK', name: 'Icelandic Króna' }, { id: 'NZD', name: 'New Zealand Dollar' }, { id: 'ZAR', name: 'South African Rand' }, { id: 'MXN', name: 'Mexican Peso' }, From db644ae2b7d74e96df1c9b8a26811ff21717170f Mon Sep 17 00:00:00 2001 From: Jamal Date: Mon, 7 Sep 2026 11:51:35 +0400 Subject: [PATCH 6/7] Add Azerbaijani language names and locale strings --- public/intl/language/az-AZ.json | 184 +++++++++++ public/intl/messages/az-AZ.json | 535 ++++++++++++++++++++++++++++++++ src/lib/lang.ts | 2 + 3 files changed, 721 insertions(+) create mode 100644 public/intl/language/az-AZ.json create mode 100644 public/intl/messages/az-AZ.json diff --git a/public/intl/language/az-AZ.json b/public/intl/language/az-AZ.json new file mode 100644 index 000000000..6fe64cc17 --- /dev/null +++ b/public/intl/language/az-AZ.json @@ -0,0 +1,184 @@ +{ + "ab": "Abxaz dili", + "aa": "Afar dili", + "af": "Afrikaans dili", + "ak": "Akan dili", + "sq": "Alban dili", + "am": "Amhar dili", + "ar": "Ərəb dili", + "an": "Araqon dili", + "hy": "Erməni dili", + "as": "Assam dili", + "av": "Avar dili", + "ae": "Avesta dili", + "ay": "Aymara dili", + "az": "Azərbaycan dili", + "bm": "Bambara dili", + "ba": "Başqırd dili", + "eu": "Bask dili", + "be": "Belarus dili", + "bn": "Benqal dili", + "bh": "Bihari dili", + "bi": "Bislama dili", + "bs": "Bosniya dili", + "br": "Breton dili", + "bg": "Bolqar dili", + "my": "Birma dili", + "ca": "Katalan dili", + "ch": "Çamorro dili", + "ce": "Çeçen dili", + "ny": "Çiçeva dili", + "zh": "Çin dili", + "cv": "Çuvaş dili", + "kw": "Korn dili", + "co": "Korsika dili", + "cr": "Kri dili", + "hr": "Xorvat dili", + "cs": "Çex dili", + "da": "Danimarka dili", + "dv": "Divehi dili", + "nl": "Holland dili", + "en": "İngilis dili", + "eo": "Esperanto dili", + "et": "Eston dili", + "ee": "Eve dili", + "fo": "Farer dili", + "fj": "Fici dili", + "fi": "Fin dili", + "fr": "Fransız dili", + "ff": "Fula dili", + "gl": "Qalisiya dili", + "ka": "Gürcü dili", + "de": "Alman dili", + "el": "Yunan dili (müasir)", + "gn": "Qvarani dili", + "gu": "Qucarat dili", + "ht": "Haiti dili", + "ha": "Hausa dili", + "he": "İvrit dili (müasir)", + "hz": "Herero dili", + "hi": "Hind dili", + "ho": "Hiri Motu dili", + "hu": "Macar dili", + "ia": "İnterlingua dili", + "id": "İndoneziya dili", + "ie": "İnterlingue dili", + "ga": "İrland dili", + "ig": "İqbo dili", + "ik": "İnupiak dili", + "io": "İdo dili", + "is": "İsland dili", + "it": "İtalyan dili", + "iu": "İnuktitut dili", + "ja": "Yapon dili", + "jv": "Yava dili", + "kl": "Qrenland dili", + "kn": "Kannada dili", + "kr": "Kanuri dili", + "ks": "Kəşmir dili", + "kk": "Qazax dili", + "km": "Kxmer dili", + "ki": "Kikuyu dili", + "rw": "Kinyaruanda dili", + "ky": "Qırğız dili", + "kv": "Komi dili", + "kg": "Konqo dili", + "ko": "Koreya dili", + "ku": "Kürd dili", + "kj": "Kvanyama dili", + "la": "Latın dili", + "lb": "Lüksemburq dili", + "lg": "Luqanda dili", + "li": "Limburq dili", + "ln": "Lingala dili", + "lo": "Lao dili", + "lt": "Litva dili", + "lu": "Luba-Katanqa dili", + "lv": "Latış dili", + "gv": "Men dili", + "mk": "Makedon dili", + "mg": "Malaqasiya dili", + "ms": "Malay dili", + "ml": "Malayalam dili", + "mt": "Malta dili", + "mi": "Maori dili", + "mr": "Marathi dili", + "mh": "Marşal dili", + "mn": "Monqol dili", + "na": "Nauru dili", + "nv": "Navaho dili", + "nb": "Norveç bokmal dili", + "nd": "Şimal ndebele dili", + "ne": "Nepal dili", + "ng": "Ndonqa dili", + "nn": "Norveç nynorsk dili", + "no": "Norveç dili", + "ii": "Nuosu dili", + "nr": "Cənub ndebele dili", + "oc": "Oksitan dili", + "oj": "Ocibve dili", + "cu": "Qədim kilsə slavyan dili", + "om": "Oromo dili", + "or": "Oriya dili", + "os": "Osetin dili", + "pa": "Pəncab dili", + "pi": "Pali dili", + "fa": "Fars dili", + "pl": "Polyak dili", + "ps": "Puştu dili", + "pt": "Portuqal dili", + "qu": "Keçua dili", + "rm": "Romanş dili", + "rn": "Kirundi dili", + "ro": "Rumın dili", + "ru": "Rus dili", + "sa": "Sanskrit dili", + "sc": "Sardiniya dili", + "sd": "Sindhi dili", + "se": "Şimal sami dili", + "sm": "Samoa dili", + "sg": "Sanqo dili", + "sr": "Serb dili", + "gd": "Şotland qael dili", + "sn": "Şona dili", + "si": "Sinhal dili", + "sk": "Slovak dili", + "sl": "Sloven dili", + "so": "Somali dili", + "st": "Cənub soto dili", + "es": "İspan dili", + "su": "Sunda dili", + "sw": "Suahili dili", + "ss": "Svati dili", + "sv": "İsveç dili", + "ta": "Tamil dili", + "te": "Teluqu dili", + "tg": "Tacik dili", + "th": "Tay dili", + "ti": "Tiqrinya dili", + "bo": "Tibet dili", + "tk": "Türkmən dili", + "tl": "Taqaloq dili", + "tn": "Tsvana dili", + "to": "Tonqa dili", + "tr": "Türk dili", + "ts": "Tsonqa dili", + "tt": "Tatar dili", + "tw": "Tvi dili", + "ty": "Tahiti dili", + "ug": "Uyğur dili", + "uk": "Ukrayna dili", + "ur": "Urdu dili", + "uz": "Özbək dili", + "ve": "Venda dili", + "vi": "Vyetnam dili", + "vo": "Volapük dili", + "wa": "Valon dili", + "cy": "Uels dili", + "wo": "Volof dili", + "fy": "Qərbi friz dili", + "xh": "Xosa dili", + "yi": "İdiş dili", + "yo": "Yoruba dili", + "za": "Çjuan dili" +} diff --git a/public/intl/messages/az-AZ.json b/public/intl/messages/az-AZ.json new file mode 100644 index 000000000..a1a33b592 --- /dev/null +++ b/public/intl/messages/az-AZ.json @@ -0,0 +1,535 @@ +{ + "label": { + "access-code": "Giriş kodu", + "account": "Hesab", + "action": "Əməliyyat", + "actions": "Əməliyyatlar", + "activity": "Fəaliyyət", + "add": "Əlavə et", + "add-board": "Lövhə əlavə et", + "add-description": "Təsvir əlavə et", + "add-link": "Keçid əlavə et", + "add-member": "Üzv əlavə et", + "add-pixel": "Piksel əlavə et", + "add-step": "Addım əlavə et", + "add-website": "Veb sayt əlavə et", + "admin": "Admin", + "affiliate": "Tərəfdaş", + "after": "Sonra", + "all": "Hamısı", + "all-time": "Bütün vaxt", + "analysis": "Təhlil", + "analytics": "Analitika", + "application": "Tətbiq", + "apply": "Tətbiq et", + "attribution": "Atribusiya", + "attribution-description": "İstifadəçilərin marketinqinizlə necə əlaqə qurduğunu və nəyin çevrilmələrə səbəb olduğunu görün.", + "audience": "Auditoriya", + "average": "Orta", + "back": "Geri", + "before": "Əvvəl", + "behavior": "Davranış", + "block-selector": "Blok seçici", + "boards": "Lövhələr", + "board-type": "Lövhə növü", + "bounce-rate": "Səkmə dərəcəsi", + "breakdown": "Bölgü", + "browser": "Brauzer", + "browsers": "Brauzerlər", + "campaign": "Kampaniya", + "campaigns": "Kampaniyalar", + "cancel": "Ləğv et", + "close": "Bağla", + "change-password": "Parolu dəyiş", + "channel": "Kanal", + "channels": "Kanallar", + "chart": "Qrafik", + "cities": "Şəhərlər", + "city": "Şəhər", + "clear-all": "Hamısını təmizlə", + "cls": "CLS", + "cohort": "Koqorta", + "cohorts": "Koqortalar", + "compare": "Müqayisə et", + "compare-dates": "Tarixləri müqayisə et", + "confirm": "Təsdiqlə", + "confirm-password": "Parolu təsdiqlə", + "contains": "Ehtiva edir", + "regex-match": "Regex-ə uyğun gəlir", + "regex-not-match": "Regex-ə uyğun gəlmir", + "content": "Məzmun", + "continue": "Davam et", + "conversion": "Çevrilmə", + "conversion-rate": "Çevrilmə dərəcəsi", + "conversion-step": "Çevrilmə addımı", + "count": "Say", + "countries": "Ölkələr", + "country": "Ölkə", + "create": "Yarat", + "create-report": "Hesabat yarat", + "create-team": "Komanda yarat", + "create-user": "İstifadəçi yarat", + "created": "Yaradılıb", + "created-by": "Yaradan", + "criteria": "Meyar", + "cumulative": "Kumulyativ", + "currency": "Valyuta", + "current": "Cari", + "current-password": "Cari parol", + "custom-range": "Fərdi aralıq", + "dashboard": "İdarə paneli", + "data": "Məlumat", + "date": "Tarix", + "date-range": "Tarix aralığı", + "day": "Gün", + "default-date-range": "Standart tarix aralığı", + "delete": "Sil", + "delete-report": "Hesabatı sil", + "delete-team": "Komandanı sil", + "delete-user": "İstifadəçini sil", + "delete-website": "Veb saytı sil", + "description": "Təsvir", + "desktop": "Masaüstü", + "destination-url": "Təyinat URL-i", + "details": "Detallar", + "device": "Cihaz", + "devices": "Cihazlar", + "direct": "Birbaşa", + "dismiss": "İmtina et", + "distinct-id": "Unikal ID", + "documentation": "Sənədləşdirmə", + "does-not-contain": "Ehtiva etmir", + "does-not-include": "Daxil deyil", + "doest-not-exist": "Mövcud deyil", + "domain": "Domen", + "download": "Endir", + "dropoff": "Tərk etmə", + "duration": "Müddət", + "min-duration-seconds": "Minimum müddət (saniyə)", + "edit": "Redaktə et", + "design": "Dizayn", + "edit-member": "Üzvü redaktə et", + "email": "E-poçt", + "enable-share-url": "Paylaşım URL-ini aktivləşdir", + "end-step": "Son addım", + "entry": "Giriş səhifəsi", + "environment": "Mühit", + "event": "Hadisə", + "event-data": "Hadisə məlumatları", + "event-properties": "Hadisə xüsusiyyətləri", + "event-name": "Hadisə adı", + "events": "Hadisələr", + "exclude-bounce": "Səkməni çıxart", + "exists": "Mövcuddur", + "exit": "Çıxış səhifəsi", + "false": "Yalan", + "fcp": "FCP", + "field": "Sahə", + "fields": "Sahələr", + "filter": "Süzgəc", + "filter-combined": "Birləşdirilmiş", + "filter-raw": "Xam", + "filters": "Süzgəclər", + "filters-enabled": "Süzgəclər aktivdir", + "property-filter": "Xüsusiyyət süzgəci", + "activity-by-property": "Xüsusiyyətə görə fəaliyyət", + "add-note": "Qeyd əlavə et", + "first-click": "İlk klik", + "first-seen": "İlk görülmə", + "funnel": "Huni", + "funnel-description": "İstifadəçilərin çevrilmə və tərk etmə dərəcəsini anlayın.", + "funnels": "Hunilər", + "goal": "Hədəf", + "goals": "Hədəflər", + "good": "Yaxşı", + "goals-description": "Səhifə baxışları və hadisələr üçün hədəflərinizi izləyin.", + "greater-than": "Böyükdür", + "greater-than-equals": "Böyük və ya bərabərdir", + "grouped": "Qruplaşdırılıb", + "growth": "Artım", + "heatmap": "İstilik xəritəsi", + "heatmap-description": "Ziyarətçilərin səhifələrinizdə harada kliklədiyini və nə qədər aşağı sürüşdürdüyünü vizuallaşdırın.", + "heatmaps": "İstilik xəritələri", + "hostname": "Host adı", + "hour": "Saat", + "includes": "Daxildir", + "insight": "Dərketmə", + "insights": "Dərketmələr", + "insights-description": "Seqmentlərdən və süzgəclərdən istifadə edərək məlumatınıza daha dərindən nəzər salın.", + "inp": "INP", + "invalid-url": "Yanlış URL", + "is": "Bərabərdir", + "is-false": "Yalandır", + "is-not": "Bərabər deyil", + "is-not-set": "Təyin edilməyib", + "is-set": "Təyin edilib", + "is-true": "Doğrudur", + "join": "Qoşul", + "join-team": "Komandaya qoşul", + "journey": "Səyahət", + "journey-description": "İstifadəçilərin veb saytınızda necə hərəkət etdiyini anlayın.", + "journeys": "Səyahətlər", + "language": "Dil", + "languages": "Dillər", + "laptop": "Noutbuk", + "last-click": "Son klik", + "last-days": "Son {x} gün", + "last-hours": "Son {x} saat", + "last-months": "Son {x} ay", + "last-seen": "Son görülmə", + "lcp": "LCP", + "leave": "Tərk et", + "leave-team": "Komandanı tərk et", + "less-than": "Kiçikdir", + "less-than-equals": "Kiçik və ya bərabərdir", + "link": "Keçid", + "links": "Keçidlər", + "location": "Məkan", + "login": "Giriş", + "llm": "LLM", + "logout": "Çıxış", + "manage": "İdarə et", + "manager": "Menecer", + "mask-level": "Maskalama səviyyəsi", + "max": "Maks", + "max-duration": "Maksimum müddət", + "match": "Uyğunluq", + "match-all": "Hamısı", + "match-any": "İstənilən", + "maximize": "Böyüt", + "medium": "Vasitə", + "member": "Üzv", + "members": "Üzvlər", + "min": "Min", + "minute": "Dəqiqə", + "mobile": "Mobil", + "model": "Model", + "month": "Ay", + "more": "Daha çox", + "my-account": "Mənim hesabım", + "my-websites": "Mənim veb saytlarım", + "name": "Ad", + "needs-improvement": "Təkmilləşdirmə tələb edir", + "new-password": "Yeni parol", + "none": "Yoxdur", + "number-of-records": "{x} {x, plural, one {qeyd} other {qeyd}}", + "ok": "OK", + "online": "Onlayn", + "open": "Qarışıq", + "organic-search": "Üzvi axtarış", + "organic-shopping": "Üzvi alış-veriş", + "organic-social": "Üzvi sosial", + "organic-video": "Üzvi video", + "os": "OS", + "other": "Digər", + "overview": "İcmal", + "owner": "Sahib", + "page": "Səhifə", + "page-of": "Səhifə {current} / {total}", + "page-views": "Səhifə baxışları", + "pageTitle": "Səhifə başlığı", + "period": "Dövr", + "pages": "Səhifələr", + "paid-ads": "Ödənişli reklamlar", + "paid-search": "Ödənişli axtarış", + "paid-shopping": "Ödənişli alış-veriş", + "paid-social": "Ödənişli sosial", + "paid-video": "Ödənişli video", + "password": "Parol", + "path": "Yol", + "paths": "Yollar", + "performance": "Performans", + "pixel": "Piksel", + "pixels": "Piksellər", + "poor": "Zəif", + "powered-by": "{name} tərəfindən işə salınıb", + "preferences": "Tərcihlər", + "previous": "Əvvəlki", + "previous-period": "Əvvəlki dövr", + "previous-year": "Əvvəlki il", + "profile": "Profil", + "profiles": "Profillər", + "properties": "Xüsusiyyətlər", + "property": "Xüsusiyyət", + "queries": "Sorğular", + "query": "Sorğu", + "query-parameters": "Sorğu parametrləri", + "realtime": "Real vaxt", + "recorded": "Qeydə alınıb", + "referral": "İstinad", + "referrer": "Yönləndirən sayt", + "referrers": "Yönləndirən saytlar", + "refresh": "Təzələ", + "regenerate": "Yenidən yarat", + "region": "Bölgə", + "regions": "Bölgələr", + "remaining": "Qalıb", + "remove": "Çıxar", + "remove-member": "Üzvü çıxar", + "play": "Oynat", + "replay": "Təkrar baxış", + "replay-id": "Təkrar baxış ID-si", + "recorder-code": "Qeydiyyat kodu", + "replay-enabled": "Təkrar baxış aktivdir", + "replays": "Təkrar baxışlar", + "reports": "Hesabatlar", + "required": "Məcburi", + "reset": "Sıfırla", + "reset-website": "Veb saytı sıfırla", + "retention": "Saxlanma", + "retention-days": "Saxlanma günləri", + "retention-description": "Veb saytınızın istifadəçi sadiqliyini onların nə qədər tez-tez qayıtdığını izləyərək ölçün.", + "revenue": "Gəlir", + "revenue-description": "Gəlir məlumatınıza və istifadəçilərin xərcləmə tərzinə nəzər salın.", + "role": "Rol", + "run-query": "Sorğunu işə sal", + "save": "Yadda saxla", + "save-cohort": "Koqortanı yadda saxla", + "save-replay": "Təkrar baxışı yadda saxla", + "save-segment": "Seqmenti yadda saxla", + "sample-rate": "Nümunə tezliyi", + "sample-size": "Nümunə ölçüsü", + "screen": "Ekran", + "screens": "Ekranlar", + "search": "Axtarış", + "segment": "Seqment", + "segments": "Seqmentlər", + "select": "Seç", + "select-link": "Keçid seç", + "select-pixel": "Piksel seç", + "select-date": "Tarix seç", + "select-filter": "Süzgəc seç", + "select-role": "Rol seç", + "select-component": "Komponent seç", + "select-event": "Hadisə seç", + "select-website": "Veb sayt seç", + "session": "Sessiya", + "session-data": "Sessiya xüsusiyyətləri", + "sessions": "Sessiyalar", + "settings": "Parametrlər", + "share": "Paylaş", + "share-url": "Paylaşım URL-i", + "single-day": "Tək gün", + "slug": "Slug", + "sms": "SMS", + "source": "Mənbə", + "sources": "Mənbələr", + "start-step": "Başlanğıc addım", + "steps": "Addımlar", + "sum": "Cəm", + "support": "Dəstək", + "switch-account": "Hesabı dəyiş", + "switch-to-card-view": "Kart görünüşünə keç", + "switch-to-table-view": "Cədvəl görünüşünə keç", + "table": "Cədvəl", + "tablet": "Planşet", + "tag": "Etiket", + "tags": "Etiketlər", + "team": "Komanda", + "team-id": "Komanda ID-si", + "team-manager": "Komanda meneceri", + "team-member": "Komanda üzvü", + "team-name": "Komanda adı", + "team-owner": "Komanda sahibi", + "team-settings": "Komanda parametrləri", + "team-view-only": "Komanda yalnız baxış", + "team-websites": "Komanda veb saytları", + "teams": "Komandalar", + "term": "Termin", + "terms": "Terminlər", + "theme": "Tema", + "this-month": "Bu ay", + "this-week": "Bu həftə", + "this-year": "Bu il", + "timezone": "Saat qurşağı", + "title": "Başlıq", + "today": "Bu gün", + "toggle-charts": "Qrafikləri aç/bağla", + "total": "Cəmi", + "total-records": "Ümumi qeydlər", + "tracking-code": "İzləmə kodu", + "traffic": "Trafik", + "transactions": "Tranzaksiyalar", + "transfer": "Köçür", + "transfer-website": "Veb saytı köçür", + "true": "Doğru", + "ttfb": "TTFB", + "type": "Növ", + "unique": "Unikal", + "unique-events": "Unikal hadisələr", + "unique-visitors": "Unikal ziyarətçilər", + "aov": "AOV", + "arpu": "ARPU", + "uniqueCustomers": "Unikal müştərilər", + "customers": "Müştərilər", + "note": "Qeyd", + "notes": "Qeydlər", + "orders": "Sifarişlər", + "saved": "Yadda saxlanıldı", + "unknown": "Naməlum", + "untitled": "Adsız", + "upgrade": "Təkmilləşdir", + "update": "Yenilə", + "url": "URL", + "user": "İstifadəçi", + "username": "İstifadəçi adı", + "users": "İstifadəçilər", + "utm": "UTM", + "utm-campaign": "UTM kampaniyası", + "utm-content": "UTM məzmunu", + "utm-description": "Kampaniyalarınızı UTM parametrləri vasitəsilə izləyin.", + "utm-medium": "UTM vasitəsi", + "utm-source": "UTM mənbəyi", + "utm-term": "UTM termini", + "value": "Dəyər", + "version": "Versiya", + "view": "Bax", + "view-details": "Ətraflı bax", + "view-only": "Yalnız baxış", + "views": "Baxışlar", + "views-per-visit": "Ziyarət başına baxış", + "visit-duration": "Ziyarət müddəti", + "visitors": "Ziyarətçilər", + "visits": "Ziyarətlər", + "website": "Veb sayt", + "website-id": "Veb sayt ID-si", + "websites": "Veb saytlar", + "window": "Pəncərə", + "yesterday": "Dünən", + "security": "Təhlükəsizlik", + "two-factor-auth": "İkifaktorlu doğrulama", + "two-factor-setup-title": "İkifaktorlu doğrulamanı qur", + "two-factor-step1": "1-ci addım", + "two-factor-step2": "2-ci addım", + "two-factor-scan-qr": "QR kodu skan et", + "two-factor-cant-scan": "QR kodu skan edə bilmirsiniz?", + "two-factor-manual-entry": "Bunun əvəzinə bu məxfi açarı daxil edin:", + "two-factor-copy-code": "Kodu kopyala", + "two-factor-code-copied": "Kopyalandı!", + "two-factor-get-code": "Doğrulama kodu al", + "two-factor-enter-code": "Doğrulama kodunu daxil et", + "two-factor-use-backup": "Bunun əvəzinə ehtiyat kodundan istifadə et", + "two-factor-save-backup-codes": "Ehtiyat kodlarınızı yadda saxlayın", + "two-factor-download-codes": "Kodları endir", + "two-factor-saved-codes": "Ehtiyat kodlarımı yadda saxladım", + "two-factor-backup-file-title": "Umami - İkifaktorlu doğrulama ehtiyat kodları", + "two-factor-success-title": "İkifaktorlu doğrulama aktivləşdirildi", + "two-factor-enable": "2FA-nı aktivləşdir", + "two-factor-disable": "2FA-nı söndür", + "two-factor-disable-title": "İkifaktorlu doğrulamanı söndür", + "two-factor-status-not-configured": "Konfiqurasiya edilməyib", + "two-factor-active": "Aktiv", + "two-factor-require-global": "Bütün istifadəçilər üçün 2FA tələb et", + "two-factor-require-user": "Bu istifadəçi üçün 2FA tələb et", + "two-factor-require-team": "Bu komandanın bütün üzvləri üçün 2FA tələb et", + "two-factor-reset": "2FA-nı sıfırla", + "two-factor-global-active-tooltip": "Qlobal 2FA aktivdir", + "two-factor-verify": "Təsdiqlə", + "two-factor-login": "İkifaktorlu doğrulama", + "api-keys": "API açarları", + "api-key": "API açarı", + "create-api-key": "API açarı yarat", + "last-used": "Son istifadə", + "never": "Heç vaxt", + "copy": "Kopyala", + "authorize": "İcazə ver", + "deny": "Rədd et" + }, + "message": { + "action-confirmation": "Təsdiqləmək üçün aşağıdakı xanaya {confirmation} yazın.", + "active-users": "{x} cari {x, plural, one {ziyarətçi} other {ziyarətçi}}", + "bad-request": "Yanlış sorğu", + "collected-data": "Toplanmış məlumat", + "confirm-delete": "{target} silmək istədiyinizə əminsiniz?", + "confirm-leave": "{target} tərk etmək istədiyinizə əminsiniz?", + "confirm-remove": "{target} çıxarmaq istədiyinizə əminsiniz?", + "confirm-reset": "{target} sıfırlamaq istədiyinizə əminsiniz?", + "delete-team-warning": "Komandanı silmək onun bütün veb saytlarını da siləcək.", + "delete-website-warning": "Veb saytın bütün məlumatları silinəcək.", + "error": "Nəsə səhv getdi.", + "empty-dashboard": "İdarə paneliniz boşdur. İlk komponentinizi əlavə etmək üçün Dizayn üzərinə klikləyin.", + "event-log": "{url} ünvanında {event}", + "forbidden": "Qadağandır", + "go-to-settings": "Parametrlərə keç", + "incorrect-username-password": "İstifadəçi adı və/və ya parol yanlışdır.", + "invalid-domain": "Yanlış domen. http/https daxil etməyin.", + "invalid-value": "Yanlış dəyər", + "max-length": "Maksimum {n} simvol uzunluğunda", + "min-password-length": "Minimum {n} simvol uzunluğunda", + "new-version-available": "Umami-nin yeni versiyası {version} mövcuddur!", + "no-data-available": "Məlumat yoxdur.", + "no-event-data": "Hadisə məlumatı yoxdur.", + "no-match-password": "Parollar uyğun gəlmir.", + "no-results-found": "Nəticə tapılmadı.", + "no-team-websites": "Bu komandanın heç bir veb saytı yoxdur.", + "no-teams": "Siz heç bir komanda yaratmamısınız.", + "no-users": "İstifadəçi yoxdur.", + "no-websites-configured": "Sizin heç bir konfiqurasiya edilmiş veb saytınız yoxdur.", + "not-found": "Tapılmadı", + "nothing-selected": "Heç nə seçilməyib.", + "page-not-found": "Səhifə tapılmadı", + "reset-website": "Bu veb saytı sıfırlamaq üçün təsdiqləmək məqsədilə aşağıdakı xanaya {confirmation} yazın.", + "reset-website-warning": "Bu veb saytın bütün statistikası silinəcək, lakin parametrləriniz qalacaq.", + "saved": "Yadda saxlanıldı.", + "select-board-entity-first": "Əvvəlcə veb sayt, piksel və ya keçid seçin.", + "select-component-preview": "Önbaxış üçün komponent seçin", + "select-website-first": "Əvvəlcə veb sayt seçin", + "sever-error": "Server xətası", + "share-url": "Veb saytınızın statistikası aşağıdakı URL-də hər kəsə açıqdır:", + "team-already-member": "Siz artıq bu komandanın üzvüsünüz.", + "team-not-found": "Komanda tapılmadı.", + "team-websites-info": "Veb saytları komandadakı hər kəs görə bilər.", + "tracking-code": "Bu veb saytın statistikasını izləmək üçün aşağıdakı kodu HTML-inizin '...' bölməsinə yerləşdirin.", + "transfer-team-website-to-user": "Bu veb sayt hesabınıza köçürülsün?", + "transfer-user-website-to-team": "Bu veb saytın köçürüləcəyi komandanı seçin.", + "transfer-website": "Veb saytın mülkiyyətini hesabınıza və ya başqa komandaya köçürün.", + "triggered-event": "Baş vermiş hadisə", + "unauthorized": "İcazəsiz", + "user-deleted": "İstifadəçi silindi.", + "replay-min-duration-description": "Yalnız bu müddətə bərabər və ya ondan uzun təkrar baxışları göstər.", + "viewed-page": "Baxılan səhifə", + "upgrade-required": "Bu funksiya {plan} plan abunəliyi tələb edir.", + "visitor-log": "{country}-dan {os} {device} üzərində {browser} istifadə edən ziyarətçi", + "two-factor-setup-description": "Hər dəfə daxil olanda, parolunuza əlavə olaraq, bir dəfəlik kod yaratmaq və istifadə etmək üçün doğrulayıcı tətbiqdən istifadə edəcəksiniz.", + "two-factor-setup-required-description": "Umami-yə daxil olmaq üçün ikifaktorlu doğrulama (2FA) tələb olunur.", + "two-factor-step1-description": "Aşağıdakı QR kodu skan edin və ya məxfi açarı əl ilə doğrulayıcı tətbiqinizə daxil edin.", + "two-factor-step2-description": "Doğrulayıcı tətbiqinizdə gördüyünüz 6 rəqəmli kodu daxil edin.", + "two-factor-enabled-description": "Hesabınız artıq ikifaktorlu doğrulama ilə qorunur. Bundan sonra hər daxil olanda doğrulayıcı tətbiqinizdən bir kod daxil etməli olacaqsınız.", + "two-factor-backup-description": "Doğrulayıcı tətbiqinizə girişi itirsəniz, daxil olmaq üçün bu bir dəfəlik ehtiyat kodlarından birini istifadə edə bilərsiniz. Onları təhlükəsiz bir yerdə saxlayın, bir daha göstərilməyəcək.", + "two-factor-backup-codes-once": "Bu kodları təhlükəsiz saxlayın. Hər kod yalnız bir dəfə istifadə edilə bilər.", + "two-factor-login-description": "Doğrulayıcı tətbiqinizdən 6 rəqəmli kodu daxil edin.", + "two-factor-disable-description": "Təsdiqləmək üçün parolunuzu və doğrulayıcı tətbiqinizdən doğrulama kodunu daxil edin.", + "two-factor-required-message": "Hesabınız üçün ikifaktorlu doğrulama məcburidir və söndürülə bilməz. Kömək üçün administratorunuzla əlaqə saxlayın.", + "two-factor-override-note": "Qlobal 2FA aktiv olduqda istifadəçi və komanda üzrə 2FA parametrləri əhəmiyyətini itirir.", + "two-factor-admin-global-description": "Bütün istifadəçilərdən növbəti daxilolmada ikifaktorlu doğrulamanı qurmalarını tələb edin. 2FA-nı hələ konfiqurasiya etməmiş istifadəçilərdən Umami-yə giriş əldə etməzdən əvvəl bunu etmək istəniləcək.", + "two-factor-require-user-description": "Aktivləşdirildikdə, bu istifadəçi hələ etməyibsə növbəti daxilolmada 2FA qurmağa dəvət ediləcək.", + "two-factor-require-team-description": "Aktivləşdirildikdə, bu komandanın bütün üzvləri hələ etməyiblərsə növbəti daxilolmada 2FA qurmağa dəvət ediləcək.", + "two-factor-user-description": "Hesabınıza əlavə təhlükəsizlik qatı əlavə edin. Hər daxil olanda parolunuz və doğrulayıcı tətbiqinizdən bir kod lazım olacaq.", + "two-factor-reset-description": "Bu istifadəçinin cari ikifaktorlu konfiqurasiyasını silin. Siyasətə görə 2FA hələ tələb olunursa, növbəti daxilolmada yeni QR kodu skan etmələri istəniləcək.", + "two-factor-error-invalid-code": "Yanlış doğrulama kodu. Zəhmət olmasa yenidən cəhd edin.", + "two-factor-error-missing-token": "Doğrulama tokeni yoxdur.", + "two-factor-error-invalid-partial-token": "Yanlış və ya vaxtı bitmiş doğrulama tokeni.", + "two-factor-error-invalid-backup-code": "Yanlış ehtiyat kodu.", + "two-factor-error-code-used": "Bu kod artıq istifadə olunub. Növbəti kodu gözləyin.", + "two-factor-error-too-many-attempts": "Çox sayda uğursuz cəhd. Hesabınız müvəqqəti bloklanıb.", + "two-factor-error-disable-not-allowed": "İkifaktorlu doğrulama məcburidir və söndürülə bilməz.", + "two-factor-error-incorrect-password": "Yanlış parol.", + "two-factor-error-already-enabled": "İkifaktorlu doğrulama artıq aktivdir.", + "two-factor-error-not-enabled": "İkifaktorlu doğrulama aktiv deyil.", + "two-factor-error-no-pending-setup": "Gözləyən ikifaktorlu doğrulama qurulumu tapılmadı.", + "two-factor-error-not-configured": "Serverdə etibarlı TWO_FACTOR_ENCRYPTION_KEY olmadığı üçün ikifaktorlu doğrulama əlçatan deyil. Kömək üçün administratorunuzla əlaqə saxlayın.", + "two-factor-locked-message": "Çox sayda uğursuz cəhd. {time} vaxtında yenidən cəhd edin.", + "two-factor-active-description": "Hesabınız ikifaktorlu doğrulama ilə qorunur. Hər daxilolma doğrulayıcı tətbiqinizdən bir kod tələb edir.", + "api-key-created": "API açarınızı indi kopyalayın. Onu bir daha görə bilməyəcəksiniz.", + "no-api-keys": "Siz heç bir API açarı yaratmamısınız.", + "oauth-login-prompt": "Davam etmək üçün daxil olun.", + "oauth-consent": "{name} Umami hesabınıza giriş əldə etmək istəyir.", + "oauth-permissions": "Bu, tətbiqə aşağıdakılara icazə verəcək:", + "oauth-scope-websites-read": "Girişiniz olan veb saytları görmək", + "oauth-scope-analytics-read": "Bu veb saytların analitika məlumatlarını oxumaq", + "oauth-redirect-notice": "Təsdiqlədikdən sonra {url} ünvanına yönləndiriləcəksiniz.", + "oauth-signed-in-as": "{username} kimi daxil olunub", + "oauth-invalid-request": "Bu icazə sorğusu etibarsızdır.", + "oauth-redirecting": "Yönləndirilir…" + } +} diff --git a/src/lib/lang.ts b/src/lib/lang.ts index 20737a334..976fd7e7c 100644 --- a/src/lib/lang.ts +++ b/src/lib/lang.ts @@ -1,5 +1,6 @@ import { arSA, + az, be, bg, bn, @@ -49,6 +50,7 @@ import { export const languages = { 'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' }, + 'az-AZ': { label: 'Azərbaycan', dateLocale: az }, 'be-BY': { label: 'Беларуская', dateLocale: be }, 'bg-BG': { label: 'български език', dateLocale: bg }, 'bn-BD': { label: 'বাংলা', dateLocale: bn }, From 1ee36860c4e15cb00af4d8810463f538ec06d5da Mon Sep 17 00:00:00 2001 From: dongwonmoon Date: Tue, 8 Sep 2026 15:35:49 +0900 Subject: [PATCH 7/7] Avoid sorting PostgreSQL visitor count queries --- src/lib/prisma.test.ts | 58 ++++++++++++++++++- src/lib/prisma.ts | 3 +- .../sql/sessions/getWebsiteSessions.ts | 2 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/lib/prisma.test.ts b/src/lib/prisma.test.ts index 3a8e9df2e..cbb417bea 100644 --- a/src/lib/prisma.test.ts +++ b/src/lib/prisma.test.ts @@ -24,9 +24,10 @@ vi.mock('@/generated/prisma/client', () => ({ })); let getRawQueryClient!: typeof import('./prisma').getRawQueryClient; +let prisma!: typeof import('./prisma').default; beforeAll(async () => { - ({ getRawQueryClient } = await import('./prisma')); + ({ getRawQueryClient, default: prisma } = await import('./prisma')); }); interface RawQueryClient { @@ -80,3 +81,58 @@ describe('getRawQueryClient', () => { expect(getRawQueryClient(client, { write: true })).toBe(client); }); }); + +describe('pagedRawQuery default ordering', () => { + function mockQueries(count = '4') { + const queryRaw = vi.mocked((prisma.client as any).$queryRawUnsafe); + queryRaw.mockClear(); + queryRaw.mockImplementation(async (sql: string) => + sql.includes('count(*) as num') ? [{ num: count }] : [{ id: 1 }], + ); + return queryRaw; + } + + test.each([undefined, 5])( + 'applies trusted default order only to the page query with cap %s', + async maxResults => { + const queryRaw = mockQueries('5'); + const result = await prisma.pagedRawQuery( + 'select * from thing', + {}, + { page: 2, pageSize: 2, maxResults }, + 'test', + 'max(created_at) desc, session_id', + ); + + expect(queryRaw).toHaveBeenCalledTimes(2); + expect(queryRaw.mock.calls[0][0]).not.toContain('order by max(created_at) desc'); + expect(queryRaw.mock.calls[1][0]).toContain('order by max(created_at) desc, session_id'); + expect(queryRaw.mock.calls[1][0]).toContain('limit 2 offset 2'); + expect(result).toEqual({ + data: [{ id: 1 }], + count: 5, + page: 2, + pageSize: 2, + isCapped: !!maxResults, + orderBy: undefined, + }); + }, + ); + + test('keeps explicit ordering and legacy no-order behavior', async () => { + const queryRaw = mockQueries(); + const result = await prisma.pagedRawQuery( + 'select * from thing', + {}, + { page: 1, pageSize: 2, orderBy: 'name', sortDescending: true }, + undefined, + 'max(created_at) desc, session_id', + ); + expect(queryRaw.mock.calls[1][0]).toContain('order by name desc'); + expect(queryRaw.mock.calls[1][0]).not.toContain('order by max(created_at) desc, session_id'); + expect(result.orderBy).toBe('name'); + queryRaw.mockClear(); + await prisma.pagedRawQuery('select * from thing', {}, { page: 1, pageSize: 2 }); + expect(queryRaw.mock.calls[1][0]).not.toMatch(/order by/); + }); +}); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 2a09c6ad0..11dc2a189 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -786,6 +786,7 @@ async function pagedRawQuery( queryParams: Record, filters: QueryFilters, name?: string, + defaultOrderBy?: string, ) { const { page = 1, pageSize, orderBy, sortDescending = false } = filters; const size = +pageSize || DEFAULT_PAGE_SIZE; @@ -793,7 +794,7 @@ async function pagedRawQuery( const direction = sortDescending ? 'desc' : 'asc'; const statements = [ - orderBy && `order by ${orderBy} ${direction}`, + orderBy ? `order by ${orderBy} ${direction}` : defaultOrderBy && `order by ${defaultOrderBy}`, +size > 0 && `limit ${+size} offset ${offset}`, ] .filter(n => n) diff --git a/src/queries/sql/sessions/getWebsiteSessions.ts b/src/queries/sql/sessions/getWebsiteSessions.ts index 67d8f7323..541b69be5 100644 --- a/src/queries/sql/sessions/getWebsiteSessions.ts +++ b/src/queries/sql/sessions/getWebsiteSessions.ts @@ -75,11 +75,11 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { session.country, session.region, session.city - order by max(website_event.created_at) desc, session.session_id `, queryParams, filters, FUNCTION_NAME, + 'max(website_event.created_at) desc, session.session_id', ); }