From 19ccfa0745410c4e501a6ce7a577ab51893fbb44 Mon Sep 17 00:00:00 2001 From: Matt Harrington Date: Fri, 13 Jun 2025 12:17:18 -0700 Subject: [PATCH 1/6] fixing the clickhouse schema file --- db/clickhouse/schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index fef600e0..1080b4d1 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -122,7 +122,7 @@ CREATE TABLE umami.website_event_stats_hourly min_time SimpleAggregateFunction(min, DateTime('UTC')), max_time SimpleAggregateFunction(max, DateTime('UTC')), tag SimpleAggregateFunction(groupArrayArray, Array(String)), - distinct_id, + distinct_id String, created_at Datetime('UTC') ) ENGINE = AggregatingMergeTree @@ -213,7 +213,7 @@ FROM (SELECT min(created_at) min_time, max(created_at) max_time, arrayFilter(x -> x != '', groupArray(tag)) tag, - distinct_id String, + distinct_id, toStartOfHour(created_at) timestamp FROM umami.website_event GROUP BY website_id, From 42c0ccc2eb6cc5aa0ed34992a04bef97e9019d10 Mon Sep 17 00:00:00 2001 From: Sov3rain <10755450+Sov3rain@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:17:22 +0200 Subject: [PATCH 2/6] Add keepalive option to tracker fetch request --- src/tracker/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tracker/index.js b/src/tracker/index.js index 905ba434..76d29a1d 100644 --- a/src/tracker/index.js +++ b/src/tracker/index.js @@ -150,6 +150,7 @@ try { const res = await fetch(endpoint, { + keepalive: true, method: 'POST', body: JSON.stringify({ type, payload }), headers: { From d72833af133bacdfc3a8093f7a86216e41369659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Alasj=C3=B6?= Date: Thu, 26 Jun 2025 11:37:05 +0200 Subject: [PATCH 3/6] Default empty string, prevent frameAncestors to be undefined --- next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index 792d21f1..9646a7f6 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -16,7 +16,7 @@ const disableLogin = process.env.DISABLE_LOGIN; const disableUI = process.env.DISABLE_UI; const faviconURL = process.env.FAVICON_URL; const forceSSL = process.env.FORCE_SSL; -const frameAncestors = process.env.ALLOWED_FRAME_URLS; +const frameAncestors = process.env.ALLOWED_FRAME_URLS ? process.env.ALLOWED_FRAME_URLS : ''; const privateMode = process.env.PRIVATE_MODE; const trackerScriptName = process.env.TRACKER_SCRIPT_NAME; const trackerScriptURL = process.env.TRACKER_SCRIPT_URL; From 5a5e3a1502d977b25740d25ae90f5e2c3de75982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Alasj=C3=B6?= Date: Thu, 26 Jun 2025 11:45:42 +0200 Subject: [PATCH 4/6] Nullish coalescing --- next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.mjs b/next.config.mjs index 9646a7f6..69ce52be 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -16,7 +16,7 @@ const disableLogin = process.env.DISABLE_LOGIN; const disableUI = process.env.DISABLE_UI; const faviconURL = process.env.FAVICON_URL; const forceSSL = process.env.FORCE_SSL; -const frameAncestors = process.env.ALLOWED_FRAME_URLS ? process.env.ALLOWED_FRAME_URLS : ''; +const frameAncestors = process.env.ALLOWED_FRAME_URLS ?? ''; const privateMode = process.env.PRIVATE_MODE; const trackerScriptName = process.env.TRACKER_SCRIPT_NAME; const trackerScriptURL = process.env.TRACKER_SCRIPT_URL; From 26ddfd5a80683b1750a821d87cadff781d8fa146 Mon Sep 17 00:00:00 2001 From: Alex Escalante <656454+AlexEscalante@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:40:21 -0600 Subject: [PATCH 5/6] Sanitize IP to remove port for geolocation lookup Sanitize IP address to remove port number before geolocation lookup. This ensures proper MaxMind database resolution in setups where IP:PORT is passed by the proxy. --- src/lib/detect.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/detect.ts b/src/lib/detect.ts index a023d27d..ae750469 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -127,7 +127,9 @@ export async function getLocation(ip: string = '', headers: Headers, hasPayloadI global[MAXMIND] = await maxmind.open(path.resolve(dir, 'GeoLite2-City.mmdb')); } - const result = global[MAXMIND].get(ip); + // When the client IP is extracted from headers, sometimes the value includes a port + const cleanIp = ip?.split(':')[0]; + const result = global[MAXMIND].get(cleanIp); if (result) { const country = result.country?.iso_code ?? result?.registered_country?.iso_code; From b73a67915da2940ea0aaa6839dd12347cb8b5419 Mon Sep 17 00:00:00 2001 From: eoussama Date: Sun, 6 Jul 2025 21:18:04 +0100 Subject: [PATCH 6/6] Added optional website ID for creation --- cypress/e2e/api-website.cy.ts | 33 +++++++++++++++++++++++++++++++++ src/app/api/websites/route.ts | 5 +++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/api-website.cy.ts b/cypress/e2e/api-website.cy.ts index 3fba48d3..7f7d17c3 100644 --- a/cypress/e2e/api-website.cy.ts +++ b/cypress/e2e/api-website.cy.ts @@ -1,3 +1,5 @@ +import { uuid } from '../../src/lib/crypto'; + describe('Website API tests', () => { Cypress.session.clearAllSavedSessions(); @@ -65,6 +67,37 @@ describe('Website API tests', () => { }); }); + it('Creates a website with a fixed ID.', () => { + cy.fixture('websites').then(data => { + const websiteCreate = data.websiteCreate; + const fixedId = uuid(); + cy.request({ + method: 'POST', + url: '/api/websites', + headers: { + 'Content-Type': 'application/json', + Authorization: Cypress.env('authorization'), + }, + body: { ...websiteCreate, id: fixedId }, + }).then(response => { + expect(response.status).to.eq(200); + expect(response.body).to.have.property('id', fixedId); + expect(response.body).to.have.property('name', 'Cypress Website'); + expect(response.body).to.have.property('domain', 'cypress.com'); + + // cleanup + cy.request({ + method: 'DELETE', + url: `/api/websites/${fixedId}`, + headers: { + 'Content-Type': 'application/json', + Authorization: Cypress.env('authorization'), + }, + }); + }); + }); + }); + it('Returns all tracked websites.', () => { cy.request({ method: 'GET', diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index b8fb2a0b..7a2146f8 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -26,6 +26,7 @@ export async function POST(request: Request) { domain: z.string().max(500), shareId: z.string().max(50).nullable().optional(), teamId: z.string().nullable().optional(), + id: z.string().uuid().nullable().optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -34,14 +35,14 @@ export async function POST(request: Request) { return error(); } - const { name, domain, shareId, teamId } = body; + const { id, name, domain, shareId, teamId } = body; if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { return unauthorized(); } const data: any = { - id: uuid(), + id: id ?? uuid(), createdBy: auth.user.id, name, domain,