Merge branch 'master' into dev

# Conflicts:
#	.github/workflows/ci.yml
#	src/lang/es-ES.json
#	src/lang/sl-SI.json
#	src/lib/constants.ts
#	src/lib/detect.ts
#	src/queries/sql/reports/getRevenue.ts
This commit is contained in:
Mike Cao 2025-09-16 21:11:12 -07:00
commit 04c06443a8
9 changed files with 259 additions and 370 deletions

View file

@ -19,18 +19,14 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
env:
DATABASE_TYPE: ${{ matrix.db-type }}
- run: npm install --global pnpm
- run: pnpm install
- run: pnpm test
- run: pnpm build

1
.gitignore vendored
View file

@ -4,6 +4,7 @@
node_modules
.pnp
.pnp.js
.pnpm-store
# testing
/coverage

View file

@ -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:

View file

@ -134,7 +134,7 @@
"label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas",
"label.last-months": "Últimos {x} meses",
"label.last-seen": "Última vez visto",
"label.last-seen": "Visto por última vez",
"label.leave": "Abandonar",
"label.leave-team": "Abandonar equipo",
"label.less-than": "Menor que",
@ -210,8 +210,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": "Ingresos",
"label.revenue-description": "Consulte sus ingresos a lo largo del tiempo.",
"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",
@ -223,7 +224,6 @@
"label.select-role": "Seleccionar rol",
"label.select-website": "Seleccionar sitio web",
"label.session": "Sesión",
"label.session-data": "Datos de sesión",
"label.sessions": "Sesiones",
"label.settings": "Ajustes",
"label.share": "Compartir",
@ -259,18 +259,19 @@
"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.user": "Usuario",
"label.user-property": "Propiedad de usuario",
"label.username": "Nombre de usuario",
"label.users": "Usuarios",
"label.utm": "UTM",

View file

@ -32,27 +32,21 @@
"label.cities": "Mesta",
"label.city": "Mesto",
"label.clear-all": "Počisti vse",
"label.cohort": "Kohorta",
"label.compare": "Primerjaj",
"label.compare-dates": "Primerjaj datume",
"label.confirm": "Potrdi",
"label.confirm-password": "Potrdi geslo",
"label.contains": "Vsebuje",
"label.content": "Vsebina",
"label.continue": "Nadaljuj",
"label.conversion": "Konverzija",
"label.conversion-rate": "Stopnja konverzije",
"label.conversion-step": "Korak konverzije",
"label.count": "Števec",
"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": "Ustvaril",
"label.currency": "Valuta",
"label.current": "Trenutno",
"label.current-password": "Trenutno geslo",
"label.custom-range": "Obdobje po meri",
@ -83,16 +77,14 @@
"label.edit": "Uredi",
"label.edit-dashboard": "Uredi nadzorno ploščo",
"label.edit-member": "Uredi člana",
"label.email": "Email",
"label.enable-share-url": "Uredi povezavo za deljenje",
"label.enable-share-url": "Omogoči povezavo za deljenje",
"label.end-step": "Končni korak",
"label.entry": "Vhodni URL",
"label.entry": "Vstopni URL",
"label.event": "Dogodek",
"label.event-data": "Podatki dogodka",
"label.event-name": "Ime dogodka",
"label.events": "Dogodki",
"label.exists": "Obstaja",
"label.exit": "Exit URL",
"label.exit": "Izhodni URL",
"label.false": "Napačno",
"label.field": "Polje",
"label.fields": "Polja",
@ -100,22 +92,18 @@
"label.filter-combined": "Skupaj",
"label.filter-raw": "Neobdelano",
"label.filters": "Filtri",
"label.first-click": "Prvi klik",
"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.funnels": "Lijaki",
"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.grouped": "Združeno",
"label.hostname": "Ime gostitelja",
"label.includes": "Vključuje",
"label.insight": "Vpogled",
"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-false": "Je napačno",
"label.is-not": "Ni",
@ -124,9 +112,8 @@
"label.is-true": "Je res",
"label.join": "Pridruži se",
"label.join-team": "Pridruži se ekipi",
"label.journey": "Potovanje",
"label.journey": "Uporabniška pot",
"label.journey-description": "Razumite, kako uporabniki krmarijo po vašem spletnem mestu.",
"label.journeys": "Potovanja",
"label.language": "Jezik",
"label.languages": "Jeziki",
"label.laptop": "Prenosni računalnik",
@ -134,7 +121,7 @@
"label.last-days": "Zadnjih {x} dni",
"label.last-hours": "Zadnjih {x} ur",
"label.last-months": "Zadnjih {x} mesecev",
"label.last-seen": "Zadnjič videno",
"label.last-seen": "Nazadnje viden",
"label.leave": "Zapusti",
"label.leave-team": "Zapusti ekipo",
"label.less-than": "Manjše kot",
@ -142,11 +129,9 @@
"label.links": "Povezave",
"label.login": "Prijava",
"label.logout": "Odjava",
"label.manage": "Manage",
"label.manager": "Manager",
"label.manage": "Upravljaj",
"label.manager": "Upravitelj",
"label.max": "Največ",
"label.maximize": "Razširi",
"label.medium": "Srednje",
"label.member": "Član",
"label.members": "Člani",
"label.min": "Najmanj",
@ -182,7 +167,6 @@
"label.password": "Geslo",
"label.path": "Pot",
"label.paths": "Poti",
"label.pixels": "Pikslov",
"label.powered-by": "Poganja {name}",
"label.previous": "Prejšnji",
"label.previous-period": "Prejšnje obdobje",
@ -203,48 +187,44 @@
"label.regions": "Regije",
"label.remaining": "Preostalo",
"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": "Prihodek",
"label.revenue-description": "Oglejte si svoj prihodek skozi čas.",
"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",
"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-filter": "Izberi filter",
"label.select-role": "Select role",
"label.select-role": "Izberi vlogo",
"label.select-website": "Izberi spletno mesto",
"label.session": "Seja",
"label.session-data": "Podatki seje",
"label.sessions": "Seje",
"label.settings": "Nastavitve",
"label.share": "Deli",
"label.share-url": "Deli povezavo",
"label.single-day": "En dan",
"label.sms": "SMS",
"label.sources": "Viri",
"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.tag": "Oznaka",
"label.tags": "Oznake",
"label.team": "Ekipa",
"label.team-id": "ID ekipe",
"label.team-manager": "Vodja ekipe",
"label.team-manager": "Upravitelj ekipe",
"label.team-member": "Član ekipe",
"label.team-name": "Ime ekipe",
"label.team-owner": "Lastnik ekipe",
"label.team-settings": "Nastavitve 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.terms": "Pogoji",
@ -286,18 +266,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.bad-request": "Bad request",
"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}",
@ -327,12 +306,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.unauthorized": "Unauthorized",
"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",
"message.user-deleted": "Uporabnik je izbrisan.",
"message.viewed-page": "Viewed page",
"message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}"
"message.viewed-page": "Ogledana stran",
"message.visitor-log": "Obiskovalec iz {country} uporablja {browser} na {os} {device}",
"message.visitors-dropped-off": "Osip obiskovalcev"
}

View file

@ -1,6 +1,7 @@
import * as detect from '../detect';
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';
@ -16,6 +17,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);
});

View file

@ -126,12 +126,12 @@ export const DATA_TYPES = {
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 = {
@ -223,7 +223,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',
@ -261,8 +261,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)',
@ -284,15 +284,17 @@ export const BROWSERS = {
phantomjs: 'PhantomJS',
safari: 'Safari',
samsung: 'Samsung',
silk: 'Silk',
searchbot: 'Searchbot',
silk: 'Silk',
yandexbrowser: 'Yandex',
} as const;
// 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',
'true-client-ip',
@ -301,354 +303,246 @@ export const IP_ADDRESS_HEADERS = [
'x-forwarded',
'forwarded',
'x-appengine-user-ip',
'x-nf-client-connection-ip',
'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',
'ecosia.org',
'google.',
'msn.com',
'perplexity.ai',
'search.brave.com',
'yandex.',
'baidu.com',
'ecosia.org',
'chatgpt.com',
'perplexity.ai',
];
export const SHOPPING_DOMAINS = [
'amazon.',
'ebay.com',
'walmart.com',
'alibab.com',
'alibaba.com',
'aliexpress.com',
'etsy.com',
'amazon.',
'bestbuy.com',
'target.com',
'ebay.com',
'etsy.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=',
'dclid=',
'twclid=',
'li_fat_id=',
'epik=',
'ttclid=',
'scid=',
'aid=',
'pc_id=',
'ad_id=',
'rdt_cid=',
'aid=',
'dclid=',
'epik=',
'fbclid=',
'gclid=',
'li_fat_id=',
'msclkid=',
'ob_click_id=',
'pc_id=',
'rdt_cid=',
'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: 'Baidu', domain: 'baidu.com', match: 'baidu.' },
{ name: 'Bing', domain: 'bing.com', match: 'bing.' },
{ name: 'Brave', domain: 'brave.com', match: 'brave.' },
{ name: 'DuckDuckGo', domain: 'duckduckgo.com', match: 'duckduckgo.' },
{ name: 'Twitter', domain: 'twitter.com', match: ['twitter.', 't.co', 'x.com'] },
{ name: 'Instagram', domain: 'instagram.com', match: ['instagram.', 'ig.com'] },
{ name: 'Snapchat', domain: 'snapchat.com', match: 'snapchat.' },
{ name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' },
{ name: 'ChatGPT', domain: 'chatgpt.com', match: 'chatgpt.' },
{ 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.' },
{ name: 'Pinterest', domain: 'pinterest.com', match: 'pinterest.' },
{ 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: 'Yahoo', domain: 'yahoo.com', match: 'yahoo.' },
{ name: 'Yandex', domain: 'yandex.ru', match: 'yandex.' },
{ name: 'Baidu', domain: 'baidu.com', match: 'baidu.' },
];
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',
MRT: 'MR',
MUS: 'MU',
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',
NGA: 'NG',
NIU: 'NU',
NFK: 'NF',
MNE: 'ME',
MNG: 'MN',
MNP: 'MP',
MOZ: 'MZ',
MRT: 'MR',
MSR: 'MS',
MTQ: 'MQ',
MUS: 'MU',
MWI: 'MW',
MYS: 'MY',
MYT: 'YT',
NAM: 'NA',
NCL: 'NC',
NER: 'NE',
NFK: 'NF',
NGA: 'NG',
NIC: 'NI',
NIU: 'NU',
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',
SGS: 'GS',
SHN: 'SH',
SJM: 'SJ',
SLB: 'SB',
SLE: 'SL',
SMR: 'SM',
SOM: 'SO',
SPM: 'PM',
SRB: 'RS',
SSD: 'SS',
STP: 'ST',
SUR: 'SR',
SVK: 'SK',
SVN: 'SI',
SLB: 'SB',
SOM: 'SO',
ZAF: 'ZA',
SGS: 'GS',
SSD: 'SS',
ESP: 'ES',
LKA: 'LK',
SDN: 'SD',
SUR: 'SR',
SJM: 'SJ',
SWZ: 'SZ',
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',
};
export const CURRENCIES = [

View file

@ -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,11 +115,12 @@ 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'));
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,
@ -106,18 +128,6 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI
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,
};
}
}

View file

@ -217,6 +217,7 @@ async function clickhouseQuery(
and website_revenue.currency = {currency:String}
${filterQuery}
group by website_event.country
order by value desc
`,
queryParams,
);