From 02f0df3a2e28a0ccc47b243b3d8031221ea23875 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Apr 2025 13:59:23 -0700 Subject: [PATCH 1/4] Allow custom favicon URL. Closes #3365 --- next.config.js => next.config.mjs | 15 +++++++++------ src/components/common/Favicon.tsx | 8 ++++---- src/lib/constants.ts | 1 + 3 files changed, 14 insertions(+), 10 deletions(-) rename next.config.js => next.config.mjs (95%) diff --git a/next.config.js b/next.config.mjs similarity index 95% rename from next.config.js rename to next.config.mjs index 621c8b5b..ac8490a3 100644 --- a/next.config.js +++ b/next.config.mjs @@ -1,5 +1,9 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -require('dotenv').config(); +import dotenv from 'dotenv'; +import { createRequire } from 'module'; + +dotenv.config(); + +const require = createRequire(import.meta.url); const pkg = require('./package.json'); const TRACKER_SCRIPT = '/script.js'; @@ -12,6 +16,7 @@ const corsMaxAge = process.env.CORS_MAX_AGE; const defaultLocale = process.env.DEFAULT_LOCALE; 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 privateMode = process.env.PRIVATE_MODE; @@ -180,17 +185,17 @@ if (cloudMode && cloudUrl) { } /** @type {import('next').NextConfig} */ -const config = { +export default { reactStrictMode: false, env: { basePath, cloudMode, cloudUrl, - configUrl: '/config', currentVersion: pkg.version, defaultLocale, disableLogin, disableUI, + faviconURL, privateMode, }, basePath, @@ -237,5 +242,3 @@ const config = { return [...redirects]; }, }; - -module.exports = config; diff --git a/src/components/common/Favicon.tsx b/src/components/common/Favicon.tsx index ea3f31aa..c02fe74f 100644 --- a/src/components/common/Favicon.tsx +++ b/src/components/common/Favicon.tsx @@ -1,4 +1,4 @@ -import { GROUPED_DOMAINS } from '@/lib/constants'; +import { FAVICON_URL, GROUPED_DOMAINS } from '@/lib/constants'; function getHostName(url: string) { const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?([^:/\n?=]+)/im); @@ -10,10 +10,10 @@ export function Favicon({ domain, ...props }) { return null; } + const url = process.env.faviconURL || FAVICON_URL; const hostName = domain ? getHostName(domain) : null; - const src = hostName - ? `https://icons.duckduckgo.com/ip3/${GROUPED_DOMAINS[hostName]?.domain || hostName}.ico` - : null; + const domainName = GROUPED_DOMAINS[hostName]?.domain || hostName; + const src = hostName ? url.replace(/\{\{\s*domain\s*}}/, domainName) : null; return hostName ? : null; } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index f2a42fa7..2060811f 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -12,6 +12,7 @@ export const HOMEPAGE_URL = 'https://umami.is'; export const REPO_URL = 'https://github.com/umami-software/umami'; export const UPDATES_URL = 'https://api.umami.is/v1/updates'; export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png'; +export const FAVICON_URL = 'https://icons.duckduckgo.com/ip3/{{domain}}.ico'; export const DEFAULT_LOCALE = process.env.defaultLocale || 'en-US'; export const DEFAULT_THEME = 'light'; From b8a582c8daa3df8b736d9ca814ce0428b98dd99c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Apr 2025 14:18:23 -0700 Subject: [PATCH 2/4] Fixed events search. Closes #3329 --- package.json | 2 +- src/queries/sql/events/getWebsiteEvents.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e65b7196..7ff69489 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "umami", "version": "2.18.0", - "description": "A simple, fast, privacy-focused alternative to Google Analytics.", + "description": "A modern, privacy-focused alternative to Google Analytics.", "author": "Umami Software, Inc. ", "license": "MIT", "homepage": "https://umami.is", diff --git a/src/queries/sql/events/getWebsiteEvents.ts b/src/queries/sql/events/getWebsiteEvents.ts index 6fe7a0a1..d47b0665 100644 --- a/src/queries/sql/events/getWebsiteEvents.ts +++ b/src/queries/sql/events/getWebsiteEvents.ts @@ -52,7 +52,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar limit 1000) select * from events `, - { ...params, query: `%${search}%` }, + { ...params, search: `%${search}%` }, pageParams, ); } From d4b786380be27717ce7f69cfe047920ca8a19cfc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Apr 2025 18:06:41 -0700 Subject: [PATCH 3/4] Events filtering. Closes #3356 --- .../[websiteId]/events/EventsPage.tsx | 12 ++++++-- src/components/charts/Chart.tsx | 30 +++++++++++-------- src/components/metrics/EventsChart.tsx | 15 ++++++++-- src/components/metrics/EventsTable.tsx | 23 ++++++++++++-- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx index cf4c19ef..285a230e 100644 --- a/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx +++ b/src/app/(main)/websites/[websiteId]/events/EventsPage.tsx @@ -4,27 +4,33 @@ import EventsDataTable from './EventsDataTable'; import EventsMetricsBar from './EventsMetricsBar'; import EventsChart from '@/components/metrics/EventsChart'; import { GridRow } from '@/components/layout/Grid'; -import MetricsTable from '@/components/metrics/MetricsTable'; +import EventsTable from '@/components/metrics/EventsTable'; import { useMessages } from '@/components/hooks'; import { Item, Tabs } from 'react-basics'; import { useState } from 'react'; import EventProperties from './EventProperties'; export default function EventsPage({ websiteId }) { + const [label, setLabel] = useState(null); const [tab, setTab] = useState('activity'); const { formatMessage, labels } = useMessages(); + const handleLabelClick = (value: string) => { + setLabel(value !== label ? value : ''); + }; + return ( <> - - +
diff --git a/src/components/charts/Chart.tsx b/src/components/charts/Chart.tsx index dde01eb4..a21086bf 100644 --- a/src/components/charts/Chart.tsx +++ b/src/components/charts/Chart.tsx @@ -34,7 +34,7 @@ export function Chart({ className, chartOptions, }: ChartProps) { - const canvas = useRef(); + const canvas = useRef(null); const chart = useRef(null); const [legendItems, setLegendItems] = useState([]); @@ -86,7 +86,7 @@ export function Chart({ dataset.data = data?.datasets[index]?.data; if (chart.current.legend.legendItems[index]) { - chart.current.legend.legendItems[index].text = data?.datasets[index]?.label; + chart.current.legend.legendItems[index].text = data.datasets[index]?.label; } } }); @@ -95,6 +95,12 @@ export function Chart({ } } + if (data.focusLabel !== null) { + chart.current.data.datasets.forEach(ds => { + ds.hidden = data.focusLabel ? ds.label !== data.focusLabel : false; + }); + } + chart.current.options = options; // Allow config changes before update @@ -105,16 +111,6 @@ export function Chart({ setLegendItems(chart.current.legend.legendItems); }; - useEffect(() => { - if (data) { - if (!chart.current) { - createChart(data); - } else { - updateChart(data); - } - } - }, [data, options]); - const handleLegendClick = (item: LegendItem) => { if (type === 'bar') { const { datasetIndex } = item; @@ -136,6 +132,16 @@ export function Chart({ setLegendItems(chart.current.legend.legendItems); }; + useEffect(() => { + if (data) { + if (!chart.current) { + createChart(data); + } else { + updateChart(data); + } + } + }, [data, options]); + return ( <>
diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index 9655c4a4..594a69f7 100644 --- a/src/components/metrics/EventsChart.tsx +++ b/src/components/metrics/EventsChart.tsx @@ -1,21 +1,23 @@ +import { useMemo, useState, useEffect } from 'react'; import { colord } from 'colord'; import BarChart from '@/components/charts/BarChart'; import { useDateRange, useLocale, useWebsiteEventsSeries } from '@/components/hooks'; import { renderDateLabels } from '@/lib/charts'; import { CHART_COLORS } from '@/lib/constants'; -import { useMemo } from 'react'; export interface EventsChartProps { websiteId: string; className?: string; + focusLabel?: string; } -export function EventsChart({ websiteId, className }: EventsChartProps) { +export function EventsChart({ websiteId, className, focusLabel }: EventsChartProps) { const { dateRange: { startDate, endDate, unit, value }, } = useDateRange(websiteId); const { locale } = useLocale(); const { data, isLoading } = useWebsiteEventsSeries(websiteId); + const [label, setLabel] = useState(focusLabel); const chartData = useMemo(() => { if (!data) return []; @@ -42,8 +44,15 @@ export function EventsChart({ websiteId, className }: EventsChartProps) { borderWidth: 1, }; }), + focusLabel, }; - }, [data, startDate, endDate, unit]); + }, [data, startDate, endDate, unit, focusLabel]); + + useEffect(() => { + if (label !== focusLabel) { + setLabel(focusLabel); + } + }, [focusLabel]); return ( void; +} + +export function EventsTable({ onLabelClick, ...props }: EventsTableProps) { const { formatMessage, labels } = useMessages(); - function handleDataLoad(data: any) { + const handleDataLoad = (data: any) => { props.onDataLoad?.(data); - } + }; + + const renderLabel = ({ x: label }) => { + if (onLabelClick) { + return ( +
onLabelClick(label)} style={{ cursor: 'pointer' }}> + {label} +
+ ); + } + + return label; + }; return ( ); } From d44be466d8cc4de4203dcdcb1180b572379c7327 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Apr 2025 21:48:59 -0700 Subject: [PATCH 4/4] Changed session id generation. --- src/app/api/send/route.ts | 12 ++++++------ src/lib/constants.ts | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index f3c8697e..582b05ff 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -55,7 +55,7 @@ export async function POST(request: Request) { title, tag, timestamp, - id = '', + id, } = payload; // Cache check @@ -101,7 +101,7 @@ export async function POST(request: Request) { const sessionSalt = hash(startOfMonth(createdAt).toUTCString()); const visitSalt = hash(startOfHour(createdAt).toUTCString()); - const sessionId = uuid(websiteId, ip, userAgent, sessionSalt, id); + const sessionId = id ? uuid(websiteId, id) : uuid(websiteId, ip, userAgent, sessionSalt); // Find session if (!clickhouse.enabled && !cache?.sessionId) { @@ -148,6 +148,10 @@ export async function POST(request: Request) { const urlQuery = currentUrl.search.substring(1); const urlDomain = currentUrl.hostname.replace(/^www./, ''); + let referrerPath: string; + let referrerQuery: string; + let referrerDomain: string; + // UTM Params const utmSource = currentUrl.searchParams.get('utm_source'); const utmMedium = currentUrl.searchParams.get('utm_medium'); @@ -167,10 +171,6 @@ export async function POST(request: Request) { urlPath = urlPath.replace(/(.+)\/$/, '$1'); } - let referrerPath: string; - let referrerQuery: string; - let referrerDomain: string; - if (referrer) { const referrerUrl = new URL(referrer, base); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2060811f..7e7e606b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ export const CURRENT_VERSION = process.env.currentVersion; export const AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale';