This commit is contained in:
badrAG 2025-02-24 20:00:01 +00:00
parent bbf91f31db
commit 6a8b804aff
27 changed files with 16152 additions and 2893 deletions

View file

@ -13,13 +13,6 @@
"ecmaVersion": 11, "ecmaVersion": 11,
"sourceType": "module" "sourceType": "module"
}, },
"settings": {
"import/resolver": {
"node": {
"moduleDirectory": ["node_modules", "src/"]
}
}
},
"extends": [ "extends": [
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"eslint:recommended", "eslint:recommended",

29
.github/workflows/cd-cloud.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Create docker images
on:
push:
branches:
- analytics
- cloud
jobs:
build:
name: Build, push, and deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Generate random hash
id: random_hash
run: echo "hash=$(openssl rand -hex 4)" >> $GITHUB_OUTPUT
- uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io
with:
image: umamisoftware/umami
tags: cloud-${{ steps.random_hash.outputs.hash }}, cloud-latest
buildArgs: DATABASE_TYPE=postgresql
registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -20,11 +20,26 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Extract version parts from input
id: extract_version
run: |
echo "version=$(echo ${{ github.event.inputs.version }})" >> $GITHUB_ENV
echo "major=$(echo ${{ github.event.inputs.version }} | cut -d. -f1)" >> $GITHUB_ENV
echo "minor=$(echo ${{ github.event.inputs.version }} | cut -d. -f2)" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_major=$(echo ${{ matrix.db-type }}-${{ env.major }})" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }}-${{ env.major }}.${{ env.minor }})" >> $GITHUB_ENV
echo "tag_patch=$(echo ${{ matrix.db-type }}-${{ env.version }})" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }}-latest)" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6 - uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }} name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with: with:
image: umami image: umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io registry: ghcr.io
multiPlatform: true multiPlatform: true
@ -36,7 +51,7 @@ jobs:
name: Build & push Docker image to docker.io for ${{ matrix.db-type }} name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with: with:
image: umamisoftware/umami image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}

View file

@ -17,14 +17,21 @@ jobs:
- name: Set env - name: Set env
run: | run: |
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_patch=$(echo ${{ matrix.db-type }})-${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1,2)" >> $GITHUB_ENV
echo "tag_major=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1)" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }})-latest" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6 - uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }} name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with: with:
image: umami image: umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io registry: ghcr.io
multiPlatform: true multiPlatform: true
@ -32,12 +39,11 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: mr-smithers-excellent/docker-build-push@v6 - uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to docker.io for ${{ matrix.db-type }} name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with: with:
image: umamisoftware/umami image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}

View file

@ -16,9 +16,9 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- node-version: 18.17 - node-version: 18.18
db-type: postgresql db-type: postgresql
- node-version: 18.17 - node-version: 18.18
db-type: mysql db-type: mysql
steps: steps:

View file

@ -1,5 +1,5 @@
# Install dependencies only when needed # Install dependencies only when needed
FROM node:18-alpine AS deps FROM node:22-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -9,7 +9,7 @@ RUN yarn config set network-timeout 300000
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM node:18-alpine AS builder FROM node:22-alpine AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
@ -26,18 +26,21 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build-docker RUN yarn build-docker
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM node:18-alpine AS runner FROM node:22-alpine AS runner
WORKDIR /app WORKDIR /app
ARG NODE_OPTIONS
ENV NODE_ENV production ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_OPTIONS $NODE_OPTIONS
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
RUN set -x \ RUN set -x \
&& apk add --no-cache curl \ && apk add --no-cache curl \
&& yarn add npm-run-all dotenv prisma semver && yarn add npm-run-all dotenv semver prisma@6.1.0
# You only need to copy next.config.js if you are NOT using the default configuration # You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js . COPY --from=builder /app/next.config.js .

116
README.md
View file

@ -1,69 +1,93 @@
# umami <p align="center">
<img src="https://content.umami.is/website/images/umami-logo.png" alt="Umami Logo" width="100">
</p>
Umami is a simple, fast, privacy-focused alternative to Google Analytics. <h1 align="center">Umami</h1>
## Getting started <p align="center">
<i>Umami is a simple, fast, privacy-focused alternative to Google Analytics.</i>
</p>
A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/) <p align="center">
<a href="https://github.com/umami-software/umami/releases">
<img src="https://img.shields.io/github/release/umami-software/umami.svg" alt="GitHub Release" />
</a>
<a href="https://github.com/umami-software/umami/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/umami-software/umami.svg" alt="MIT License" />
</a>
<a href="https://github.com/umami-software/umami/actions">
<img src="https://img.shields.io/github/actions/workflow/status/umami-software/umami/ci.yml" alt="Build Status" />
</a>
<a href="https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is" style="text-decoration: none;">
<img src="https://img.shields.io/badge/Try%20Demo%20Now-Click%20Here-brightgreen" alt="Umami Demo" />
</a>
</p>
## Installing from source ---
## 🚀 Getting Started
A detailed getting started guide can be found at [umami.is/docs](https://umami.is/docs/).
---
## 🛠 Installing from Source
### Requirements ### Requirements
- A server with Node.js version 16.13 or newer - A server with Node.js version 18.18 or newer
- A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases. - A database. Umami supports [MariaDB](https://www.mariadb.org/) (minimum v10.5), [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
### Install Yarn ### Install Yarn
``` ```bash
npm install -g yarn npm install -g yarn
``` ```
### Get the source code and install packages ### Get the Source Code and Install Packages
``` ```bash
git clone https://github.com/umami-software/umami.git git clone https://github.com/umami-software/umami.git
cd umami cd umami
yarn install yarn install
``` ```
### Configure umami ### Configure Umami
Create an `.env` file with the following Create an `.env` file with the following:
``` ```bash
DATABASE_URL=connection-url DATABASE_URL=connection-url
``` ```
The connection url is in the following format: The connection URL format:
``` ```bash
postgresql://username:mypassword@localhost:5432/mydb postgresql://username:mypassword@localhost:5432/mydb
mysql://username:mypassword@localhost:3306/mydb mysql://username:mypassword@localhost:3306/mydb
``` ```
### Build the application ### Build the Application
```bash ```bash
yarn build yarn build
``` ```
The build step will also 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**. *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**.*
### Start the application ### Start the Application
```bash ```bash
yarn start yarn start
``` ```
By default this will launch the application on `http://localhost:3000`. You will need to either *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.*
[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.
## Installing with Docker ---
To build the umami container and start up a Postgres database, run: ## 🐳 Installing with Docker
To build the Umami container and start up a Postgres database, run:
```bash ```bash
docker compose up -d docker compose up -d
@ -72,16 +96,18 @@ docker compose up -d
Alternatively, to pull just the Umami Docker image with PostgreSQL support: Alternatively, to pull just the Umami Docker image with PostgreSQL support:
```bash ```bash
docker pull ghcr.io/umami-software/umami:postgresql-latest docker pull docker.umami.is/umami-software/umami:postgresql-latest
``` ```
Or with MySQL support: Or with MySQL support:
```bash ```bash
docker pull ghcr.io/umami-software/umami:mysql-latest docker pull docker.umami.is/umami-software/umami:mysql-latest
``` ```
## Getting updates ---
## 🔄 Getting Updates
To get the latest features, simply do a pull, install any new dependencies, and rebuild: To get the latest features, simply do a pull, install any new dependencies, and rebuild:
@ -95,9 +121,39 @@ To update the Docker image, simply pull the new images and rebuild:
```bash ```bash
docker compose pull docker compose pull
docker compose up --force-recreate docker compose up --force-recreate -d
``` ```
## License ---
MIT ## 🛟 Support
<p align="center">
<a href="https://github.com/umami-software/umami">
<img src="https://img.shields.io/badge/GitHub--blue?style=social&logo=github" alt="GitHub" />
</a>
<a href="https://twitter.com/umami_software">
<img src="https://img.shields.io/badge/Twitter--blue?style=social&logo=twitter" alt="Twitter" />
</a>
<a href="https://linkedin.com/company/umami-software">
<img src="https://img.shields.io/badge/LinkedIn--blue?style=social&logo=linkedin" alt="LinkedIn" />
</a>
<a href="https://umami.is/discord">
<img src="https://img.shields.io/badge/Discord--blue?style=social&logo=discord" alt="Discord" />
</a>
</p>
[release-shield]: https://img.shields.io/github/release/umami-software/umami.svg
[releases-url]: https://github.com/umami-software/umami/releases
[license-shield]: https://img.shields.io/github/license/umami-software/umami.svg
[license-url]: https://github.com/umami-software/umami/blob/master/LICENSE
[build-shield]: https://img.shields.io/github/actions/workflow/status/umami-software/umami/ci.yml
[build-url]: https://github.com/umami-software/umami/actions
[github-shield]: https://img.shields.io/badge/GitHub--blue?style=social&logo=github
[github-url]: https://github.com/umami-software/umami
[twitter-shield]: https://img.shields.io/badge/Twitter--blue?style=social&logo=twitter
[twitter-url]: https://twitter.com/umami_software
[linkedin-shield]: https://img.shields.io/badge/LinkedIn--blue?style=social&logo=linkedin
[linkedin-url]: https://linkedin.com/company/umami-software
[discord-shield]: https://img.shields.io/badge/Discord--blue?style=social&logo=discord
[discord-url]: https://discord.com/invite/4dz4zcXYrQ

View file

@ -0,0 +1,57 @@
CREATE TABLE umami.event_data_new
(
website_id UUID,
session_id UUID,
event_id UUID,
url_path String,
event_name String,
data_key String,
string_value Nullable(String),
number_value Nullable(Decimal64(4)),
date_value Nullable(DateTime('UTC')),
data_type UInt32,
created_at DateTime('UTC'),
job_id Nullable(UUID)
)
engine = MergeTree
ORDER BY (website_id, event_id, data_key, created_at)
SETTINGS index_granularity = 8192;
INSERT INTO umami.event_data_new
SELECT website_id,
session_id,
event_id,
url_path,
event_name,
event_key,
string_value,
number_value,
date_value,
data_type,
created_at,
NULL
FROM umami.event_data;
CREATE TABLE umami.session_data
(
website_id UUID,
session_id UUID,
data_key String,
string_value Nullable(String),
number_value Nullable(Decimal64(4)),
date_value Nullable(DateTime('UTC')),
data_type UInt32,
created_at DateTime('UTC'),
job_id Nullable(UUID)
)
engine = MergeTree
ORDER BY (website_id, session_id, data_key, created_at)
SETTINGS index_granularity = 8192;
RENAME TABLE umami.event_data TO umami.event_data_old;
RENAME TABLE umami.event_data_new TO umami.event_data;
/*
DROP TABLE umami.event_data_old
*/

View file

@ -0,0 +1,77 @@
-- add tag column
ALTER TABLE umami.website_event ADD COLUMN "tag" String AFTER "event_name";
ALTER TABLE umami.website_event_stats_hourly ADD COLUMN "tag" SimpleAggregateFunction(groupArrayArray, Array(String)) AFTER "max_time";
-- update materialized view
DROP TABLE umami.website_event_stats_hourly_mv;
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
TO umami.website_event_stats_hourly
AS
SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
referrer_domain,
page_title,
event_type,
event_name,
views,
min_time,
max_time,
tag,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
argMinState(url_path, created_at) entry_url,
argMaxState(url_path, created_at) exit_url,
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type = 1) views,
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
event_type,
timestamp);

View file

@ -26,12 +26,15 @@ CREATE TABLE umami.website_event
--events --events
event_type UInt32, event_type UInt32,
event_name String, event_name String,
tag String,
created_at DateTime('UTC'), created_at DateTime('UTC'),
job_id UUID job_id Nullable(UUID)
) )
engine = MergeTree ENGINE = MergeTree
ORDER BY (website_id, session_id, created_at) PARTITION BY toYYYYMM(created_at)
SETTINGS index_granularity = 8192; ORDER BY (toStartOfHour(created_at), website_id, session_id, visit_id, created_at)
PRIMARY KEY (toStartOfHour(created_at), website_id, session_id, visit_id)
SETTINGS index_granularity = 8192;
CREATE TABLE umami.event_data CREATE TABLE umami.event_data
( (
@ -40,14 +43,156 @@ CREATE TABLE umami.event_data
event_id UUID, event_id UUID,
url_path String, url_path String,
event_name String, event_name String,
event_key String, data_key String,
string_value Nullable(String), string_value Nullable(String),
number_value Nullable(Decimal64(4)), --922337203685477.5625 number_value Nullable(Decimal64(4)),
date_value Nullable(DateTime('UTC')), date_value Nullable(DateTime('UTC')),
data_type UInt32, data_type UInt32,
created_at DateTime('UTC'), created_at DateTime('UTC'),
job_id UUID job_id Nullable(UUID)
) )
engine = MergeTree ENGINE = MergeTree
ORDER BY (website_id, event_id, event_key, created_at) ORDER BY (website_id, event_id, data_key, created_at)
SETTINGS index_granularity = 8192; SETTINGS index_granularity = 8192;
CREATE TABLE umami.session_data
(
website_id UUID,
session_id UUID,
data_key String,
string_value Nullable(String),
number_value Nullable(Decimal64(4)),
date_value Nullable(DateTime('UTC')),
data_type UInt32,
created_at DateTime('UTC'),
job_id Nullable(UUID)
)
ENGINE = ReplacingMergeTree
ORDER BY (website_id, session_id, data_key)
SETTINGS index_granularity = 8192;
-- stats hourly
CREATE TABLE umami.website_event_stats_hourly
(
website_id UUID,
session_id UUID,
visit_id UUID,
hostname LowCardinality(String),
browser LowCardinality(String),
os LowCardinality(String),
device LowCardinality(String),
screen LowCardinality(String),
language LowCardinality(String),
country LowCardinality(String),
subdivision1 LowCardinality(String),
city String,
entry_url AggregateFunction(argMin, String, DateTime('UTC')),
exit_url AggregateFunction(argMax, String, DateTime('UTC')),
url_path SimpleAggregateFunction(groupArrayArray, Array(String)),
url_query SimpleAggregateFunction(groupArrayArray, Array(String)),
referrer_domain SimpleAggregateFunction(groupArrayArray, Array(String)),
page_title SimpleAggregateFunction(groupArrayArray, Array(String)),
event_type UInt32,
event_name SimpleAggregateFunction(groupArrayArray, Array(String)),
views SimpleAggregateFunction(sum, UInt64),
min_time SimpleAggregateFunction(min, DateTime('UTC')),
max_time SimpleAggregateFunction(max, DateTime('UTC')),
tag SimpleAggregateFunction(groupArrayArray, Array(String)),
created_at Datetime('UTC')
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (
website_id,
event_type,
toStartOfHour(created_at),
cityHash64(visit_id),
visit_id
)
SAMPLE BY cityHash64(visit_id);
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
TO umami.website_event_stats_hourly
AS
SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
referrer_domain,
page_title,
event_type,
event_name,
views,
min_time,
max_time,
tag,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
argMinState(url_path, created_at) entry_url,
argMaxState(url_path, created_at) exit_url,
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type = 1) views,
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
subdivision1,
city,
event_type,
timestamp);
-- projections
ALTER TABLE umami.website_event
ADD PROJECTION website_event_url_path_projection (
SELECT * ORDER BY toStartOfDay(created_at), website_id, url_path, created_at
);
ALTER TABLE umami.website_event MATERIALIZE PROJECTION website_event_url_path_projection;
ALTER TABLE umami.website_event
ADD PROJECTION website_event_referrer_domain_projection (
SELECT * ORDER BY toStartOfDay(created_at), website_id, referrer_domain, created_at
);
ALTER TABLE umami.website_event MATERIALIZE PROJECTION website_event_referrer_domain_projection;

View file

@ -11,7 +11,7 @@ CREATE TABLE `user` (
UNIQUE INDEX `user_user_id_key`(`user_id`), UNIQUE INDEX `user_user_id_key`(`user_id`),
UNIQUE INDEX `user_username_key`(`username`), UNIQUE INDEX `user_username_key`(`username`),
PRIMARY KEY (`user_id`) PRIMARY KEY (`user_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `session` ( CREATE TABLE `session` (
@ -33,7 +33,7 @@ CREATE TABLE `session` (
INDEX `session_created_at_idx`(`created_at`), INDEX `session_created_at_idx`(`created_at`),
INDEX `session_website_id_idx`(`website_id`), INDEX `session_website_id_idx`(`website_id`),
PRIMARY KEY (`session_id`) PRIMARY KEY (`session_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `website` ( CREATE TABLE `website` (
@ -53,7 +53,7 @@ CREATE TABLE `website` (
INDEX `website_created_at_idx`(`created_at`), INDEX `website_created_at_idx`(`created_at`),
INDEX `website_share_id_idx`(`share_id`), INDEX `website_share_id_idx`(`share_id`),
PRIMARY KEY (`website_id`) PRIMARY KEY (`website_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `website_event` ( CREATE TABLE `website_event` (
@ -76,7 +76,7 @@ CREATE TABLE `website_event` (
INDEX `website_event_website_id_created_at_idx`(`website_id`, `created_at`), INDEX `website_event_website_id_created_at_idx`(`website_id`, `created_at`),
INDEX `website_event_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`), INDEX `website_event_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
PRIMARY KEY (`event_id`) PRIMARY KEY (`event_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `event_data` ( CREATE TABLE `event_data` (
@ -95,7 +95,7 @@ CREATE TABLE `event_data` (
INDEX `event_data_website_event_id_idx`(`website_event_id`), INDEX `event_data_website_event_id_idx`(`website_event_id`),
INDEX `event_data_website_id_website_event_id_created_at_idx`(`website_id`, `website_event_id`, `created_at`), INDEX `event_data_website_id_website_event_id_created_at_idx`(`website_id`, `website_event_id`, `created_at`),
PRIMARY KEY (`event_id`) PRIMARY KEY (`event_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `team` ( CREATE TABLE `team` (
@ -109,7 +109,7 @@ CREATE TABLE `team` (
UNIQUE INDEX `team_access_code_key`(`access_code`), UNIQUE INDEX `team_access_code_key`(`access_code`),
INDEX `team_access_code_idx`(`access_code`), INDEX `team_access_code_idx`(`access_code`),
PRIMARY KEY (`team_id`) PRIMARY KEY (`team_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `team_user` ( CREATE TABLE `team_user` (
@ -124,7 +124,7 @@ CREATE TABLE `team_user` (
INDEX `team_user_team_id_idx`(`team_id`), INDEX `team_user_team_id_idx`(`team_id`),
INDEX `team_user_user_id_idx`(`user_id`), INDEX `team_user_user_id_idx`(`user_id`),
PRIMARY KEY (`team_user_id`) PRIMARY KEY (`team_user_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `team_website` ( CREATE TABLE `team_website` (
@ -137,7 +137,7 @@ CREATE TABLE `team_website` (
INDEX `team_website_team_id_idx`(`team_id`), INDEX `team_website_team_id_idx`(`team_id`),
INDEX `team_website_website_id_idx`(`website_id`), INDEX `team_website_website_id_idx`(`website_id`),
PRIMARY KEY (`team_website_id`) PRIMARY KEY (`team_website_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddSystemUser -- AddSystemUser
INSERT INTO user (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa'); INSERT INTO user (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa');

View file

@ -21,7 +21,7 @@ CREATE TABLE `session_data` (
INDEX `session_data_website_id_idx`(`website_id`), INDEX `session_data_website_id_idx`(`website_id`),
INDEX `session_data_session_id_idx`(`session_id`), INDEX `session_data_session_id_idx`(`session_id`),
PRIMARY KEY (`session_data_id`) PRIMARY KEY (`session_data_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable -- CreateTable
CREATE TABLE `report` ( CREATE TABLE `report` (
@ -41,7 +41,7 @@ CREATE TABLE `report` (
INDEX `report_type_idx`(`type`), INDEX `report_type_idx`(`type`),
INDEX `report_name_idx`(`name`), INDEX `report_name_idx`(`name`),
PRIMARY KEY (`report_id`) PRIMARY KEY (`report_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- EventData migration -- EventData migration
UPDATE event_data UPDATE event_data

View file

@ -0,0 +1,20 @@
-- DropIndex
DROP INDEX `event_data_website_id_created_at_event_key_idx` ON `event_data`;
-- DropIndex
DROP INDEX `event_data_website_id_website_event_id_created_at_idx` ON `event_data`;
-- AlterTable
ALTER TABLE `event_data` RENAME COLUMN `event_key` TO `data_key`;
-- AlterTable
ALTER TABLE `session_data` RENAME COLUMN `event_key` TO `data_key`;
-- CreateIndex
CREATE INDEX `event_data_website_id_created_at_data_key_idx` ON `event_data`(`website_id`, `created_at`, `data_key`);
-- CreateIndex
CREATE INDEX `session_data_session_id_created_at_idx` ON `session_data`(`session_id`, `created_at`);
-- CreateIndex
CREATE INDEX `session_data_website_id_created_at_data_key_idx` ON `session_data`(`website_id`, `created_at`, `data_key`);

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `website_event` ADD COLUMN `tag` VARCHAR(50) NULL;
-- CreateIndex
CREATE INDEX `website_event_website_id_created_at_tag_idx` ON `website_event`(`website_id`, `created_at`, `tag`);

View file

@ -1,5 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native"]
} }
datasource db { datasource db {
@ -19,10 +20,10 @@ model User {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
websiteUser Website[] @relation("user") websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser") websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[] teamUser TeamUser[]
report Report[] report Report[]
@@map("user") @@map("user")
} }
@ -102,6 +103,7 @@ model WebsiteEvent {
pageTitle String? @map("page_title") @db.VarChar(500) pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.UnsignedInt eventType Int @default(1) @map("event_type") @db.UnsignedInt
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
eventData EventData[] eventData EventData[]
session Session @relation(fields: [sessionId], references: [id]) session Session @relation(fields: [sessionId], references: [id])
@ -116,6 +118,7 @@ model WebsiteEvent {
@@index([websiteId, createdAt, referrerDomain]) @@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle]) @@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName]) @@index([websiteId, createdAt, eventName])
@@index([websiteId, createdAt, tag])
@@index([websiteId, sessionId, createdAt]) @@index([websiteId, sessionId, createdAt])
@@index([websiteId, visitId, createdAt]) @@index([websiteId, visitId, createdAt])
@@map("website_event") @@map("website_event")
@ -125,7 +128,7 @@ model EventData {
id String @id() @map("event_data_id") @db.VarChar(36) id String @id() @map("event_data_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36) websiteEventId String @map("website_event_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0) dateValue DateTime? @map("date_value") @db.Timestamp(0)
@ -138,9 +141,8 @@ model EventData {
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@index([websiteEventId]) @@index([websiteEventId])
@@index([websiteId, websiteEventId, createdAt])
@@index([websiteId, createdAt]) @@index([websiteId, createdAt])
@@index([websiteId, createdAt, eventKey]) @@index([websiteId, createdAt, dataKey])
@@map("event_data") @@map("event_data")
} }
@ -148,7 +150,7 @@ model SessionData {
id String @id() @map("session_data_id") @db.VarChar(36) id String @id() @map("session_data_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36)
sessionId String @map("session_id") @db.VarChar(36) sessionId String @map("session_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0) dateValue DateTime? @map("date_value") @db.Timestamp(0)
@ -161,6 +163,8 @@ model SessionData {
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@index([sessionId]) @@index([sessionId])
@@index([sessionId, createdAt])
@@index([websiteId, createdAt, dataKey])
@@map("session_data") @@map("session_data")
} }
@ -173,8 +177,8 @@ model Team {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0) deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
website Website[] website Website[]
teamUser TeamUser[] teamUser TeamUser[]
@@index([accessCode]) @@index([accessCode])
@@map("team") @@map("team")

View file

@ -0,0 +1,18 @@
-- DropIndex
DROP INDEX IF EXISTS "event_data_website_id_created_at_event_key_idx";
-- AlterTable
ALTER TABLE "event_data" RENAME COLUMN "event_key" TO "data_key";
-- AlterTable
ALTER TABLE "session_data" DROP COLUMN "deleted_at";
ALTER TABLE "session_data" RENAME COLUMN "session_key" TO "data_key";
-- CreateIndex
CREATE INDEX "event_data_website_id_created_at_data_key_idx" ON "event_data"("website_id", "created_at", "data_key");
-- CreateIndex
CREATE INDEX "session_data_session_id_created_at_idx" ON "session_data"("session_id", "created_at");
-- CreateIndex
CREATE INDEX "session_data_website_id_created_at_data_key_idx" ON "session_data"("website_id", "created_at", "data_key");

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "website_event" ADD COLUMN "tag" VARCHAR(50);
-- CreateIndex
CREATE INDEX "website_event_website_id_created_at_tag_idx" ON "website_event"("website_id", "created_at", "tag");

View file

@ -1,5 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native"]
} }
datasource db { datasource db {
@ -19,8 +20,8 @@ model User {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
websiteUser Website[] @relation("user") websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser") websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[] teamUser TeamUser[]
report Report[] report Report[]
@ -102,6 +103,7 @@ model WebsiteEvent {
pageTitle String? @map("page_title") @db.VarChar(500) pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.Integer eventType Int @default(1) @map("event_type") @db.Integer
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
eventData EventData[] eventData EventData[]
session Session @relation(fields: [sessionId], references: [id]) session Session @relation(fields: [sessionId], references: [id])
@ -116,6 +118,7 @@ model WebsiteEvent {
@@index([websiteId, createdAt, referrerDomain]) @@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle]) @@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName]) @@index([websiteId, createdAt, eventName])
@@index([websiteId, createdAt, tag])
@@index([websiteId, sessionId, createdAt]) @@index([websiteId, sessionId, createdAt])
@@index([websiteId, visitId, createdAt]) @@index([websiteId, visitId, createdAt])
@@map("website_event") @@map("website_event")
@ -125,7 +128,7 @@ model EventData {
id String @id() @map("event_data_id") @db.Uuid id String @id() @map("event_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
websiteEventId String @map("website_event_id") @db.Uuid websiteEventId String @map("website_event_id") @db.Uuid
eventKey String @map("event_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6) dateValue DateTime? @map("date_value") @db.Timestamptz(6)
@ -139,7 +142,7 @@ model EventData {
@@index([websiteId]) @@index([websiteId])
@@index([websiteEventId]) @@index([websiteEventId])
@@index([websiteId, createdAt]) @@index([websiteId, createdAt])
@@index([websiteId, createdAt, eventKey]) @@index([websiteId, createdAt, dataKey])
@@map("event_data") @@map("event_data")
} }
@ -147,13 +150,12 @@ model SessionData {
id String @id() @map("session_data_id") @db.Uuid id String @id() @map("session_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid sessionId String @map("session_id") @db.Uuid
sessionKey String @map("session_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6) dateValue DateTime? @map("date_value") @db.Timestamptz(6)
dataType Int @map("data_type") @db.Integer dataType Int @map("data_type") @db.Integer
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
deletedAt DateTime? @default(now()) @map("deleted_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id]) website Website @relation(fields: [websiteId], references: [id])
session Session @relation(fields: [sessionId], references: [id]) session Session @relation(fields: [sessionId], references: [id])
@ -161,6 +163,8 @@ model SessionData {
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@index([sessionId]) @@index([sessionId])
@@index([sessionId, createdAt])
@@index([websiteId, createdAt, dataKey])
@@map("session_data") @@map("session_data")
} }

View file

@ -1,5 +1,4 @@
--- ---
version: '3'
services: services:
umami: umami:
image: ghcr.io/umami-software/umami:postgresql-latest image: ghcr.io/umami-software/umami:postgresql-latest
@ -12,6 +11,7 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
init: true
restart: always restart: always
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"] test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]

3
next-env.d.ts vendored
View file

@ -1,6 +1,5 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information. // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View file

@ -1,29 +1,31 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
require('dotenv').config(); require('dotenv').config();
const path = require('path');
const pkg = require('./package.json'); const pkg = require('./package.json');
const basePath = process.env.BASE_PATH || ''; const TRACKER_SCRIPT = '/script.js';
const forceSSL = process.env.FORCE_SSL || '';
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || ''; const basePath = process.env.BASE_PATH;
const defaultLocale = process.env.DEFAULT_LOCALE || ''; const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT;
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || ''; const cloudMode = process.env.CLOUD_MODE;
const cloudMode = process.env.CLOUD_MODE || ''; const cloudUrl = process.env.CLOUD_URL;
const cloudUrl = process.env.CLOUD_URL || ''; const corsMaxAge = process.env.CORS_MAX_AGE;
const disableLogin = process.env.DISABLE_LOGIN || ''; const defaultLocale = process.env.DEFAULT_LOCALE;
const disableUI = process.env.DISABLE_UI || ''; const disableLogin = process.env.DISABLE_LOGIN;
const hostURL = process.env.HOST_URL || ''; const disableUI = process.env.DISABLE_UI;
const privateMode = process.env.PRIVATE_MODE || ''; const forceSSL = process.env.FORCE_SSL;
const privateMode = process.env.PRIVATE_MODE;
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME;
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL;
const contentSecurityPolicy = [ const contentSecurityPolicy = [
`default-src 'self'`, `default-src 'self'`,
`img-src *`, `img-src * data:`,
`script-src 'self' 'unsafe-eval' 'unsafe-inline'`, `script-src 'self' 'unsafe-eval' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`,
`connect-src 'self' api.umami.is cloud.umami.is` `connect-src 'self' api.umami.is cloud.umami.is`,
]; ];
const headers = [ const defaultHeaders = [
{ {
key: 'X-DNS-Prefetch-Control', key: 'X-DNS-Prefetch-Control',
value: 'on', value: 'on',
@ -38,14 +40,52 @@ const headers = [
]; ];
if (forceSSL) { if (forceSSL) {
headers.push({ defaultHeaders.push({
key: 'Strict-Transport-Security', key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload', value: 'max-age=63072000; includeSubDomains; preload',
}); });
} }
const trackerHeaders = [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Cache-Control',
value: 'public, max-age=86400, must-revalidate',
},
];
const headers = [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ key: 'Access-Control-Allow-Headers', value: '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, DELETE, POST, PUT' },
{ key: 'Access-Control-Max-Age', value: corsMaxAge || '86400' },
],
},
{
source: '/:path*',
headers: defaultHeaders,
},
{
source: TRACKER_SCRIPT,
headers: trackerHeaders,
},
];
const rewrites = []; const rewrites = [];
if (trackerScriptURL) {
rewrites.push({
source: TRACKER_SCRIPT,
destination: trackerScriptURL,
});
}
if (collectApiEndpoint) { if (collectApiEndpoint) {
rewrites.push({ rewrites.push({
source: collectApiEndpoint, source: collectApiEndpoint,
@ -53,19 +93,6 @@ if (collectApiEndpoint) {
}); });
} }
if (trackerScriptName) {
const names = trackerScriptName?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
rewrites.push({
source: `/${name.replace(/^\/+/, '')}`,
destination: '/script.js',
});
});
}
}
const redirects = [ const redirects = [
{ {
source: '/settings', source: '/settings',
@ -84,6 +111,27 @@ const redirects = [
}, },
]; ];
// Adding rewrites + headers for all alternative tracker script names.
if (trackerScriptName) {
const names = trackerScriptName?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
const normalizedSource = `/${name.replace(/^\/+/, '')}`;
rewrites.push({
source: normalizedSource,
destination: TRACKER_SCRIPT,
});
headers.push({
source: normalizedSource,
headers: trackerHeaders,
});
});
}
}
if (cloudMode && cloudUrl) { if (cloudMode && cloudUrl) {
redirects.push({ redirects.push({
source: '/settings/:path*', source: '/settings/:path*',
@ -118,7 +166,6 @@ const config = {
defaultLocale, defaultLocale,
disableLogin, disableLogin,
disableUI, disableUI,
hostURL,
privateMode, privateMode,
}, },
basePath, basePath,
@ -129,36 +176,26 @@ const config = {
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
webpack(config) { webpack(config) {
const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.('.svg')); config.module.rules.push({
test: /\.svg$/,
config.module.rules.push( issuer: /\.(js|ts)x?$/,
{ use: ['@svgr/webpack'],
...fileLoaderRule, });
test: /\.svg$/i,
resourceQuery: /url/,
},
{
test: /\.svg$/i,
issuer: fileLoaderRule.issuer,
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] },
use: ['@svgr/webpack'],
},
);
fileLoaderRule.exclude = /\.svg$/i;
config.resolve.alias['public'] = path.resolve('./public');
return config; return config;
}, },
async headers() { async headers() {
return [ return headers;
{
source: '/:path*',
headers,
},
];
}, },
async rewrites() { async rewrites() {
return [ return [
@ -167,6 +204,10 @@ const config = {
source: '/telemetry.js', source: '/telemetry.js',
destination: '/api/scripts/telemetry', destination: '/api/scripts/telemetry',
}, },
{
source: '/teams/:teamId/:path((?!settings).*)*',
destination: '/:path*',
},
]; ];
}, },
async redirects() { async redirects() {

View file

@ -11,6 +11,7 @@
"@tanstack/react-query": "^4.33.0", "@tanstack/react-query": "^4.33.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"colord": "^2.9.2", "colord": "^2.9.2",
"date-fns-tz": "^1.1.4",
"immer": "^9.0.12", "immer": "^9.0.12",
"moment-timezone": "^0.5.35", "moment-timezone": "^0.5.35",
"next": "^13.4.0", "next": "^13.4.0",

View file

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "2.11.0", "version": "2.16.1",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>", "author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT", "license": "MIT",
@ -10,7 +10,7 @@
"url": "https://github.com/umami-software/umami.git" "url": "https://github.com/umami-software/umami.git"
}, },
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start", "start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app", "build-docker": "npm-run-all build-db build-tracker build-geo build-app",
@ -64,14 +64,19 @@
".next/cache" ".next/cache"
], ],
"dependencies": { "dependencies": {
"@clickhouse/client": "^0.2.2", "@clickhouse/client": "^1.10.1",
"@date-fns/utc": "^1.2.0",
"@dicebear/collection": "^9.2.1",
"@dicebear/core": "^9.2.1",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
"@prisma/client": "5.11.0", "@hello-pangea/dnd": "^17.0.0",
"@prisma/extension-read-replicas": "^0.3.0", "@prisma/client": "6.1.0",
"@prisma/extension-read-replicas": "^0.4.0",
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.28.6", "@tanstack/react-query": "^5.28.6",
"@umami/prisma-client": "^0.14.0", "@umami/prisma-client": "^0.14.0",
"@umami/redis-client": "^0.18.0", "@umami/redis-client": "^0.26.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"chart.js": "^4.4.2", "chart.js": "^4.4.2",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
@ -81,7 +86,6 @@
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"date-fns-tz": "^1.1.4", "date-fns-tz": "^1.1.4",
"dateformat": "^5.0.3",
"debug": "^4.3.4", "debug": "^4.3.4",
"del": "^6.0.0", "del": "^6.0.0",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
@ -93,20 +97,19 @@
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"is-docker": "^3.0.0", "is-docker": "^3.0.0",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",
"isbot": "^5.1.1", "isbot": "^5.1.16",
"jsonwebtoken": "^9.0.2",
"kafkajs": "^2.1.0", "kafkajs": "^2.1.0",
"maxmind": "^4.3.6", "maxmind": "^4.3.24",
"md5": "^2.3.0", "md5": "^2.3.0",
"moment-timezone": "^0.5.35", "next": "15.0.4",
"next": "14.1.4",
"next-basics": "^0.39.0",
"node-fetch": "^3.2.8", "node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prisma": "5.11.0", "prisma": "6.1.0",
"react": "^18.2.0", "pure-rand": "^6.1.0",
"react-basics": "^0.123.0", "react": "^19.0.0",
"react-beautiful-dnd": "^13.1.0", "react-basics": "^0.126.0",
"react-dom": "^18.2.0", "react-dom": "^19.0.0",
"react-error-boundary": "^4.0.4", "react-error-boundary": "^4.0.4",
"react-intl": "^6.5.5", "react-intl": "^6.5.5",
"react-simple-maps": "^2.3.0", "react-simple-maps": "^2.3.0",
@ -114,14 +117,15 @@
"react-window": "^1.8.6", "react-window": "^1.8.6",
"request-ip": "^3.3.0", "request-ip": "^3.3.0",
"semver": "^7.5.4", "semver": "^7.5.4",
"serialize-error": "^12.0.0",
"thenby": "^1.3.4", "thenby": "^1.3.4",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"yup": "^0.32.11", "zod": "^3.24.1",
"zustand": "^4.3.8" "zustand": "^4.5.5"
}, },
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^4.2.29", "@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^4.41.3", "@netlify/plugin-nextjs": "^5.8.1",
"@rollup/plugin-alias": "^5.0.0", "@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
@ -130,16 +134,17 @@
"@svgr/rollup": "^8.1.0", "@svgr/rollup": "^8.1.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/cypress": "^1.1.3", "@types/cypress": "^1.1.3",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.14",
"@types/node": "^20.9.0", "@types/node": "^22.13.4",
"@types/react": "^18.2.41", "@types/react": "^19.0.8",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^19.0.2",
"@types/react-intl": "^3.0.0",
"@types/react-window": "^1.8.8", "@types/react-window": "^1.8.8",
"@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3", "@typescript-eslint/parser": "^6.7.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cypress": "^13.6.6", "cypress": "^13.6.6",
"esbuild": "^0.17.17", "esbuild": "^0.25.0",
"eslint": "^8.33.0", "eslint": "^8.33.0",
"eslint-config-next": "^14.0.4", "eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
@ -175,6 +180,6 @@
"tar": "^6.1.2", "tar": "^6.1.2",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.4.3" "typescript": "^5.5.3"
} }
} }

File diff suppressed because one or more lines are too long

View file

@ -19,13 +19,8 @@ const customResolver = resolve({
const aliasConfig = { const aliasConfig = {
entries: [ entries: [
{ find: /^app/, replacement: path.resolve('./src/app') }, { find: /^@/, replacement: path.resolve('./src/') },
{ find: /^components/, replacement: path.resolve('./src/components') },
{ find: /^hooks/, replacement: path.resolve('./src/hooks') },
{ find: /^lib/, replacement: path.resolve('./src/lib') },
{ find: /^store/, replacement: path.resolve('./src/store') },
{ find: /^public/, replacement: path.resolve('./public') }, { find: /^public/, replacement: path.resolve('./public') },
{ find: /^assets/, replacement: path.resolve('./src/assets') },
], ],
customResolver, customResolver,
}; };

View file

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2021", "target": "es2022",
"outDir": "./build", "outDir": "./build",
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
@ -21,16 +21,10 @@
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": false, "incremental": false,
"baseUrl": "./src", "types": ["jest"],
"typeRoots": ["node_modules/@types"],
"paths": { "paths": {
"react": ["./node_modules/@types/react"], "@/*": ["./src/*"]
"assets/*": ["./assets/*"],
"components/*": ["./components/*"],
"lib/*": ["./lib/*"],
"pages/*": ["./pages/*"],
"queries/*": ["./queries/*"],
"store/*": ["./store/*"],
"styles/*": ["./styles/*"]
}, },
"plugins": [ "plugins": [
{ {

4826
yarn.lock

File diff suppressed because it is too large Load diff