Merge branch 'dev' into analytics

This commit is contained in:
Mike Cao 2025-04-30 10:09:05 -07:00
commit 54dbb43b5c
29 changed files with 810 additions and 11176 deletions

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ node_modules
/public/script.js
/geo
/dist
src/generated/prisma/
# misc
.DS_Store

View file

@ -38,18 +38,12 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.i
- A server with Node.js version 18.18 or newer
- 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
```bash
npm install -g yarn
```
### Get the Source Code and Install Packages
```bash
git clone https://github.com/umami-software/umami.git
cd umami
yarn install
npm install
```
### Configure Umami
@ -70,7 +64,7 @@ mysql://username:mypassword@localhost:3306/mydb
### Build the Application
```bash
yarn build
npm 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**.*
@ -78,7 +72,7 @@ yarn build
### Start the Application
```bash
yarn start
npm 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.*
@ -113,8 +107,8 @@ To get the latest features, simply do a pull, install any new dependencies, and
```bash
git pull
yarn install
yarn build
npm install
npm run build
```
To update the Docker image, simply pull the new images and rebuild:

View file

@ -0,0 +1,103 @@
-- add tag column
ALTER TABLE umami.website_event ADD COLUMN "distinct_id" String AFTER "tag";
ALTER TABLE umami.website_event_stats_hourly ADD COLUMN "distinct_id" String AFTER "tag";
ALTER TABLE umami.session_data ADD COLUMN "distinct_id" String AFTER "data_type";
-- 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,
region,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
utm_source,
utm_medium,
utm_campaign,
utm_content,
utm_term,
referrer_domain,
page_title,
gclid,
fbclid,
msclkid,
ttclid,
li_fat_id,
twclid,
event_type,
event_name,
views,
min_time,
max_time,
tag,
distinct_id,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
region,
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(utm_source)) utm_source,
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
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,
distinct_id,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
region,
city,
event_type,
distinct_id,
timestamp);

View file

@ -38,6 +38,7 @@ CREATE TABLE umami.website_event
event_type UInt32,
event_name String,
tag String,
distinct_id String,
created_at DateTime('UTC'),
job_id Nullable(UUID)
)
@ -75,6 +76,7 @@ CREATE TABLE umami.session_data
number_value Nullable(Decimal64(4)),
date_value Nullable(DateTime('UTC')),
data_type UInt32,
distinct_id String,
created_at DateTime('UTC'),
job_id Nullable(UUID)
)
@ -120,6 +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,
created_at Datetime('UTC')
)
ENGINE = AggregatingMergeTree
@ -172,6 +175,7 @@ SELECT
min_time,
max_time,
tag,
distinct_id,
timestamp as created_at
FROM (SELECT
website_id,
@ -209,6 +213,7 @@ FROM (SELECT
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
distinct_id String,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
@ -224,6 +229,7 @@ GROUP BY website_id,
region,
city,
event_type,
distinct_id,
timestamp);
-- projections

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE `session` ADD COLUMN `distinct_id` VARCHAR(50) NULL;
-- AlterTable
ALTER TABLE `session_data` ADD COLUMN `distinct_id` VARCHAR(50) NULL;

View file

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "mysql"
provider = "mysql"

View file

@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
binaryTargets = ["native"]
}
@ -29,17 +30,18 @@ model User {
}
model Session {
id String @id @unique @map("session_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
device String? @db.VarChar(20)
screen String? @db.VarChar(11)
language String? @db.VarChar(35)
country String? @db.Char(2)
region String? @db.Char(20)
city String? @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
id String @id @unique @map("session_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36)
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
device String? @db.VarChar(20)
screen String? @db.VarChar(11)
language String? @db.VarChar(35)
country String? @db.Char(2)
region String? @db.Char(20)
city String? @db.VarChar(50)
distinctId String? @map("distinct_id") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
websiteEvent WebsiteEvent[]
sessionData SessionData[]
@ -165,6 +167,7 @@ model SessionData {
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0)
dataType Int @map("data_type") @db.UnsignedInt
distinctId String? @map("distinct_id") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
website Website @relation(fields: [websiteId], references: [id])

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "session" ADD COLUMN "distinct_id" VARCHAR(50);
-- AlterTable
ALTER TABLE "session_data" ADD COLUMN "distinct_id" VARCHAR(50);

View file

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
provider = "postgresql"

View file

@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
binaryTargets = ["native"]
}
@ -29,17 +30,18 @@ model User {
}
model Session {
id String @id @unique @map("session_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
device String? @db.VarChar(20)
screen String? @db.VarChar(11)
language String? @db.VarChar(35)
country String? @db.Char(2)
region String? @db.VarChar(20)
city String? @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
id String @id @unique @map("session_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
device String? @db.VarChar(20)
screen String? @db.VarChar(11)
language String? @db.VarChar(35)
country String? @db.Char(2)
region String? @db.VarChar(20)
city String? @db.VarChar(50)
distinctId String? @map("distinct_id") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
websiteEvent WebsiteEvent[]
sessionData SessionData[]
@ -165,6 +167,7 @@ model SessionData {
numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6)
dataType Int @map("data_type") @db.Integer
distinctId String? @map("distinct_id") @db.VarChar(50)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id])

View file

@ -1,8 +1,6 @@
import dotenv from 'dotenv';
import 'dotenv/config';
import { createRequire } from 'module';
dotenv.config();
const require = createRequire(import.meta.url);
const pkg = require('./package.json');

View file

@ -72,11 +72,10 @@
"@dicebear/core": "^9.2.1",
"@fontsource/inter": "^4.5.15",
"@hello-pangea/dnd": "^17.0.0",
"@prisma/client": "6.1.0",
"@prisma/extension-read-replicas": "^0.4.0",
"@prisma/client": "6.6.0",
"@prisma/extension-read-replicas": "^0.4.1",
"@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.28.6",
"@umami/prisma-client": "^0.14.0",
"@umami/redis-client": "^0.26.0",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
@ -107,7 +106,7 @@
"next": "15.3.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"prisma": "6.1.0",
"prisma": "6.6.0",
"pure-rand": "^6.1.0",
"react": "^19.0.0",
"react-basics": "^0.126.0",

518
pnpm-lock.yaml generated
View file

@ -27,20 +27,17 @@ importers:
specifier: ^17.0.0
version: 17.0.0(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@prisma/client':
specifier: 6.1.0
version: 6.1.0(prisma@6.1.0)
specifier: 6.6.0
version: 6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)
'@prisma/extension-read-replicas':
specifier: ^0.4.0
version: 0.4.1(@prisma/client@6.1.0(prisma@6.1.0))
specifier: ^0.4.1
version: 0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))
'@react-spring/web':
specifier: ^9.7.3
version: 9.7.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-query':
specifier: ^5.28.6
version: 5.74.3(react@19.1.0)
'@umami/prisma-client':
specifier: ^0.14.0
version: 0.14.0(@prisma/client@6.1.0(prisma@6.1.0))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.1.0(prisma@6.1.0)))
'@umami/redis-client':
specifier: ^0.26.0
version: 0.26.0
@ -132,8 +129,8 @@ importers:
specifier: ^4.1.5
version: 4.1.5
prisma:
specifier: 6.1.0
version: 6.1.0
specifier: 6.6.0
version: 6.6.0(typescript@5.8.3)
pure-rand:
specifier: ^6.1.0
version: 6.1.0
@ -355,6 +352,47 @@ importers:
specifier: ^5.5.3
version: 5.8.3
dist:
dependencies:
'@tanstack/react-query':
specifier: ^4.33.0
version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classnames:
specifier: ^2.3.1
version: 2.5.1
colord:
specifier: ^2.9.2
version: 2.9.3
date-fns-tz:
specifier: ^1.1.4
version: 1.3.8(date-fns@2.30.0)
immer:
specifier: ^9.0.12
version: 9.0.21
moment-timezone:
specifier: ^0.5.35
version: 0.5.48
next:
specifier: ^13.4.0
version: 13.5.11(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
next-basics:
specifier: ^0.36.0
version: 0.36.0(next@13.5.11(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react:
specifier: ^18.2.0
version: 18.3.1
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
react-intl:
specifier: ^5.24.7
version: 5.25.1(react@18.3.1)(typescript@4.9.5)
zustand:
specifier: ^4.3.8
version: 4.5.6(@types/react@19.1.2)(immer@9.0.21)(react@18.3.1)
src/generated/prisma: {}
packages:
'@ampproject/remapping@2.3.0':
@ -1509,6 +1547,9 @@ packages:
'@formatjs/ecma402-abstract@2.2.4':
resolution: {integrity: sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==}
'@formatjs/fast-memoize@1.2.1':
resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
'@formatjs/fast-memoize@2.2.3':
resolution: {integrity: sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==}
@ -1524,9 +1565,15 @@ packages:
'@formatjs/icu-skeleton-parser@1.8.8':
resolution: {integrity: sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==}
'@formatjs/intl-displaynames@5.4.3':
resolution: {integrity: sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==}
'@formatjs/intl-displaynames@6.8.5':
resolution: {integrity: sha512-85b+GdAKCsleS6cqVxf/Aw/uBd+20EM0wDpgaxzHo3RIR3bxF4xCJqH/Grbzx8CXurTgDDZHPdPdwJC+May41w==}
'@formatjs/intl-listformat@6.5.3':
resolution: {integrity: sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==}
'@formatjs/intl-listformat@7.7.5':
resolution: {integrity: sha512-Wzes10SMNeYgnxYiKsda4rnHP3Q3II4XT2tZyOgnH5fWuHDtIkceuWlRQNsvrI3uiwP4hLqp2XdQTCsfkhXulg==}
@ -1547,6 +1594,14 @@ packages:
typescript:
optional: true
'@formatjs/intl@2.2.1':
resolution: {integrity: sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==}
peerDependencies:
typescript: ^4.5
peerDependenciesMeta:
typescript:
optional: true
'@formatjs/ts-transformer@2.13.0':
resolution: {integrity: sha512-mu7sHXZk1NWZrQ3eUqugpSYo8x5/tXkrI4uIbFqCEC0eNgQaIcoKgVeDFgDAcgG+cEme2atAUYSFF+DFWC4org==}
peerDependencies:
@ -1804,54 +1859,111 @@ packages:
resolution: {integrity: sha512-j/Jt/MMFy70f1LIa6qkviNWVoEIoF9ZICdDC3TF/IvStxWHGAYHm8pkqqbi1VqzjUEDUNu1ZGjo+VElZyenRBg==}
engines: {node: '>=18.0.0'}
'@next/env@13.5.11':
resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==}
'@next/env@15.3.0':
resolution: {integrity: sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==}
'@next/eslint-plugin-next@14.2.28':
resolution: {integrity: sha512-GQUPA1bTZy5qZdPV5MOHB18465azzhg8xm5o2SqxMF+h1rWNjB43y6xmIPHG5OV2OiU3WxuINpusXom49DdaIQ==}
'@next/swc-darwin-arm64@13.5.9':
resolution: {integrity: sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-arm64@15.3.0':
resolution: {integrity: sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@13.5.9':
resolution: {integrity: sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-darwin-x64@15.3.0':
resolution: {integrity: sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@13.5.9':
resolution: {integrity: sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-gnu@15.3.0':
resolution: {integrity: sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@13.5.9':
resolution: {integrity: sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.3.0':
resolution: {integrity: sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@13.5.9':
resolution: {integrity: sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-gnu@15.3.0':
resolution: {integrity: sha512-ZMQ9yzDEts/vkpFLRAqfYO1wSpIJGlQNK9gZ09PgyjBJUmg8F/bb8fw2EXKgEaHbCc4gmqMpDfh+T07qUphp9A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@13.5.9':
resolution: {integrity: sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.3.0':
resolution: {integrity: sha512-RFwq5VKYTw9TMr4T3e5HRP6T4RiAzfDJ6XsxH8j/ZeYq2aLsBqCkFzwMI0FmnSsLaUbOb46Uov0VvN3UciHX5A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@13.5.9':
resolution: {integrity: sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-arm64-msvc@15.3.0':
resolution: {integrity: sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-ia32-msvc@13.5.9':
resolution: {integrity: sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@next/swc-win32-x64-msvc@13.5.9':
resolution: {integrity: sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@next/swc-win32-x64-msvc@15.3.0':
resolution: {integrity: sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==}
engines: {node: '>= 10'}
@ -1878,34 +1990,40 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@prisma/client@6.1.0':
resolution: {integrity: sha512-AbQYc5+EJKm1Ydfq3KxwcGiy7wIbm4/QbjCKWWoNROtvy7d6a3gmAGkKjK0iUCzh+rHV8xDhD5Cge8ke/kiy5Q==}
'@prisma/client@6.6.0':
resolution: {integrity: sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==}
engines: {node: '>=18.18'}
peerDependencies:
prisma: '*'
typescript: '>=5.1.0'
peerDependenciesMeta:
prisma:
optional: true
typescript:
optional: true
'@prisma/debug@6.1.0':
resolution: {integrity: sha512-0himsvcM4DGBTtvXkd2Tggv6sl2JyUYLzEGXXleFY+7Kp6rZeSS3hiTW9mwtUlXrwYbJP6pwlVNB7jYElrjWUg==}
'@prisma/config@6.6.0':
resolution: {integrity: sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==}
'@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959':
resolution: {integrity: sha512-PdJqmYM2Fd8K0weOOtQThWylwjsDlTig+8Pcg47/jszMuLL9iLIaygC3cjWJLda69siRW4STlCTMSgOjZzvKPQ==}
'@prisma/debug@6.6.0':
resolution: {integrity: sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==}
'@prisma/engines@6.1.0':
resolution: {integrity: sha512-GnYJbCiep3Vyr1P/415ReYrgJUjP79fBNc1wCo7NP6Eia0CzL2Ot9vK7Infczv3oK7JLrCcawOSAxFxNFsAERQ==}
'@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a':
resolution: {integrity: sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==}
'@prisma/engines@6.6.0':
resolution: {integrity: sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==}
'@prisma/extension-read-replicas@0.4.1':
resolution: {integrity: sha512-mCMDloqUKUwx2o5uedTs1FHX3Nxdt1GdRMoeyp1JggjiwOALmIYWhxfIN08M2BZ0w8SKwvJqicJZMjkQYkkijw==}
peerDependencies:
'@prisma/client': ^6.5.0
'@prisma/fetch-engine@6.1.0':
resolution: {integrity: sha512-asdFi7TvPlEZ8CzSZ/+Du5wZ27q6OJbRSXh+S8ISZguu+S9KtS/gP7NeXceZyb1Jv1SM1S5YfiCv+STDsG6rrg==}
'@prisma/fetch-engine@6.6.0':
resolution: {integrity: sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==}
'@prisma/get-platform@6.1.0':
resolution: {integrity: sha512-ia8bNjboBoHkmKGGaWtqtlgQOhCi7+f85aOkPJKgNwWvYrT6l78KgojLekE8zMhVk0R9lWcifV0Pf8l3/15V0Q==}
'@prisma/get-platform@6.6.0':
resolution: {integrity: sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==}
'@react-spring/animated@9.7.5':
resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==}
@ -2120,9 +2238,27 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@swc/helpers@0.5.2':
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
'@tanstack/query-core@4.36.1':
resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==}
'@tanstack/query-core@5.74.3':
resolution: {integrity: sha512-Mqk+5o3qTuAiZML248XpNH8r2cOzl15+LTbUsZQEwvSvn1GU4VQhvqzAbil36p+MBxpr/58oBSnRzhrBevDhfg==}
'@tanstack/react-query@4.36.1':
resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
'@tanstack/react-query@5.74.3':
resolution: {integrity: sha512-QrycUn0wxjVPzITvQvOxFRdhlAwIoOQSuav7qWD4SWCoKCdLbyRZ2vji2GuBq/glaxbF4wBx3fqcYRDOt8KDTA==}
peerDependencies:
@ -2358,12 +2494,6 @@ packages:
resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
engines: {node: ^16.0.0 || >=18.0.0}
'@umami/prisma-client@0.14.0':
resolution: {integrity: sha512-62aAE5s5m/M1T88JIvOyZk2LzQIJ/iEeF57ewd/qKKNZdgXfP1aib+IAQ74db85vq6TwFyHMaXBeXztNNby5Fw==}
peerDependencies:
'@prisma/client': ^4.8.0
'@prisma/extension-read-replicas': ^0.3.0
'@umami/redis-client@0.26.0':
resolution: {integrity: sha512-j2vxb1gYF5zfk7BkrHgau2MwKsB5ijbQh2w1WoIvbP41cqTMsFm/zUrjhZ0cP1ZxR/riQR1AWxKmqNggYRZ5eA==}
@ -3412,6 +3542,11 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
esbuild-register@3.6.0:
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
peerDependencies:
esbuild: '>=0.12 <1'
esbuild@0.25.2:
resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
engines: {node: '>=18'}
@ -3857,6 +3992,9 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
glob@10.3.10:
resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
engines: {node: '>=16 || 14 >=14.17'}
@ -4078,6 +4216,9 @@ packages:
intl-messageformat@10.7.7:
resolution: {integrity: sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==}
intl-messageformat@9.13.0:
resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
ipaddr.js@2.2.0:
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
engines: {node: '>= 10'}
@ -4868,6 +5009,12 @@ packages:
resolution: {integrity: sha512-yx8H/1H5AfnufiLnzzPqPf4yr/dKU9IFT1rPVwSkrKWHsQEeVVd6+X+L0nUbXhlEFTu3y/7hu38CFmEVgzvyeg==}
engines: {node: '>=10', npm: '>=6'}
moment-timezone@0.5.48:
resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@ -4882,6 +5029,28 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-basics@0.36.0:
resolution: {integrity: sha512-Nwou8pCjFuoD/ZxUw9iKC7hhZeWbo/ng0ze74yck3W89MNc/CepwCDziflAHY5XcmIVNmpXOCu9OfmzTdVRPWQ==}
peerDependencies:
next: ^13.4.0
react: ^18.2.0
react-dom: ^18.2.0
next@13.5.11:
resolution: {integrity: sha512-WUPJ6WbAX9tdC86kGTu92qkrRdgRqVrY++nwM+shmWQwmyxt4zhZfR59moXSI4N8GDYCBY3lIAqhzjDd4rTC8Q==}
engines: {node: '>=16.14.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
sass:
optional: true
next@15.3.0:
resolution: {integrity: sha512-k0MgP6BsK8cZ73wRjMazl2y2UcXj49ZXLDEgx6BikWuby/CN+nh81qFFI16edgd7xYpe/jj2OZEIwCoqnzz0bQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
@ -5587,10 +5756,15 @@ packages:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
prisma@6.1.0:
resolution: {integrity: sha512-aFI3Yi+ApUxkwCJJwyQSwpyzUX7YX3ihzuHNHOyv4GJg3X5tQsmRaJEnZ+ZyfHpMtnyahhmXVfbTZ+lS8ZtfKw==}
prisma@6.6.0:
resolution: {integrity: sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==}
engines: {node: '>=18.18'}
hasBin: true
peerDependencies:
typescript: '>=5.1.0'
peerDependenciesMeta:
typescript:
optional: true
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
@ -5648,6 +5822,11 @@ packages:
react: ^18.2.0
react-dom: ^18.2.0
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies:
@ -5664,6 +5843,15 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-intl@5.25.1:
resolution: {integrity: sha512-pkjdQDvpJROoXLMltkP/5mZb0/XqrqLoPGKUCfbdkP8m6U9xbK40K51Wu+a4aQqTEvEK5lHBk0fWzUV72SJ3Hg==}
peerDependencies:
react: ^16.3.0 || 17 || 18
typescript: ^4.5
peerDependenciesMeta:
typescript:
optional: true
react-intl@6.8.9:
resolution: {integrity: sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==}
peerDependencies:
@ -5714,6 +5902,10 @@ packages:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
@ -5935,6 +6127,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
@ -6187,6 +6382,19 @@ packages:
style-search@0.1.0:
resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==}
styled-jsx@5.1.1:
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
babel-plugin-macros:
optional: true
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@ -6576,6 +6784,10 @@ packages:
walker@1.0.8:
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
watchpack@2.4.0:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@ -7953,6 +8165,10 @@ snapshots:
'@formatjs/intl-localematcher': 0.5.8
tslib: 2.8.1
'@formatjs/fast-memoize@1.2.1':
dependencies:
tslib: 2.8.1
'@formatjs/fast-memoize@2.2.3':
dependencies:
tslib: 2.8.1
@ -7979,12 +8195,24 @@ snapshots:
'@formatjs/ecma402-abstract': 2.2.4
tslib: 2.8.1
'@formatjs/intl-displaynames@5.4.3':
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/intl-localematcher': 0.2.25
tslib: 2.8.1
'@formatjs/intl-displaynames@6.8.5':
dependencies:
'@formatjs/ecma402-abstract': 2.2.4
'@formatjs/intl-localematcher': 0.5.8
tslib: 2.8.1
'@formatjs/intl-listformat@6.5.3':
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/intl-localematcher': 0.2.25
tslib: 2.8.1
'@formatjs/intl-listformat@7.7.5':
dependencies:
'@formatjs/ecma402-abstract': 2.2.4
@ -8016,6 +8244,18 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
'@formatjs/intl@2.2.1(typescript@4.9.5)':
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/fast-memoize': 1.2.1
'@formatjs/icu-messageformat-parser': 2.1.0
'@formatjs/intl-displaynames': 5.4.3
'@formatjs/intl-listformat': 6.5.3
intl-messageformat: 9.13.0
tslib: 2.8.1
optionalDependencies:
typescript: 4.9.5
'@formatjs/ts-transformer@2.13.0(ts-jest@29.3.2(@babel/core@7.26.10)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.10))(esbuild@0.25.2)(jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3)))(typescript@5.8.3))':
dependencies:
intl-messageformat-parser: 6.1.2
@ -8357,33 +8597,62 @@ snapshots:
'@netlify/plugin-nextjs@5.10.6': {}
'@next/env@13.5.11': {}
'@next/env@15.3.0': {}
'@next/eslint-plugin-next@14.2.28':
dependencies:
glob: 10.3.10
'@next/swc-darwin-arm64@13.5.9':
optional: true
'@next/swc-darwin-arm64@15.3.0':
optional: true
'@next/swc-darwin-x64@13.5.9':
optional: true
'@next/swc-darwin-x64@15.3.0':
optional: true
'@next/swc-linux-arm64-gnu@13.5.9':
optional: true
'@next/swc-linux-arm64-gnu@15.3.0':
optional: true
'@next/swc-linux-arm64-musl@13.5.9':
optional: true
'@next/swc-linux-arm64-musl@15.3.0':
optional: true
'@next/swc-linux-x64-gnu@13.5.9':
optional: true
'@next/swc-linux-x64-gnu@15.3.0':
optional: true
'@next/swc-linux-x64-musl@13.5.9':
optional: true
'@next/swc-linux-x64-musl@15.3.0':
optional: true
'@next/swc-win32-arm64-msvc@13.5.9':
optional: true
'@next/swc-win32-arm64-msvc@15.3.0':
optional: true
'@next/swc-win32-ia32-msvc@13.5.9':
optional: true
'@next/swc-win32-x64-msvc@13.5.9':
optional: true
'@next/swc-win32-x64-msvc@15.3.0':
optional: true
@ -8404,34 +8673,42 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@prisma/client@6.1.0(prisma@6.1.0)':
'@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)':
optionalDependencies:
prisma: 6.1.0
prisma: 6.6.0(typescript@5.8.3)
typescript: 5.8.3
'@prisma/debug@6.1.0': {}
'@prisma/engines-version@6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959': {}
'@prisma/engines@6.1.0':
'@prisma/config@6.6.0':
dependencies:
'@prisma/debug': 6.1.0
'@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959
'@prisma/fetch-engine': 6.1.0
'@prisma/get-platform': 6.1.0
esbuild: 0.25.2
esbuild-register: 3.6.0(esbuild@0.25.2)
transitivePeerDependencies:
- supports-color
'@prisma/extension-read-replicas@0.4.1(@prisma/client@6.1.0(prisma@6.1.0))':
dependencies:
'@prisma/client': 6.1.0(prisma@6.1.0)
'@prisma/debug@6.6.0': {}
'@prisma/fetch-engine@6.1.0':
dependencies:
'@prisma/debug': 6.1.0
'@prisma/engines-version': 6.1.0-21.11f085a2012c0f4778414c8db2651556ee0ef959
'@prisma/get-platform': 6.1.0
'@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': {}
'@prisma/get-platform@6.1.0':
'@prisma/engines@6.6.0':
dependencies:
'@prisma/debug': 6.1.0
'@prisma/debug': 6.6.0
'@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
'@prisma/fetch-engine': 6.6.0
'@prisma/get-platform': 6.6.0
'@prisma/extension-read-replicas@0.4.1(@prisma/client@6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3))':
dependencies:
'@prisma/client': 6.6.0(prisma@6.6.0(typescript@5.8.3))(typescript@5.8.3)
'@prisma/fetch-engine@6.6.0':
dependencies:
'@prisma/debug': 6.6.0
'@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
'@prisma/get-platform': 6.6.0
'@prisma/get-platform@6.6.0':
dependencies:
'@prisma/debug': 6.6.0
'@react-spring/animated@9.7.5(react@19.1.0)':
dependencies:
@ -8666,8 +8943,22 @@ snapshots:
dependencies:
tslib: 2.8.1
'@swc/helpers@0.5.2':
dependencies:
tslib: 2.8.1
'@tanstack/query-core@4.36.1': {}
'@tanstack/query-core@5.74.3': {}
'@tanstack/react-query@4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/query-core': 4.36.1
react: 18.3.1
use-sync-external-store: 1.5.0(react@18.3.1)
optionalDependencies:
react-dom: 18.3.1(react@18.3.1)
'@tanstack/react-query@5.74.3(react@19.1.0)':
dependencies:
'@tanstack/query-core': 5.74.3
@ -8954,15 +9245,6 @@ snapshots:
'@typescript-eslint/types': 6.21.0
eslint-visitor-keys: 3.4.3
'@umami/prisma-client@0.14.0(@prisma/client@6.1.0(prisma@6.1.0))(@prisma/extension-read-replicas@0.4.1(@prisma/client@6.1.0(prisma@6.1.0)))':
dependencies:
'@prisma/client': 6.1.0(prisma@6.1.0)
'@prisma/extension-read-replicas': 0.4.1(@prisma/client@6.1.0(prisma@6.1.0))
chalk: 4.1.2
debug: 4.4.0(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
'@umami/redis-client@0.26.0':
dependencies:
debug: 4.4.0(supports-color@8.1.1)
@ -10180,6 +10462,13 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
esbuild-register@3.6.0(esbuild@0.25.2):
dependencies:
debug: 4.4.0(supports-color@8.1.1)
esbuild: 0.25.2
transitivePeerDependencies:
- supports-color
esbuild@0.25.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.2
@ -10766,6 +11055,8 @@ snapshots:
dependencies:
is-glob: 4.0.3
glob-to-regexp@0.4.1: {}
glob@10.3.10:
dependencies:
foreground-child: 3.3.1
@ -10975,6 +11266,13 @@ snapshots:
'@formatjs/icu-messageformat-parser': 2.9.4
tslib: 2.8.1
intl-messageformat@9.13.0:
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/fast-memoize': 1.2.1
'@formatjs/icu-messageformat-parser': 2.1.0
tslib: 2.8.1
ipaddr.js@2.2.0: {}
is-array-buffer@3.0.5:
@ -11949,6 +12247,12 @@ snapshots:
mmdb-lib@2.1.1: {}
moment-timezone@0.5.48:
dependencies:
moment: 2.30.1
moment@2.30.1: {}
ms@2.1.2: {}
ms@2.1.3: {}
@ -11957,6 +12261,40 @@ snapshots:
natural-compare@1.4.0: {}
next-basics@0.36.0(next@13.5.11(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
bcryptjs: 2.4.3
jsonwebtoken: 9.0.2
next: 13.5.11(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
pure-rand: 6.1.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
next@13.5.11(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 13.5.11
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001714
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.1(@babel/core@7.26.10)(react@18.3.1)
watchpack: 2.4.0
optionalDependencies:
'@next/swc-darwin-arm64': 13.5.9
'@next/swc-darwin-x64': 13.5.9
'@next/swc-linux-arm64-gnu': 13.5.9
'@next/swc-linux-arm64-musl': 13.5.9
'@next/swc-linux-x64-gnu': 13.5.9
'@next/swc-linux-x64-musl': 13.5.9
'@next/swc-win32-arm64-msvc': 13.5.9
'@next/swc-win32-ia32-msvc': 13.5.9
'@next/swc-win32-x64-msvc': 13.5.9
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
next@15.3.0(@babel/core@7.26.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.3.0
@ -12658,11 +12996,15 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.3.1
prisma@6.1.0:
prisma@6.6.0(typescript@5.8.3):
dependencies:
'@prisma/engines': 6.1.0
'@prisma/config': 6.6.0
'@prisma/engines': 6.6.0
optionalDependencies:
fsevents: 2.3.3
typescript: 5.8.3
transitivePeerDependencies:
- supports-color
process@0.11.10: {}
@ -12716,6 +13058,12 @@ snapshots:
react-hook-form: 7.55.0(react@19.1.0)
react-window: 1.8.11(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-dom@19.1.0(react@19.1.0):
dependencies:
react: 19.1.0
@ -12730,6 +13078,22 @@ snapshots:
dependencies:
react: 19.1.0
react-intl@5.25.1(react@18.3.1)(typescript@4.9.5):
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/icu-messageformat-parser': 2.1.0
'@formatjs/intl': 2.2.1(typescript@4.9.5)
'@formatjs/intl-displaynames': 5.4.3
'@formatjs/intl-listformat': 6.5.3
'@types/hoist-non-react-statics': 3.3.6
'@types/react': 18.3.20
hoist-non-react-statics: 3.3.2
intl-messageformat: 9.13.0
react: 18.3.1
tslib: 2.8.1
optionalDependencies:
typescript: 4.9.5
react-intl@6.8.9(react@19.1.0)(typescript@5.8.3):
dependencies:
'@formatjs/ecma402-abstract': 2.2.4
@ -12782,6 +13146,10 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react@18.3.1:
dependencies:
loose-envify: 1.4.0
react@19.1.0: {}
read-babelrc-up@1.1.0:
@ -13057,6 +13425,10 @@ snapshots:
safer-buffer@2.1.2: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
scheduler@0.26.0: {}
schema-utils@2.7.1:
@ -13370,6 +13742,13 @@ snapshots:
style-search@0.1.0: {}
styled-jsx@5.1.1(@babel/core@7.26.10)(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
optionalDependencies:
'@babel/core': 7.26.10
styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.1.0):
dependencies:
client-only: 0.0.1
@ -13759,6 +14138,10 @@ snapshots:
dependencies:
react: 19.1.0
use-sync-external-store@1.5.0(react@18.3.1):
dependencies:
react: 18.3.1
use-sync-external-store@1.5.0(react@19.1.0):
dependencies:
react: 19.1.0
@ -13804,6 +14187,11 @@ snapshots:
dependencies:
makeerror: 1.0.12
watchpack@2.4.0:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
web-streams-polyfill@3.3.3: {}
which-boxed-primitive@1.1.1:
@ -13943,6 +14331,14 @@ snapshots:
zod@3.24.3: {}
zustand@4.5.6(@types/react@19.1.2)(immer@9.0.21)(react@18.3.1):
dependencies:
use-sync-external-store: 1.5.0(react@18.3.1)
optionalDependencies:
'@types/react': 19.1.2
immer: 9.0.21
react: 18.3.1
zustand@4.5.6(@types/react@19.1.2)(immer@9.0.21)(react@19.1.0):
dependencies:
use-sync-external-store: 1.5.0(react@19.1.0)

10
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,10 @@
packages:
- '**'
ignoredBuiltDependencies:
- cypress
- esbuild
- sharp
onlyBuiltDependencies:
- '@prisma/client'
- '@prisma/engines'
- prisma

View file

@ -1,17 +0,0 @@
module.exports = {
plugins: [
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'custom-properties': false,
},
},
],
],
};

View file

@ -1,6 +1,6 @@
/* eslint-disable no-console */
require('dotenv').config();
const { PrismaClient } = require('@prisma/client');
const { PrismaClient } = require('../src/generated/prisma/index.js');
const chalk = require('chalk');
const { execSync } = require('child_process');
const semver = require('semver');

View file

@ -0,0 +1,48 @@
-----------------------------------------------------
-- postgreSQL
-----------------------------------------------------
UPDATE "website_event" we
SET fbclid = url.fbclid,
gclid = url.gclid,
li_fat_id = url.li_fat_id,
msclkid = url.msclkid,
ttclid = url.ttclid,
twclid = url.twclid,
utm_campaign = url.utm_campaign,
utm_content = url.utm_content,
utm_medium = url.utm_medium,
utm_source = url.utm_source,
utm_term = url.utm_term
FROM (SELECT event_id, website_id, session_id,
(regexp_matches(url_query, '(?:[&?]|^)fbclid=([^&]+)', 'i'))[1] AS fbclid,
(regexp_matches(url_query, '(?:[&?]|^)gclid=([^&]+)', 'i'))[1] AS gclid,
(regexp_matches(url_query, '(?:[&?]|^)li_fat_id=([^&]+)', 'i'))[1] AS li_fat_id,
(regexp_matches(url_query, '(?:[&?]|^)msclkid=([^&]+)', 'i'))[1] AS msclkid,
(regexp_matches(url_query, '(?:[&?]|^)ttclid=([^&]+)', 'i'))[1] AS ttclid,
(regexp_matches(url_query, '(?:[&?]|^)twclid=([^&]+)', 'i'))[1] AS twclid,
(regexp_matches(url_query, '(?:[&?]|^)utm_campaign=([^&]+)', 'i'))[1] AS utm_campaign,
(regexp_matches(url_query, '(?:[&?]|^)utm_content=([^&]+)', 'i'))[1] AS utm_content,
(regexp_matches(url_query, '(?:[&?]|^)utm_medium=([^&]+)', 'i'))[1] AS utm_medium,
(regexp_matches(url_query, '(?:[&?]|^)utm_source=([^&]+)', 'i'))[1] AS utm_source,
(regexp_matches(url_query, '(?:[&?]|^)utm_term=([^&]+)', 'i'))[1] AS utm_term
FROM "website_event") url
WHERE we.event_id = url.event_id
and we.session_id = url.session_id
and we.website_id = url.website_id;
-----------------------------------------------------
-- mySQL
-----------------------------------------------------
UPDATE `website_event`
SET fbclid = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)fbclid=[^&]+'), '=', -1), '&', 1),
gclid = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)gclid=[^&]+'), '=', -1), '&', 1),
li_fat_id = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)li_fat_id=[^&]+'), '=', -1), '&', 1),
msclkid = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)msclkid=[^&]+'), '=', -1), '&', 1),
ttclid = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)ttclid=[^&]+'), '=', -1), '&', 1),
twclid = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)twclid=[^&]+'), '=', -1), '&', 1),
utm_campaign = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_campaign=[^&]+'), '=', -1), '&', 1),
utm_content = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_content=[^&]+'), '=', -1), '&', 1),
utm_medium = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_medium=[^&]+'), '=', -1), '&', 1),
utm_source = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_source=[^&]+'), '=', -1), '&', 1),
utm_term = SUBSTRING_INDEX(SUBSTRING_INDEX(REGEXP_SUBSTR(url_query, '(?:[&?]|^)utm_term=[^&]+'), '=', -1), '&', 1)
WHERE 1 = 1;

View file

@ -14,7 +14,7 @@ export default function SessionsDataTable({
const queryResult = useWebsiteSessions(websiteId);
return (
<DataTable queryResult={queryResult} allowSearch={false} renderEmpty={() => children}>
<DataTable queryResult={queryResult} allowSearch={true} renderEmpty={() => children}>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
</DataTable>
);

View file

@ -18,7 +18,8 @@ export default function SessionInfo({ data }) {
<dd>
{data?.id} <CopyIcon value={data?.id} />
</dd>
<dt>{formatMessage(labels.distinctId)}</dt>
<dd>{data?.distinctId}</dd>
<dt>{formatMessage(labels.lastSeen)}</dt>
<dd>{formatTimezoneDate(data?.lastAt, 'PPPPpp')}</dd>

View file

@ -121,6 +121,7 @@ export async function POST(request: Request) {
country,
region,
city,
distinctId: id,
});
} catch (e: any) {
if (!e.message.toLowerCase().includes('unique constraint')) {
@ -144,7 +145,7 @@ export async function POST(request: Request) {
const base = hostname ? `https://${hostname}` : 'https://localhost';
const currentUrl = new URL(url, base);
let urlPath = currentUrl.pathname;
let urlPath = currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname;
const urlQuery = currentUrl.search.substring(1);
const urlDomain = currentUrl.hostname.replace(/^www./, '');
@ -215,6 +216,7 @@ export async function POST(request: Request) {
region,
city,
tag,
distinctId: id,
createdAt,
});
}
@ -228,6 +230,7 @@ export async function POST(request: Request) {
websiteId,
sessionId,
sessionData: data,
distinctId: id,
createdAt,
});
}

View file

@ -131,6 +131,7 @@ export const labels = defineMessages({
all: { id: 'label.all', defaultMessage: 'All' },
session: { id: 'label.session', defaultMessage: 'Session' },
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
distinctId: { id: 'label.distinct-id', defaultMessage: 'Distinct ID' },
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
activity: { id: 'label.activity', defaultMessage: 'Activity' },
dismiss: { id: 'label.dismiss', defaultMessage: 'Dismiss' },

View file

@ -1,5 +1,6 @@
import debug from 'debug';
import prisma from '@umami/prisma-client';
import { PrismaClient } from '@/generated/prisma/index.js';
import { readReplicas } from '@prisma/extension-read-replicas';
import { formatInTimeZone } from 'date-fns-tz';
import { MYSQL, POSTGRESQL, getDatabaseType } from '@/lib/db';
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
@ -10,6 +11,16 @@ import { filtersToArray } from './params';
const log = debug('umami:prisma');
const PRISMA = 'prisma';
const PRISMA_LOG_OPTIONS = {
log: [
{
emit: 'event',
level: 'query',
},
],
};
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%dT%H:%i:00',
hour: '%Y-%m-%d %H:00:00',
@ -234,14 +245,16 @@ async function rawQuery(sql: string, data: object): Promise<any> {
return db === MYSQL ? '?' : `$${params.length}${type ?? ''}`;
});
return prisma.rawQuery(query, params);
return process.env.DATABASE_REPLICA_URL
? client.$replica().$queryRawUnsafe(query, ...params)
: client.$queryRawUnsafe(query, ...params);
}
async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams) {
const { page = 1, pageSize, orderBy, sortDescending = false } = pageParams || {};
const size = +pageSize || DEFAULT_PAGE_SIZE;
const data = await prisma.client[model].findMany({
const data = await client[model].findMany({
...criteria,
...{
...(size > 0 && { take: +size, skip: +size * (+page - 1) }),
@ -255,7 +268,7 @@ async function pagedQuery<T>(model: string, criteria: T, pageParams: PageParams)
},
});
const count = await prisma.client[model].count({ where: (criteria as any).where });
const count = await client[model].count({ where: (criteria as any).where });
return { data, count, page: +page, pageSize: size, orderBy };
}
@ -323,8 +336,55 @@ function getSearchParameters(query: string, filters: { [key: string]: any }[]) {
};
}
function transaction(input: any, options?: any) {
return client.$transaction(input, options);
}
function getClient(params?: {
logQuery?: boolean;
queryLogger?: () => void;
replicaUrl?: string;
options?: any;
}): PrismaClient {
const {
logQuery = !!process.env.LOG_QUERY,
queryLogger,
replicaUrl = process.env.DATABASE_REPLICA_URL,
options,
} = params || {};
const prisma = new PrismaClient({
errorFormat: 'pretty',
...(logQuery && PRISMA_LOG_OPTIONS),
...options,
});
if (replicaUrl) {
prisma.$extends(
readReplicas({
url: replicaUrl,
}),
);
}
if (logQuery) {
prisma.$on('query' as never, queryLogger || log);
}
if (process.env.NODE_ENV !== 'production') {
global[PRISMA] = prisma;
}
log('Prisma initialized');
return prisma;
}
const client = global[PRISMA] || getClient();
export default {
...prisma,
client,
transaction,
getAddIntervalQuery,
getCastColumnQuery,
getDayDiffQuery,

View file

@ -3,13 +3,13 @@ import { REDIS, UmamiRedisClient } from '@umami/redis-client';
const enabled = !!process.env.REDIS_URL;
function getClient() {
const client = new UmamiRedisClient(process.env.REDIS_URL);
const redis = new UmamiRedisClient(process.env.REDIS_URL);
if (process.env.NODE_ENV !== 'production') {
global[REDIS] = client;
global[REDIS] = redis;
}
return client;
return redis;
}
const client = global[REDIS] || getClient();

View file

@ -39,6 +39,7 @@ export async function saveEvent(args: {
region?: string;
city?: string;
tag?: string;
distinctId?: string;
createdAt?: Date;
}) {
return runQuery({
@ -182,6 +183,7 @@ async function clickhouseQuery(data: {
region?: string;
city?: string;
tag?: string;
distinctId?: string;
createdAt?: Date;
}) {
const {
@ -211,6 +213,7 @@ async function clickhouseQuery(data: {
region,
city,
tag,
distinctId,
createdAt,
...args
} = data;
@ -247,6 +250,7 @@ async function clickhouseQuery(data: {
event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
tag: tag,
distinct_id: distinctId,
created_at: getUTCString(createdAt),
};

View file

@ -2,7 +2,19 @@ import { Prisma } from '@prisma/client';
import prisma from '@/lib/prisma';
export async function createSession(data: Prisma.SessionCreateInput) {
const { id, websiteId, browser, os, device, screen, language, country, region, city } = data;
const {
id,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId,
} = data;
return prisma.client.session.create({
data: {
@ -16,6 +28,7 @@ export async function createSession(data: Prisma.SessionCreateInput) {
country,
region,
city,
distinctId,
},
});
}

View file

@ -15,6 +15,7 @@ async function relationalQuery(websiteId: string, sessionId: string) {
return rawQuery(
`
select id,
distinct_id as "distinctId",
website_id as "websiteId",
hostname,
browser,
@ -33,6 +34,7 @@ async function relationalQuery(websiteId: string, sessionId: string) {
sum(${getTimestampDiffSQL('min_time', 'max_time')}) as "totaltime"
from (select
session.session_id as id,
session.distinct_id,
website_event.visit_id,
session.website_id,
website_event.hostname,
@ -52,8 +54,8 @@ async function relationalQuery(websiteId: string, sessionId: string) {
join website_event on website_event.session_id = session.session_id
where session.website_id = {{websiteId::uuid}}
and session.session_id = {{sessionId::uuid}}
group by session.session_id, visit_id, session.website_id, website_event.hostname, session.browser, session.os, session.device, session.screen, session.language, session.country, session.region, session.city) t
group by id, website_id, hostname, browser, os, device, screen, language, country, region, city;
group by session.session_id, session.distinct_id, visit_id, session.website_id, website_event.hostname, session.browser, session.os, session.device, session.screen, session.language, session.country, session.region, session.city) t
group by id, distinct_id, website_id, hostname, browser, os, device, screen, language, country, region, city;
`,
{ websiteId, sessionId },
).then(result => result?.[0]);
@ -66,6 +68,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
`
select id,
websiteId,
distinctId,
hostname,
browser,
os,
@ -83,6 +86,7 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
sum(max_time-min_time) as totaltime
from (select
session_id as id,
distinct_id as distinctId,
visit_id,
website_id as websiteId,
hostname,
@ -101,8 +105,8 @@ async function clickhouseQuery(websiteId: string, sessionId: string) {
from website_event_stats_hourly
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
group by session_id, visit_id, website_id, hostname, browser, os, device, screen, language, country, region, city) t
group by id, websiteId, hostname, browser, os, device, screen, language, country, region, city;
group by session_id, distinct_id, visit_id, website_id, hostname, browser, os, device, screen, language, country, region, city) t
group by id, websiteId, distinctId, hostname, browser, os, device, screen, language, country, region, city;
`,
{ websiteId, sessionId },
).then(result => result?.[0]);

View file

@ -1,5 +1,5 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
import { PageParams, QueryFilters } from '@/lib/types';
@ -14,10 +14,14 @@ export async function getWebsiteSessions(
async function relationalQuery(websiteId: string, filters: QueryFilters, pageParams: PageParams) {
const { pagedRawQuery, parseFilters } = prisma;
const { search } = pageParams;
const { filterQuery, params } = await parseFilters(websiteId, {
...filters,
});
const db = getDatabaseType();
const like = db === POSTGRESQL ? 'ilike' : 'like';
return pagedRawQuery(
`
with sessions as (
@ -43,6 +47,15 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
${filterQuery}
${
search
? `and (distinct_id ${like} {{search}}
or city ${like} {{search}}
or browser ${like} {{search}}
or os ${like} {{search}}
or device ${like} {{search}})`
: ''
}
group by session.session_id,
session.website_id,
website_event.hostname,
@ -58,7 +71,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
limit 1000)
select * from sessions
`,
params,
{ ...params, search: `%${search}%` },
pageParams,
);
}
@ -66,6 +79,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters, pagePar
async function clickhouseQuery(websiteId: string, filters: QueryFilters, pageParams?: PageParams) {
const { pagedQuery, parseFilters, getDateStringSQL } = clickhouse;
const { params, dateQuery, filterQuery } = await parseFilters(websiteId, filters);
const { search } = pageParams;
return pagedQuery(
`
@ -91,12 +105,21 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters, pagePar
where website_id = {websiteId:UUID}
${dateQuery}
${filterQuery}
${
search
? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
or (positionCaseInsensitive(city, {search:String}) > 0)
or (positionCaseInsensitive(browser, {search:String}) > 0)
or (positionCaseInsensitive(os, {search:String}) > 0)
or (positionCaseInsensitive(device, {search:String}) > 0))`
: ''
}
group by session_id, website_id, hostname, browser, os, device, screen, language, country, region, city
order by lastAt desc
limit 1000)
select * from sessions
`,
params,
{ ...params, search },
pageParams,
);
}

View file

@ -11,6 +11,7 @@ export async function saveSessionData(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
distinctId?: string;
createdAt?: Date;
}) {
return runQuery({
@ -23,10 +24,11 @@ export async function relationalQuery(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
distinctId?: string;
createdAt?: Date;
}) {
const { client } = prisma;
const { websiteId, sessionId, sessionData, createdAt } = data;
const { websiteId, sessionId, sessionData, distinctId, createdAt } = data;
const jsonKeys = flattenJSON(sessionData);
@ -39,6 +41,7 @@ export async function relationalQuery(data: {
numberValue: a.dataType === DATA_TYPE.number ? a.value : null,
dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null,
dataType: a.dataType,
distinctId,
createdAt,
}));
@ -80,9 +83,10 @@ async function clickhouseQuery(data: {
websiteId: string;
sessionId: string;
sessionData: DynamicData;
distinctId?: string;
createdAt?: Date;
}) {
const { websiteId, sessionId, sessionData, createdAt } = data;
const { websiteId, sessionId, sessionData, distinctId, createdAt } = data;
const { insert, getUTCString } = clickhouse;
const { sendMessage } = kafka;
@ -98,6 +102,7 @@ async function clickhouseQuery(data: {
string_value: getStringValue(value, dataType),
number_value: dataType === DATA_TYPE.number ? value : null,
date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null,
distinct_id: distinctId,
created_at: getUTCString(createdAt),
};
});

11034
yarn.lock

File diff suppressed because it is too large Load diff