From 438baea95671ae43a54867ee9961e003c7971f57 Mon Sep 17 00:00:00 2001 From: Mark Kuhar Date: Wed, 13 Aug 2025 11:15:35 +0200 Subject: [PATCH 01/12] update slovenian translation --- src/lang/sl-SI.json | 120 ++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index bd180bfb9b..26d4f2910d 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -4,8 +4,8 @@ "label.activity": "Dnevnik dejavnosti", "label.add": "Dodaj", "label.add-description": "Dodaj opis", - "label.add-member": "Add member", - "label.add-step": "Add step", + "label.add-member": "Dodaj člana", + "label.add-step": "Dodaj korak", "label.add-website": "Dodaj spletno mesto", "label.admin": "Administrator", "label.after": "Po", @@ -24,21 +24,21 @@ "label.cities": "Mesta", "label.city": "Mesto", "label.clear-all": "Počisti vse", - "label.compare": "Compare", + "label.compare": "Primerjaj", "label.confirm": "Potrdi", "label.confirm-password": "Potrdi geslo", "label.contains": "Vsebuje", "label.continue": "Nadaljuj", - "label.count": "Count", + "label.count": "Število", "label.countries": "Države", "label.country": "Država", - "label.create": "Create", + "label.create": "Ustvari", "label.create-report": "Ustvari poročilo", "label.create-team": "Ustvari ekipo", "label.create-user": "Ustvari uporabnika", "label.created": "Ustvarjeno", - "label.created-by": "Created By", - "label.current": "Current", + "label.created-by": "Ustvaril", + "label.current": "Trenutno", "label.current-password": "Trenutno geslo", "label.custom-range": "Obdobje po meri", "label.dashboard": "Nadzorna plošča", @@ -48,7 +48,7 @@ "label.day": "Dan", "label.default-date-range": "Privzeto časovno obdobje", "label.delete": "Izbriši", - "label.delete-report": "Delete report", + "label.delete-report": "Izbriši poročilo", "label.delete-team": "Izbriši ekipo", "label.delete-user": "Izbriši uporabnika", "label.delete-website": "Izbriši spletno mesto", @@ -63,14 +63,14 @@ "label.dropoff": "Zapustitev", "label.edit": "Uredi", "label.edit-dashboard": "Uredi nadzorno ploščo", - "label.edit-member": "Edit member", - "label.enable-share-url": "Uredi povezavo za deljenje", - "label.end-step": "End Step", - "label.entry": "Entry URL", + "label.edit-member": "Uredi člana", + "label.enable-share-url": "Omogoči povezavo za deljenje", + "label.end-step": "Končni korak", + "label.entry": "Vstopni URL", "label.event": "Dogodek", "label.event-data": "Podatki dogodka", "label.events": "Dogodki", - "label.exit": "Exit URL", + "label.exit": "Izhodni URL", "label.false": "Napačno", "label.field": "Polje", "label.fields": "Polja", @@ -78,48 +78,48 @@ "label.filter-combined": "Skupaj", "label.filter-raw": "Neobdelano", "label.filters": "Filtri", - "label.first-seen": "First seen", + "label.first-seen": "Prvič viden", "label.funnel": "Prodajni lijak", - "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.funnel-description": "Razumite stopnjo konverzije in osipa uporabnikov.", + "label.goal": "Cilj", + "label.goals": "Cilji", + "label.goals-description": "Spremljajte svoje cilje za oglede strani in dogodke.", "label.greater-than": "Večje od", "label.greater-than-equals": "Večje ali enako kot", - "label.host": "Host", - "label.hosts": "Hosts", + "label.host": "Gostitelj", + "label.hosts": "Gostitelji", "label.insights": "Vpogled", - "label.insights-description": "Dive deeper into your data by using segments and filters.", + "label.insights-description": "Poglobite se v podatke z uporabo segmentov in filtrov.", "label.is": "Je", "label.is-not": "Ni", "label.is-not-set": "Ni nastavljeno", "label.is-set": "Je nastavljeno", "label.join": "Pridruži se", "label.join-team": "Pridruži se ekipi", - "label.journey": "Journey", - "label.journey-description": "Understand how users navigate through your website.", + "label.journey": "Uporabniška pot", + "label.journey-description": "Razumite, kako uporabniki krmarijo po vašem spletnem mestu.", "label.language": "Jezik", "label.languages": "Jeziki", "label.laptop": "Prenosni računalnik", "label.last-days": "Zadnjih {x} dni", "label.last-hours": "Zadnjih {x} ur", - "label.last-months": "Last {x} months", - "label.last-seen": "Last seen", + "label.last-months": "Zadnjih {x} mesecev", + "label.last-seen": "Nazadnje viden", "label.leave": "Zapusti", "label.leave-team": "Zapusti ekipo", "label.less-than": "Manjše kot", "label.less-than-equals": "Manjše ali enako kot", "label.login": "Prijava", "label.logout": "Odjava", - "label.manage": "Manage", - "label.manager": "Manager", + "label.manage": "Upravljaj", + "label.manager": "Upravitelj", "label.max": "Največ", - "label.member": "Member", + "label.member": "Član", "label.members": "Člani", "label.min": "Najmanj", "label.mobile": "Mobilne naprave", "label.more": "Več", - "label.my-account": "My account", + "label.my-account": "Moj račun", "label.my-websites": "Moja spletna mesta", "label.name": "Ime", "label.new-password": "Novo geslo", @@ -134,15 +134,15 @@ "label.pageTitle": "Naslov strani", "label.pages": "Strani", "label.password": "Geslo", - "label.path": "Path", - "label.paths": "Paths", + "label.path": "Pot", + "label.paths": "Poti", "label.powered-by": "Poganja {name}", - "label.previous": "Previous", - "label.previous-period": "Previous period", - "label.previous-year": "Previous year", + "label.previous": "Prejšnji", + "label.previous-period": "Prejšnje obdobje", + "label.previous-year": "Prejšnje leto", "label.profile": "Profil", - "label.properties": "Properties", - "label.property": "Property", + "label.properties": "Lastnosti", + "label.property": "Lastnost", "label.queries": "Poizvedbe", "label.query": "Poizvedba", "label.query-parameters": "Parametri poizvedbe", @@ -154,41 +154,41 @@ "label.region": "Regija", "label.regions": "Regije", "label.remove": "Odstrani", - "label.remove-member": "Remove member", + "label.remove-member": "Odstrani člana", "label.reports": "Poročila", "label.required": "Zahtevano", "label.reset": "Ponastavi", "label.reset-website": "Ponastavi statistiko", "label.retention": "Ohranjanje uporabnikov", - "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-description": "Merite zadržanje uporabnikov s sledenjem, kako pogosto se vračajo.", + "label.revenue": "Prihodki", + "label.revenue-description": "Preglejte svoje prihodke skozi čas.", + "label.revenue-property": "Lastnost prihodkov", "label.role": "Vloga", "label.run-query": "Izvedi poizvedbo", "label.save": "Shrani", "label.screens": "Zasloni", - "label.search": "Search", - "label.select": "Select", + "label.search": "Išči", + "label.select": "Izberi", "label.select-date": "Izberi datum", - "label.select-role": "Select role", + "label.select-role": "Izberi vlogo", "label.select-website": "Izberi spletno mesto", - "label.session": "Session", + "label.session": "Seja", "label.sessions": "Seje", "label.settings": "Nastavitve", "label.share-url": "Deli povezavo", "label.single-day": "En dan", - "label.start-step": "Start Step", - "label.steps": "Steps", + "label.start-step": "Začetni korak", + "label.steps": "Koraki", "label.sum": "Seštevek", "label.tablet": "Tablični računalnik", "label.team": "Ekipa", "label.team-id": "ID ekipe", - "label.team-manager": "Team manager", + "label.team-manager": "Upravitelj ekipe", "label.team-member": "Član ekipe", "label.team-name": "Ime ekipe", "label.team-owner": "Lastnik ekipe", - "label.team-view-only": "Team view only", + "label.team-view-only": "Ekipa samo za ogled", "label.team-websites": "Spletna mesta ekipe", "label.teams": "Ekipe", "label.theme": "Tema", @@ -232,17 +232,17 @@ "label.visits": "Visits", "label.website": "Spletno mesto", "label.website-id": "ID spletnega mesta", - "label.websites": "Spletnih mest", + "label.websites": "Spletna mesta", "label.window": "Okno", "label.yesterday": "Včeraj", - "message.action-confirmation": "Type {confirmation} in the box below to confirm.", + "message.action-confirmation": "Za potrditev v spodnje polje vnesite {confirmation}.", "message.active-users": "{x} trenutni {x, plural, one {obiskovalec} other {obiskovalcev}}", - "message.collected-data": "Collected data", + "message.collected-data": "Zbrani podatki", "message.confirm-delete": "Ste prepričani, da želite izbrisati {target}?", "message.confirm-leave": "Ste prepričani, da želite zapustiti {target}?", - "message.confirm-remove": "Are you sure you want to remove {target}?", + "message.confirm-remove": "Ali ste prepričani, da želite odstraniti {target}?", "message.confirm-reset": "Ste prepričani, da želite ponastaviti statistiko {target}?", - "message.delete-team-warning": "Deleting a team will also delete all team websites.", + "message.delete-team-warning": "Brisanje ekipe bo izbrisalo tudi vsa spletna mesta ekipe.", "message.delete-website-warning": "Izbrisani bodo tudi vsi pripadajoči podatki.", "message.error": "Nekaj je šlo narobe.", "message.event-log": "{event} na {url}", @@ -268,12 +268,12 @@ "message.team-not-found": "Ekipa ni bila najdena.", "message.team-websites-info": "Spletne strani si lahko ogleda vsak član ekipe.", "message.tracking-code": "Koda za sledenje", - "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.transfer-team-website-to-user": "Prenesete to spletno mesto v svoj račun?", + "message.transfer-user-website-to-team": "Izberite ekipo, na katero želite prenesti to spletno mesto.", + "message.transfer-website": "Prenesite lastništvo spletnega mesta na svoj račun ali drugo ekipo.", + "message.triggered-event": "Sprožen dogodek", "message.user-deleted": "Uporabnik je izbrisan.", - "message.viewed-page": "Viewed page", + "message.viewed-page": "Ogledana stran", "message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}", - "message.visitors-dropped-off": "Visitors dropped off" + "message.visitors-dropped-off": "Osip obiskovalcev" } From a1e3b5fd4af4208caf1dc32c11a4d70e029c1f2b Mon Sep 17 00:00:00 2001 From: Mark Kuhar Date: Wed, 13 Aug 2025 12:51:06 +0200 Subject: [PATCH 02/12] fix --- src/lang/sl-SI.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/sl-SI.json b/src/lang/sl-SI.json index 26d4f2910d..9113aa45fc 100644 --- a/src/lang/sl-SI.json +++ b/src/lang/sl-SI.json @@ -160,7 +160,7 @@ "label.reset": "Ponastavi", "label.reset-website": "Ponastavi statistiko", "label.retention": "Ohranjanje uporabnikov", - "label.retention-description": "Merite zadržanje uporabnikov s sledenjem, kako pogosto se vračajo.", + "label.retention-description": "Merite uporabnikovo zadržanost s sledenjem, kako pogosto se vračajo.", "label.revenue": "Prihodki", "label.revenue-description": "Preglejte svoje prihodke skozi čas.", "label.revenue-property": "Lastnost prihodkov", @@ -268,7 +268,7 @@ "message.team-not-found": "Ekipa ni bila najdena.", "message.team-websites-info": "Spletne strani si lahko ogleda vsak član ekipe.", "message.tracking-code": "Koda za sledenje", - "message.transfer-team-website-to-user": "Prenesete to spletno mesto v svoj račun?", + "message.transfer-team-website-to-user": "Želite prenesti to spletno mesto v svoj račun?", "message.transfer-user-website-to-team": "Izberite ekipo, na katero želite prenesti to spletno mesto.", "message.transfer-website": "Prenesite lastništvo spletnega mesta na svoj račun ali drugo ekipo.", "message.triggered-event": "Sprožen dogodek", From 0e6442c469f985fe0383808d183bdc469bbfddaf Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Fri, 22 Aug 2025 16:21:57 -0700 Subject: [PATCH 03/12] chore: finish migration of yarn/npm to pnpm everywhere --- .github/workflows/ci.yml | 10 +++++----- .gitignore | 1 + README.md | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 835407b412..478e5ad16f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,10 +29,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'yarn' + cache: 'pnpm' env: DATABASE_TYPE: ${{ matrix.db-type }} - - run: npm install --global yarn - - run: yarn install - - run: yarn test - - run: yarn build + - run: npm install --global pnpm + - run: pnpm install + - run: pnpm test + - run: pnpm build diff --git a/.gitignore b/.gitignore index 70a1e19316..9cf14dd6a4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules .pnp .pnp.js +.pnpm-store # testing /coverage diff --git a/README.md b/README.md index cf84d76240..fcbe856f1e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.i ```bash git clone https://github.com/umami-software/umami.git cd umami -npm install +pnpm install ``` ### Configure Umami @@ -64,7 +64,7 @@ mysql://username:mypassword@localhost:3306/mydb ### Build the Application ```bash -npm run build +pnpm run build ``` _The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**._ @@ -72,7 +72,7 @@ _The build step will create tables in your database if you are installing for th ### Start the Application ```bash -npm run start +pnpm run start ``` _By default, this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly._ @@ -107,8 +107,8 @@ To get the latest features, simply do a pull, install any new dependencies, and ```bash git pull -npm install -npm run build +pnpm install +pnpm run build ``` To update the Docker image, simply pull the new images and rebuild: From ea2206f2e97aaed626cdd6db788d45ca140e88cd Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Mon, 25 Aug 2025 21:02:13 +0200 Subject: [PATCH 04/12] chore: sort properties alphabetically I have sorted some of the properties alphabetically so that you can see more quickly in the future which ones may still be missing. I think it's easier to add some new ones this way. I also fixed the `alibaba.com` domain from the typo `alibab.com`. --- src/lib/constants.ts | 360 +++++++++++++++---------------------------- 1 file changed, 125 insertions(+), 235 deletions(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 7ac1d2abe6..7c710326f6 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -154,12 +154,12 @@ export const KAFKA_TOPIC = { export const ROLES = { admin: 'admin', - user: 'user', - viewOnly: 'view-only', - teamOwner: 'team-owner', teamManager: 'team-manager', teamMember: 'team-member', + teamOwner: 'team-owner', teamViewOnly: 'team-view-only', + user: 'user', + viewOnly: 'view-only', } as const; export const PERMISSIONS = { @@ -267,7 +267,7 @@ export const URL_LENGTH = 500; export const PAGE_TITLE_LENGTH = 500; export const EVENT_NAME_LENGTH = 50; -export const UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; +export const UTM_PARAMS = ['utm_campaign', 'utm_content', 'utm_medium', 'utm_source', 'utm_term']; export const DESKTOP_OS = [ 'BeOS', @@ -305,8 +305,8 @@ export const OS_NAMES = { export const BROWSERS = { android: 'Android', aol: 'AOL', - beaker: 'Beaker', bb10: 'BlackBerry 10', + beaker: 'Beaker', chrome: 'Chrome', 'chromium-webview': 'Chrome (webview)', crios: 'Chrome (iOS)', @@ -328,366 +328,256 @@ export const BROWSERS = { phantomjs: 'PhantomJS', safari: 'Safari', samsung: 'Samsung', - silk: 'Silk', searchbot: 'Searchbot', + silk: 'Silk', yandexbrowser: 'Yandex', }; export const IP_ADDRESS_HEADERS = [ 'cf-connecting-ip', - 'x-client-ip', - 'x-forwarded-for', 'do-connecting-ip', 'fastly-client-ip', + 'forwarded', 'true-client-ip', - 'x-real-ip', + 'x-appengine-user-ip', + 'x-client-ip', 'x-cluster-client-ip', 'x-forwarded', - 'forwarded', - 'x-appengine-user-ip', + 'x-forwarded-for', + 'x-real-ip', ]; export const SOCIAL_DOMAINS = [ + 'bsky.app', 'facebook.com', 'fb.com', - 'instagram.com', 'ig.com', - 'twitter.com', - 't.co', - 'x.com', + 'instagram.com', 'linkedin.', - 'tiktok.', - 'reddit.', - 'threads.net', - 'bsky.app', 'news.ycombinator.com', - 'snapchat.', 'pinterest.', + 'reddit.', + 'snapchat.', + 't.co', + 'threads.net', + 'tiktok.', + 'twitter.com', + 'x.com', ]; export const SEARCH_DOMAINS = [ - 'google.', + 'baidu.com', 'bing.com', - 'msn.com', + 'chatgpt.com', 'duckduckgo.com', - 'search.brave.com', - 'yandex.', - 'baidu.com', 'ecosia.org', - 'chatgpt.com', + 'google.', + 'msn.com', 'perplexity.ai', + 'search.brave.com', + 'yandex.', ]; export const SHOPPING_DOMAINS = [ + 'alibaba.com', + 'aliexpress.com', 'amazon.', + 'bestbuy.com', 'ebay.com', - 'walmart.com', - 'alibab.com', - 'aliexpress.com', 'etsy.com', - 'bestbuy.com', - 'target.com', 'newegg.com', + 'target.com', + 'walmart.com', ]; export const EMAIL_DOMAINS = [ 'gmail.', + 'hotmail.', 'mail.yahoo.', 'outlook.', - 'hotmail.', - 'protonmail.', 'proton.me', + 'protonmail.', ]; -export const VIDEO_DOMAINS = ['youtube.', 'twitch.']; +export const VIDEO_DOMAINS = ['twitch.', 'youtube.']; export const PAID_AD_PARAMS = [ - 'utm_source=google', - 'gclid=', - 'fbclid=', - 'msclkid=', + 'ad_id=', + 'aid=', 'dclid=', - 'twclid=', - 'li_fat_id=', 'epik=', - 'ttclid=', - 'scid=', - 'aid=', + 'fbclid=', + 'gclid=', + 'li_fat_id=', + 'msclkid=', + 'ob_click_id=', 'pc_id=', - 'ad_id=', 'rdt_cid=', - 'ob_click_id=', + 'scid=', + 'ttclid=', + 'twclid=', 'utm_medium=cpc', 'utm_medium=paid', 'utm_medium=paid_social', + 'utm_source=google', ]; export const GROUPED_DOMAINS = [ - { name: 'Google', domain: 'google.com', match: 'google.' }, - { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' }, - { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' }, - { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' }, - { name: 'GitHub', domain: 'github.com', match: 'github.' }, - { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' }, { name: 'Bing', domain: 'bing.com', match: 'bing.' }, { name: 'Brave', domain: 'brave.com', match: 'brave.' }, + { name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' }, { name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' }, - { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] }, + { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' }, + { name: 'GitHub', domain: 'github.com', match: 'github.' }, + { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' }, { name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] }, - { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' }, + { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' }, { name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' }, - { name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' }, + { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' }, + { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' }, + { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] }, + { name: 'Google', domain: 'google.com', match: 'google.' }, ]; export const MAP_FILE = '/datamaps.world.json'; export const ISO_COUNTRIES = { - AFG: 'AF', - ALA: 'AX', - ALB: 'AL', - DZA: 'DZ', - ASM: 'AS', - AND: 'AD', - AGO: 'AO', - AIA: 'AI', - ATA: 'AQ', - ATG: 'AG', - ARG: 'AR', - ARM: 'AM', - ABW: 'AW', - AUS: 'AU', - AUT: 'AT', - AZE: 'AZ', - BHS: 'BS', - BHR: 'BH', - BGD: 'BD', - BRB: 'BB', - BLR: 'BY', - BEL: 'BE', - BLZ: 'BZ', - BEN: 'BJ', - BMU: 'BM', - BTN: 'BT', - BOL: 'BO', - BIH: 'BA', - BWA: 'BW', - BVT: 'BV', - BRA: 'BR', - VGB: 'VG', - IOT: 'IO', - BRN: 'BN', - BGR: 'BG', - BFA: 'BF', - BDI: 'BI', - KHM: 'KH', - CMR: 'CM', - CAN: 'CA', - CPV: 'CV', - CYM: 'KY', - CAF: 'CF', - TCD: 'TD', - CHL: 'CL', - CHN: 'CN', - HKG: 'HK', - MAC: 'MO', - CXR: 'CX', - CCK: 'CC', - COL: 'CO', - COM: 'KM', - COG: 'CG', - COD: 'CD', - COK: 'CK', - CRI: 'CR', - CIV: 'CI', - HRV: 'HR', - CUB: 'CU', - CYP: 'CY', - CZE: 'CZ', - DNK: 'DK', - DJI: 'DJ', - DMA: 'DM', - DOM: 'DO', - ECU: 'EC', - EGY: 'EG', - SLV: 'SV', - GNQ: 'GQ', - ERI: 'ER', - EST: 'EE', - ETH: 'ET', - FLK: 'FK', - FRO: 'FO', - FJI: 'FJ', - FIN: 'FI', - FRA: 'FR', - GUF: 'GF', - PYF: 'PF', - ATF: 'TF', - GAB: 'GA', - GMB: 'GM', - GEO: 'GE', - DEU: 'DE', - GHA: 'GH', - GIB: 'GI', - GRC: 'GR', - GRL: 'GL', - GRD: 'GD', - GLP: 'GP', - GUM: 'GU', - GTM: 'GT', - GGY: 'GG', - GIN: 'GN', - GNB: 'GW', - GUY: 'GY', - HTI: 'HT', - HMD: 'HM', - VAT: 'VA', - HND: 'HN', - HUN: 'HU', - ISL: 'IS', - IND: 'IN', - IDN: 'ID', - IRN: 'IR', - IRQ: 'IQ', - IRL: 'IE', - IMN: 'IM', - ISR: 'IL', - ITA: 'IT', + ANT: 'AN', + ARE: 'AE', + BLM: 'BL', + CHE: 'CH', + ESH: 'EH', + ESP: 'ES', + FSM: 'FM', + GBR: 'GB', JAM: 'JM', - JPN: 'JP', JEY: 'JE', JOR: 'JO', + JPN: 'JP', KAZ: 'KZ', KEN: 'KE', + KGZ: 'KG', KIR: 'KI', - PRK: 'KP', + KNA: 'KN', KOR: 'KR', KWT: 'KW', - KGZ: 'KG', LAO: 'LA', - LVA: 'LV', LBN: 'LB', - LSO: 'LS', LBR: 'LR', LBY: 'LY', + LCA: 'LC', LIE: 'LI', + LKA: 'LK', + LSO: 'LS', LTU: 'LT', LUX: 'LU', - MKD: 'MK', + LVA: 'LV', + MAF: 'MF', + MAR: 'MA', + MCO: 'MC', + MDA: 'MD', MDG: 'MG', - MWI: 'MW', - MYS: 'MY', MDV: 'MV', + MEX: 'MX', + MHL: 'MH', + MKD: 'MK', MLI: 'ML', MLT: 'MT', - MHL: 'MH', - MTQ: 'MQ', + MMR: 'MM', + MNE: 'ME', + MNG: 'MN', + MNP: 'MP', + MOZ: 'MZ', MRT: 'MR', + MSR: 'MS', + MTQ: 'MQ', MUS: 'MU', + MWI: 'MW', + MYS: 'MY', MYT: 'YT', - MEX: 'MX', - FSM: 'FM', - MDA: 'MD', - MCO: 'MC', - MNG: 'MN', - MNE: 'ME', - MSR: 'MS', - MAR: 'MA', - MOZ: 'MZ', - MMR: 'MM', NAM: 'NA', - NRU: 'NR', - NPL: 'NP', - NLD: 'NL', - ANT: 'AN', NCL: 'NC', - NZL: 'NZ', - NIC: 'NI', NER: 'NE', + NFK: 'NF', NGA: 'NG', + NIC: 'NI', NIU: 'NU', - NFK: 'NF', - MNP: 'MP', + NLD: 'NL', NOR: 'NO', + NPL: 'NP', + NRU: 'NR', + NZL: 'NZ', OMN: 'OM', PAK: 'PK', - PLW: 'PW', - PSE: 'PS', PAN: 'PA', - PNG: 'PG', - PRY: 'PY', + PCN: 'PN', PER: 'PE', PHL: 'PH', - PCN: 'PN', + PLW: 'PW', + PNG: 'PG', POL: 'PL', - PRT: 'PT', PRI: 'PR', + PRK: 'KP', + PRT: 'PT', + PRY: 'PY', + PSE: 'PS', QAT: 'QA', REU: 'RE', ROU: 'RO', RUS: 'RU', RWA: 'RW', - BLM: 'BL', - SHN: 'SH', - KNA: 'KN', - LCA: 'LC', - MAF: 'MF', - SPM: 'PM', - VCT: 'VC', - WSM: 'WS', - SMR: 'SM', - STP: 'ST', SAU: 'SA', + SDN: 'SD', SEN: 'SN', - SRB: 'RS', - SYC: 'SC', - SLE: 'SL', SGP: 'SG', - SVK: 'SK', - SVN: 'SI', + SGS: 'GS', + SHN: 'SH', + SJM: 'SJ', SLB: 'SB', + SLE: 'SL', + SMR: 'SM', SOM: 'SO', - ZAF: 'ZA', - SGS: 'GS', + SPM: 'PM', + SRB: 'RS', SSD: 'SS', - ESP: 'ES', - LKA: 'LK', - SDN: 'SD', + STP: 'ST', SUR: 'SR', - SJM: 'SJ', - SWZ: 'SZ', + SVK: 'SK', + SVN: 'SI', SWE: 'SE', - CHE: 'CH', + SWZ: 'SZ', + SYC: 'SC', SYR: 'SY', - TWN: 'TW', - TJK: 'TJ', - TZA: 'TZ', - THA: 'TH', - TLS: 'TL', + TCA: 'TC', TGO: 'TG', + THA: 'TH', + TJK: 'TJ', TKL: 'TK', + TKM: 'TM', + TLS: 'TL', TON: 'TO', TTO: 'TT', TUN: 'TN', TUR: 'TR', - TKM: 'TM', - TCA: 'TC', TUV: 'TV', + TWN: 'TW', + TZA: 'TZ', UGA: 'UG', UKR: 'UA', - ARE: 'AE', - GBR: 'GB', - USA: 'US', UMI: 'UM', URY: 'UY', + USA: 'US', UZB: 'UZ', - VUT: 'VU', + VCT: 'VC', VEN: 'VE', - VNM: 'VN', VIR: 'VI', + VNM: 'VN', + VUT: 'VU', WLF: 'WF', - ESH: 'EH', + WSM: 'WS', + XKX: 'XK', YEM: 'YE', + ZAF: 'ZA', ZMB: 'ZM', ZWE: 'ZW', - XKX: 'XK', }; From 8df72c55e564b10e98102b12333e5d8322f9eaa6 Mon Sep 17 00:00:00 2001 From: Michael Wallner Date: Tue, 26 Aug 2025 17:28:13 +0200 Subject: [PATCH 05/12] add support for CloudFront headers in getLocation --- src/lib/detect.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 2e6a067ddc..526ea2dfc6 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -119,6 +119,19 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI city, }; } + + // CloudFront headers + if (headers.get('cloudfront-viewer-country')) { + const country = decodeHeader(headers.get('cloudfront-viewer-country')); + const region = decodeHeader(headers.get('cloudfront-viewer-country-region')); + const city = decodeHeader(headers.get('cloudfront-viewer-city')); + + return { + country, + region: getRegionCode(country, region), + city, + }; + } } // Database lookup From 58c2d068e7a2c4369f4bae848d5319df8b3f33ae Mon Sep 17 00:00:00 2001 From: Michael Wallner Date: Wed, 27 Aug 2025 17:47:24 +0200 Subject: [PATCH 06/12] refactor getLocation to use lookup array for cleaner header extraction --- src/lib/detect.ts | 71 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 526ea2dfc6..ee9d2603c8 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -15,6 +15,27 @@ import { safeDecodeURIComponent } from '@/lib/url'; const MAXMIND = 'maxmind'; +const PROVIDER_HEADERS = [ + // Cloudflare headers + { + countryHeader: 'cf-ipcountry', + regionHeader: 'cf-region-code', + cityHeader: 'cf-ipcity', + }, + // Vercel headers + { + countryHeader: 'x-vercel-ip-country', + regionHeader: 'x-vercel-ip-country-region', + cityHeader: 'x-vercel-ip-city', + }, + // CloudFront headers + { + countryHeader: 'cloudfront-viewer-country', + regionHeader: 'cloudfront-viewer-country-region', + cityHeader: 'cloudfront-viewer-city', + }, +]; + export function getIpAddress(headers: Headers) { const customHeader = process.env.CLIENT_IP_HEADER; @@ -94,43 +115,19 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI } if (!hasPayloadIP && !process.env.SKIP_LOCATION_HEADERS) { - // Cloudflare headers - if (headers.get('cf-ipcountry')) { - const country = decodeHeader(headers.get('cf-ipcountry')); - const region = decodeHeader(headers.get('cf-region-code')); - const city = decodeHeader(headers.get('cf-ipcity')); - - return { - country, - region: getRegionCode(country, region), - city, - }; - } - - // Vercel headers - if (headers.get('x-vercel-ip-country')) { - const country = decodeHeader(headers.get('x-vercel-ip-country')); - const region = decodeHeader(headers.get('x-vercel-ip-country-region')); - const city = decodeHeader(headers.get('x-vercel-ip-city')); - - return { - country, - region: getRegionCode(country, region), - city, - }; - } - - // CloudFront headers - if (headers.get('cloudfront-viewer-country')) { - const country = decodeHeader(headers.get('cloudfront-viewer-country')); - const region = decodeHeader(headers.get('cloudfront-viewer-country-region')); - const city = decodeHeader(headers.get('cloudfront-viewer-city')); - - return { - country, - region: getRegionCode(country, region), - city, - }; + for (const provider of PROVIDER_HEADERS) { + const countryHeader = headers.get(provider.countryHeader); + if (countryHeader) { + const country = decodeHeader(countryHeader); + const region = decodeHeader(headers.get(provider.regionHeader)); + const city = decodeHeader(headers.get(provider.cityHeader)); + + return { + country, + region: getRegionCode(country, region), + city, + }; + } } } From de3e9d0be3fae7adf0d036a7b383be79bdab20f9 Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Thu, 28 Aug 2025 08:29:30 +0200 Subject: [PATCH 07/12] fix: put Google in the right order of grouped domains --- src/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 7c710326f6..2718c135c8 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -429,6 +429,7 @@ export const GROUPED_DOMAINS = [ { name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' }, { name: 'Facebook', domain: 'facebook.com', match: 'facebook.' }, { name: 'GitHub', domain: 'github.com', match: 'github.' }, + { name: 'Google', domain: 'google.com', match: 'google.' }, { name: 'Hacker News', domain: 'news.ycombinator.com', match: 'news.ycombinator.com' }, { name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] }, { name: 'LinkedIn', domain: 'linkedin.com', match: 'linkedin.' }, @@ -436,7 +437,6 @@ export const GROUPED_DOMAINS = [ { name: 'Reddit', domain: 'reddit.com', match: 'reddit.' }, { name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' }, { name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] }, - { name: 'Google', domain: 'google.com', match: 'google.' }, ]; export const MAP_FILE = '/datamaps.world.json'; From c5298d5d45fa1ae8fd5488cc539a4f505ff75b7b Mon Sep 17 00:00:00 2001 From: Chairil Fauzi Firmansyah Date: Thu, 4 Sep 2025 18:00:28 +0700 Subject: [PATCH 08/12] fix(hash): improve URL normalization and handling in tracking functions --- src/tracker/index.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/tracker/index.js b/src/tracker/index.js index 76d29a1ddd..b05d908598 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -38,6 +38,18 @@ /* Helper functions */ + const normalize = raw => { + if (!raw) return raw; + try { + const u = new URL(raw, location.href); + if (excludeSearch) u.search = ''; + if (excludeHash) u.hash = ''; + return u.toString(); + } catch (e) { + return raw; + } + }; + const getPayload = () => ({ website, screen, @@ -61,11 +73,7 @@ if (!url) return; currentRef = currentUrl; - currentUrl = new URL(url, location.href); - - if (excludeSearch) currentUrl.search = ''; - if (excludeHash) currentUrl.hash = ''; - currentUrl = currentUrl.toString(); + currentUrl = normalize(new URL(url, location.href).toString()); if (currentUrl !== currentRef) { setTimeout(track, delayDuration); @@ -210,8 +218,9 @@ }; } - let currentUrl = href; - let currentRef = referrer.startsWith(origin) ? '' : referrer; + let currentUrl = normalize(href); + let currentRef = normalize(referrer.startsWith(origin) ? '' : referrer); + let initialized = false; let disabled = false; let cache; From 2177256a2c537cdf9ab114efa74ccdc5c21320ed Mon Sep 17 00:00:00 2001 From: Nick Maynard Date: Mon, 8 Sep 2025 23:05:22 +0100 Subject: [PATCH 09/12] Fix ordering to allow X-Forwarded-For to be correctly managed by Cloudflare --- src/lib/__tests__/detect.test.ts | 7 +++++++ src/lib/constants.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lib/__tests__/detect.test.ts b/src/lib/__tests__/detect.test.ts index 1cb558ad8a..0ee3457260 100644 --- a/src/lib/__tests__/detect.test.ts +++ b/src/lib/__tests__/detect.test.ts @@ -2,6 +2,7 @@ import * as detect from '../detect'; import { expect } from '@jest/globals'; const IP = '127.0.0.1'; +const BAD_IP = '127.127.127.127'; test('getIpAddress: Custom header', () => { process.env.CLIENT_IP_HEADER = 'x-custom-ip-header'; @@ -17,6 +18,12 @@ test('getIpAddress: Standard header', () => { expect(detect.getIpAddress(new Headers({ 'x-forwarded-for': IP }))).toEqual(IP); }); +test('getIpAddress: CloudFlare header is lower priority than standard header', () => { + expect( + detect.getIpAddress(new Headers({ 'cf-connecting-ip': BAD_IP, 'x-forwarded-for': IP })), + ).toEqual(IP); +}); + test('getIpAddress: No header', () => { expect(detect.getIpAddress(new Headers())).toEqual(null); }); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2718c135c8..9c9ecd96c6 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -333,18 +333,20 @@ export const BROWSERS = { yandexbrowser: 'Yandex', }; +// The order here is important and influences how IPs are detected by lib/detect.ts +// Please do not change the order unless you know exactly what you're doing - read https://developers.cloudflare.com/fundamentals/reference/http-headers/ export const IP_ADDRESS_HEADERS = [ - 'cf-connecting-ip', + 'x-client-ip', + 'x-forwarded-for', + 'cf-connecting-ip', // This should be *after* x-forwarded-for, so that x-forwarded-for is respected if present 'do-connecting-ip', 'fastly-client-ip', - 'forwarded', 'true-client-ip', - 'x-appengine-user-ip', - 'x-client-ip', + 'x-real-ip', 'x-cluster-client-ip', 'x-forwarded', - 'x-forwarded-for', - 'x-real-ip', + 'forwarded', + 'x-appengine-user-ip', ]; export const SOCIAL_DOMAINS = [ From d471a972eb1118b925235d3a4193a4339724c014 Mon Sep 17 00:00:00 2001 From: Matias Facello Date: Tue, 9 Sep 2025 22:04:06 -0300 Subject: [PATCH 10/12] Adds missing spanish translations --- src/lang/es-ES.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lang/es-ES.json b/src/lang/es-ES.json index 5f930be7b6..9bcfab5036 100644 --- a/src/lang/es-ES.json +++ b/src/lang/es-ES.json @@ -78,7 +78,7 @@ "label.filter-combined": "Combinado", "label.filter-raw": "En crudo", "label.filters": "Filtros", - "label.first-seen": "First seen", + "label.first-seen": "Visto por primera vez", "label.funnel": "Embudo", "label.funnel-description": "Comprender conversión y abandono de usuarios.", "label.goal": "Objetivo", @@ -104,7 +104,7 @@ "label.last-days": "Últimos {x} días", "label.last-hours": "Últimas {x} horas", "label.last-months": "Últimos {x} meses", - "label.last-seen": "Last seen", + "label.last-seen": "Visto por última vez", "label.leave": "Abandonar", "label.leave-team": "Abandonar equipo", "label.less-than": "Menor que", @@ -124,7 +124,7 @@ "label.name": "Nombre", "label.new-password": "Nueva contraseña", "label.none": "Ninguno", - "label.number-of-records": "{x} {x, plural, one {record} other {records}}", + "label.number-of-records": "{x} {x, plural, one {registro} other {registros}}", "label.ok": "OK", "label.os": "Sistema", "label.overview": "Resumen", @@ -141,7 +141,7 @@ "label.previous-period": "Periodo anterior", "label.previous-year": "Año anterior", "label.profile": "Perfil", - "label.properties": "Properties", + "label.properties": "Propiedades", "label.property": "Propiedad", "label.queries": "Consultas", "label.query": "Consulta", @@ -161,9 +161,9 @@ "label.reset-website": "Reiniciar analíticas", "label.retention": "Retención", "label.retention-description": "Medir la frecuencia con la que los usuarios vuelven a tu sitio web.", - "label.revenue": "Revenue", - "label.revenue-description": "Look into your revenue across time.", - "label.revenue-property": "Revenue Property", + "label.revenue": "Ganancias", + "label.revenue-description": "Analice sus ganancias a lo largo del tiempo.", + "label.revenue-property": "Propiedad de ganancias", "label.role": "Rol", "label.run-query": "Ejecutar consulta", "label.save": "Guardar", @@ -173,7 +173,7 @@ "label.select-date": "Seleccionar fecha", "label.select-role": "Seleccionar rol", "label.select-website": "Seleccionar sitio web", - "label.session": "Session", + "label.session": "Sesión", "label.sessions": "Sesiones", "label.settings": "Ajustes", "label.share-url": "Compartir URL", @@ -202,21 +202,21 @@ "label.total": "Total", "label.total-records": "Total de registros", "label.tracking-code": "Código de rastreo", - "label.transactions": "Transactions", + "label.transactions": "Transacciones", "label.transfer": "Transferir", "label.transfer-website": "Transferir sitio web", "label.true": "Verdadero", "label.type": "Tipo", "label.unique": "Único", "label.unique-visitors": "Visitantes únicos", - "label.uniqueCustomers": "Unique Customers", + "label.uniqueCustomers": "Clientes únicos", "label.unknown": "Desconocida", "label.untitled": "Sin título", "label.update": "Actualizar", "label.url": "URL", "label.urls": "URLs", "label.user": "Usuario", - "label.user-property": "User Property", + "label.user-property": "Propiedad de usuario", "label.username": "Nombre de usuario", "label.users": "Usuarios", "label.utm": "UTM", From bfcc822b4055f0f80212770407348de0ba62bc55 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 14 Sep 2025 22:10:21 -0300 Subject: [PATCH 11/12] fix(geo): parse netlify ip header by default: `x-nf-client-connection-ip` This header is not clearly documented, but it is mentioned in semi-official sources, and I have tested it to ensure it's working properly. Without this, Umami is unable to detect geolocation by default if deployed on Netlify. * https://answers.netlify.com/t/is-the-client-ip-header-going-to-be-supported-long-term/11203/2 * https://httptoolkit.com/blog/what-is-x-forwarded-for/#and-others --- src/lib/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2718c135c8..18fc652b71 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -344,6 +344,7 @@ export const IP_ADDRESS_HEADERS = [ 'x-cluster-client-ip', 'x-forwarded', 'x-forwarded-for', + 'x-nf-client-connection-ip', 'x-real-ip', ]; From b2c829a077fc388bb17e7c045a702be507b08dc0 Mon Sep 17 00:00:00 2001 From: Nick Maynard Date: Wed, 17 Sep 2025 21:37:57 +0100 Subject: [PATCH 12/12] Add setup-pnpm to hopefully fix CI tests etc. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 478e5ad16f..d2e027cfb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,12 +19,18 @@ jobs: matrix: include: - node-version: 18.18 + pnpm-version: 10 db-type: postgresql - node-version: 18.18 + pnpm-version: 10 db-type: mysql steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 # required so that setup-node will work + with: + version: ${{ matrix.pnpm-version }} + run_install: false - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: