Compare commits

...

7 commits

Author SHA1 Message Date
Mike Cao
df786d1fbc
Merge pull request #3590 from halkeye/halkeye/chore/pnpm
Some checks are pending
Node.js CI / build (mysql, 18.18) (push) Waiting to run
Node.js CI / build (postgresql, 18.18) (push) Waiting to run
chore: finish migration of yarn/npm to pnpm everywhere
2025-08-27 22:29:51 -07:00
Mike Cao
19a87388cd
Merge pull request #3594 from 0xflotus/patch-2
chore: sort properties alphabetically
2025-08-27 20:41:27 -07:00
Mike Cao
d972765760
Merge pull request #3597 from badmike/feat/cloudfront-location-headers
feat: Add AWS CloudFront geolocation headers support
2025-08-27 20:38:52 -07:00
Michael Wallner
58c2d068e7
refactor getLocation to use lookup array for cleaner header extraction 2025-08-27 17:47:24 +02:00
Michael Wallner
8df72c55e5
add support for CloudFront headers in getLocation 2025-08-26 17:28:13 +02:00
0xflotus
ea2206f2e9
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`.
2025-08-25 21:02:13 +02:00
Gavin Mogan
0e6442c469 chore: finish migration of yarn/npm to pnpm everywhere 2025-08-22 16:21:57 -07:00
5 changed files with 182 additions and 281 deletions

View file

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

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

@ -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',
'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: '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: '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: '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',
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',
};

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,30 +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'));
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,
};
}
// 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,
};
return {
country,
region: getRegionCode(country, region),
city,
};
}
}
}