Merge branch 'umami-software:master' into master
|
|
@ -2,15 +2,9 @@
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es2020": true,
|
"es2020": true,
|
||||||
"node": true
|
"node": true,
|
||||||
|
"jest": true
|
||||||
},
|
},
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:import/recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"next"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
|
|
@ -19,29 +13,29 @@
|
||||||
"ecmaVersion": 11,
|
"ecmaVersion": 11,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint", "prettier"],
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
"alias": {
|
"node": {
|
||||||
"map": [
|
"moduleDirectory": ["node_modules", "src/"]
|
||||||
["assets", "./src/assets"],
|
|
||||||
["components", "./src/components"],
|
|
||||||
["db", "./db"],
|
|
||||||
["hooks", "./src/components/hooks"],
|
|
||||||
["lang", "./src/lang"],
|
|
||||||
["lib", "./src/lib"],
|
|
||||||
["public", "./public"],
|
|
||||||
["queries", "./src/queries"],
|
|
||||||
["store", "./src/store"],
|
|
||||||
["styles", "./src/styles"]
|
|
||||||
],
|
|
||||||
"extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"plugin:css-modules/recommended",
|
||||||
|
"plugin:cypress/recommended",
|
||||||
|
"prettier",
|
||||||
|
"next"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint", "prettier", "promise", "css-modules", "cypress"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "error",
|
"no-console": "error",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"import/no-anonymous-default-export": "off",
|
"import/no-anonymous-default-export": "off",
|
||||||
|
|
|
||||||
8
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: "🐛 Bug Report"
|
name: '🐛 Bug Report'
|
||||||
description: Create a bug report for Umami.
|
description: Create a bug report for Umami.
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|
@ -22,6 +22,10 @@ body:
|
||||||
label: Relevant log output
|
label: Relevant log output
|
||||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Which Umami version are you using? (if relevant)
|
||||||
|
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Which browser are you using? (if relevant)
|
label: Which browser are you using? (if relevant)
|
||||||
|
|
@ -29,4 +33,4 @@ body:
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: How are you deploying your application? (if relevant)
|
label: How are you deploying your application? (if relevant)
|
||||||
description: 'For example: Vercel, Railway, Docker, etc'
|
description: 'For example: Vercel, Railway, Docker, etc'
|
||||||
|
|
|
||||||
5
.github/workflows/cd.yml
vendored
|
|
@ -16,7 +16,9 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: |
|
||||||
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $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 }}
|
||||||
|
|
@ -30,6 +32,7 @@ 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:
|
||||||
|
|
|
||||||
13
.github/workflows/ci.yml
vendored
|
|
@ -16,20 +16,21 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- node-version: 18.x
|
- node-version: 18.17
|
||||||
db-type: postgresql
|
db-type: postgresql
|
||||||
- node-version: 18.x
|
- node-version: 18.17
|
||||||
db-type: mysql
|
db-type: mysql
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'yarn'
|
||||||
env:
|
env:
|
||||||
DATABASE_TYPE: ${{ matrix.db-type }}
|
DATABASE_TYPE: ${{ matrix.db-type }}
|
||||||
- run: npm install --global yarn
|
- run: npm install --global yarn
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install
|
||||||
|
- run: yarn test
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
|
|
|
||||||
2
.gitignore
vendored
|
|
@ -22,6 +22,7 @@ node_modules
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
.yarn
|
||||||
*.iml
|
*.iml
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
|
|
@ -35,6 +36,7 @@ yarn-error.log*
|
||||||
# local env files
|
# local env files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
*.env.*
|
||||||
|
|
||||||
*.dev.yml
|
*.dev.yml
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,7 @@
|
||||||
"stylelint-config-prettier"
|
"stylelint-config-prettier"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-descending-specificity": null,
|
"no-descending-specificity": null
|
||||||
"selector-pseudo-class-no-unknown": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"ignorePseudoClasses": ["global", "horizontal", "vertical"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ignoreFiles": ["**/*.js", "**/*.md"]
|
"ignoreFiles": ["**/*.js", "**/*.md"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ 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@5.17.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 .
|
||||||
|
|
|
||||||
112
README.md
|
|
@ -1,69 +1,93 @@
|
||||||
# umami
|
<p align="center">
|
||||||
|
<img src="https://umami.is/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 16.13 or newer
|
||||||
- A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases.
|
- A database. Umami supports [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 ae 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:
|
||||||
|
|
||||||
|
|
@ -98,6 +124,36 @@ docker compose pull
|
||||||
docker compose up --force-recreate
|
docker compose up --force-recreate
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
||||||
|
|
|
||||||
12
cypress.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
// default username / password on init
|
||||||
|
env: {
|
||||||
|
umami_user: 'admin',
|
||||||
|
umami_password: 'umami',
|
||||||
|
},
|
||||||
|
});
|
||||||
52
cypress/docker-compose.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
umami:
|
||||||
|
build: ../
|
||||||
|
#image: ghcr.io/umami-software/umami:postgresql-latest
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://umami:umami@db:5432/umami
|
||||||
|
DATABASE_TYPE: postgresql
|
||||||
|
APP_SECRET: replace-me-with-a-random-string
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'curl http://localhost:3000/api/heartbeat']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: umami
|
||||||
|
POSTGRES_USER: umami
|
||||||
|
POSTGRES_PASSWORD: umami
|
||||||
|
volumes:
|
||||||
|
- umami-db-data:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
cypress:
|
||||||
|
image: 'cypress/included:13.6.0'
|
||||||
|
depends_on:
|
||||||
|
- umami
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- CYPRESS_baseUrl=http://umami:3000
|
||||||
|
- CYPRESS_umami_user=admin
|
||||||
|
- CYPRESS_umami_password=umami
|
||||||
|
volumes:
|
||||||
|
- ./tsconfig.json:/tsconfig.json
|
||||||
|
- ../cypress.config.ts:/cypress.config.ts
|
||||||
|
- ./:/cypress
|
||||||
|
- ../node_modules/:/node_modules
|
||||||
|
- ../src/lib/crypto.ts:/src/lib/crypto.ts
|
||||||
|
volumes:
|
||||||
|
umami-db-data:
|
||||||
22
cypress/e2e/login.cy.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
describe('Login tests', () => {
|
||||||
|
it(
|
||||||
|
'logs user in with correct credentials and logs user out',
|
||||||
|
{
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.getDataTest('input-username').find('input').click();
|
||||||
|
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
||||||
|
cy.getDataTest('input-password').find('input').click();
|
||||||
|
cy.getDataTest('input-password')
|
||||||
|
.find('input')
|
||||||
|
.type(Cypress.env('umami_password'), { delay: 50 });
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||||
|
cy.getDataTest('button-profile').click();
|
||||||
|
cy.getDataTest('item-logout').click();
|
||||||
|
cy.url().should('eq', Cypress.config().baseUrl + '/login');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
89
cypress/e2e/website.cy.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
describe('Website tests', () => {
|
||||||
|
Cypress.session.clearAllSavedSessions();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add a website', () => {
|
||||||
|
// add website
|
||||||
|
cy.visit('/settings/websites');
|
||||||
|
cy.getDataTest('button-website-add').click();
|
||||||
|
cy.contains(/Add website/i).should('be.visible');
|
||||||
|
cy.getDataTest('input-name').find('input').click();
|
||||||
|
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
|
||||||
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
|
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 });
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||||
|
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
||||||
|
|
||||||
|
// clean-up data
|
||||||
|
cy.getDataTest('link-button-edit').first().click();
|
||||||
|
cy.contains(/Details/i).should('be.visible');
|
||||||
|
cy.getDataTest('text-field-websiteId')
|
||||||
|
.find('input')
|
||||||
|
.then($input => {
|
||||||
|
const websiteId = $input[0].value;
|
||||||
|
cy.deleteWebsite(websiteId);
|
||||||
|
});
|
||||||
|
cy.visit('/settings/websites');
|
||||||
|
cy.contains(/Add test/i).should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit a website', () => {
|
||||||
|
// prep data
|
||||||
|
cy.addWebsite('Update test', 'updatetest.com');
|
||||||
|
cy.visit('/settings/websites');
|
||||||
|
|
||||||
|
// edit website
|
||||||
|
cy.getDataTest('link-button-edit').first().click();
|
||||||
|
cy.contains(/Details/i).should('be.visible');
|
||||||
|
cy.getDataTest('input-name').find('input').click();
|
||||||
|
cy.getDataTest('input-name').find('input').clear();
|
||||||
|
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
|
||||||
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
|
cy.getDataTest('input-domain').find('input').clear();
|
||||||
|
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
|
||||||
|
cy.getDataTest('button-submit').click({ force: true });
|
||||||
|
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
||||||
|
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
||||||
|
|
||||||
|
// verify tracking script
|
||||||
|
cy.get('div')
|
||||||
|
.contains(/Tracking code/i)
|
||||||
|
.click();
|
||||||
|
cy.get('textarea').should('contain.text', Cypress.config().baseUrl + '/script.js');
|
||||||
|
|
||||||
|
// clean-up data
|
||||||
|
cy.get('div')
|
||||||
|
.contains(/Details/i)
|
||||||
|
.click();
|
||||||
|
cy.contains(/Details/i).should('be.visible');
|
||||||
|
cy.getDataTest('text-field-websiteId')
|
||||||
|
.find('input')
|
||||||
|
.then($input => {
|
||||||
|
const websiteId = $input[0].value;
|
||||||
|
cy.deleteWebsite(websiteId);
|
||||||
|
});
|
||||||
|
cy.visit('/settings/websites');
|
||||||
|
cy.contains(/Add test/i).should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Delete a website', () => {
|
||||||
|
// prep data
|
||||||
|
cy.addWebsite('Delete test', 'deletetest.com');
|
||||||
|
cy.visit('/settings/websites');
|
||||||
|
|
||||||
|
// delete website
|
||||||
|
cy.getDataTest('link-button-edit').first().click();
|
||||||
|
cy.contains(/Data/i).should('be.visible');
|
||||||
|
cy.get('div').contains(/Data/i).click();
|
||||||
|
cy.contains(/All website data will be deleted./i).should('be.visible');
|
||||||
|
cy.getDataTest('button-delete').click();
|
||||||
|
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
|
||||||
|
cy.get('input[name="confirm"').type('DELETE');
|
||||||
|
cy.get('button[type="submit"]').click();
|
||||||
|
cy.contains(/Delete test/i).should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
57
cypress/support/e2e.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
import { uuid } from '../../src/lib/crypto';
|
||||||
|
|
||||||
|
Cypress.Commands.add('getDataTest', (value: string) => {
|
||||||
|
return cy.get(`[data-test=${value}]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (username: string, password: string) => {
|
||||||
|
cy.session([username, password], () => {
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/auth/login',
|
||||||
|
body: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
Cypress.env('authorization', `bearer ${response.body.token}`);
|
||||||
|
window.localStorage.setItem('umami.auth', JSON.stringify(response.body.token));
|
||||||
|
})
|
||||||
|
.its('status')
|
||||||
|
.should('eq', 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('addWebsite', (name: string, domain: string) => {
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/websites',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: Cypress.env('authorization'),
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
id: uuid(),
|
||||||
|
createdBy: '41e2b680-648e-4b09-bcd7-3e2b10c06264',
|
||||||
|
name: name,
|
||||||
|
domain: domain,
|
||||||
|
},
|
||||||
|
}).then(response => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('deleteWebsite', (websiteId: string) => {
|
||||||
|
cy.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/api/websites/${websiteId}`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: Cypress.env('authorization'),
|
||||||
|
},
|
||||||
|
}).then(response => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
cypress/support/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
/**
|
||||||
|
* Custom command to select DOM element by data-test attribute.
|
||||||
|
* @example cy.getDataTest('greeting')
|
||||||
|
*/
|
||||||
|
getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
|
||||||
|
/**
|
||||||
|
* Custom command to login user into the app.
|
||||||
|
* @example cy.login('admin', 'password)
|
||||||
|
*/
|
||||||
|
login(username: string, password: string): Chainable<JQuery<HTMLElement>>;
|
||||||
|
/**
|
||||||
|
* Custom command to create a website
|
||||||
|
* @example cy.addWebsite('test', 'test.com')
|
||||||
|
*/
|
||||||
|
addWebsite(name: string, domain: string): Chainable<JQuery<HTMLElement>>;
|
||||||
|
/**
|
||||||
|
* Custom command to create a website
|
||||||
|
* @example cy.deleteWebsite('02d89813-7a72-41e1-87f0-8d668f85008b')
|
||||||
|
*/
|
||||||
|
deleteWebsite(websiteId: string): Chainable<JQuery<HTMLElement>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
cypress/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["es5", "dom"],
|
||||||
|
"types": ["cypress", "node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "../cypress.config.ts"]
|
||||||
|
}
|
||||||
90
db/clickhouse/migrations/02_add_visit_id.sql
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
CREATE TABLE umami.website_event_join
|
||||||
|
(
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
created_at DateTime('UTC')
|
||||||
|
)
|
||||||
|
engine = MergeTree
|
||||||
|
ORDER BY (session_id, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
INSERT INTO umami.website_event_join
|
||||||
|
SELECT DISTINCT
|
||||||
|
s.session_id,
|
||||||
|
generateUUIDv4() visit_id,
|
||||||
|
s.created_at
|
||||||
|
FROM (SELECT DISTINCT session_id,
|
||||||
|
date_trunc('hour', created_at) created_at
|
||||||
|
FROM website_event) s;
|
||||||
|
|
||||||
|
-- create new table
|
||||||
|
CREATE TABLE umami.website_event_new
|
||||||
|
(
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
event_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),
|
||||||
|
subdivision2 LowCardinality(String),
|
||||||
|
city String,
|
||||||
|
url_path String,
|
||||||
|
url_query String,
|
||||||
|
referrer_path String,
|
||||||
|
referrer_query String,
|
||||||
|
referrer_domain String,
|
||||||
|
page_title String,
|
||||||
|
event_type UInt32,
|
||||||
|
event_name String,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
job_id UUID
|
||||||
|
)
|
||||||
|
engine = MergeTree
|
||||||
|
ORDER BY (website_id, session_id, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
INSERT INTO umami.website_event_new
|
||||||
|
SELECT we.website_id,
|
||||||
|
we.session_id,
|
||||||
|
j.visit_id,
|
||||||
|
we.event_id,
|
||||||
|
we.hostname,
|
||||||
|
we.browser,
|
||||||
|
we.os,
|
||||||
|
we.device,
|
||||||
|
we.screen,
|
||||||
|
we.language,
|
||||||
|
we.country,
|
||||||
|
we.subdivision1,
|
||||||
|
we.subdivision2,
|
||||||
|
we.city,
|
||||||
|
we.url_path,
|
||||||
|
we.url_query,
|
||||||
|
we.referrer_path,
|
||||||
|
we.referrer_query,
|
||||||
|
we.referrer_domain,
|
||||||
|
we.page_title,
|
||||||
|
we.event_type,
|
||||||
|
we.event_name,
|
||||||
|
we.created_at,
|
||||||
|
we.job_id
|
||||||
|
FROM umami.website_event we
|
||||||
|
JOIN umami.website_event_join j
|
||||||
|
ON we.session_id = j.session_id
|
||||||
|
and date_trunc('hour', we.created_at) = j.created_at
|
||||||
|
|
||||||
|
RENAME TABLE umami.website_event TO umami.website_event_old;
|
||||||
|
RENAME TABLE umami.website_event_new TO umami.website_event;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DROP TABLE umami.website_event_old
|
||||||
|
DROP TABLE umami.website_event_join
|
||||||
|
|
||||||
|
*/
|
||||||
57
db/clickhouse/migrations/03_session_data.sql
Normal 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
SET allow_experimental_object_type = 1;
|
|
||||||
|
|
||||||
-- Create Event
|
-- Create Event
|
||||||
CREATE TABLE umami.website_event
|
CREATE TABLE umami.website_event
|
||||||
(
|
(
|
||||||
website_id UUID,
|
website_id UUID,
|
||||||
session_id UUID,
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
event_id UUID,
|
event_id UUID,
|
||||||
--sessions
|
--sessions
|
||||||
hostname LowCardinality(String),
|
hostname LowCardinality(String),
|
||||||
|
|
@ -28,87 +27,13 @@ CREATE TABLE umami.website_event
|
||||||
event_type UInt32,
|
event_type UInt32,
|
||||||
event_name String,
|
event_name String,
|
||||||
created_at DateTime('UTC'),
|
created_at DateTime('UTC'),
|
||||||
job_id UUID
|
job_id Nullable(UUID)
|
||||||
)
|
|
||||||
engine = MergeTree
|
|
||||||
ORDER BY (website_id, session_id, created_at)
|
|
||||||
SETTINGS index_granularity = 8192;
|
|
||||||
|
|
||||||
CREATE TABLE umami.website_event_queue (
|
|
||||||
website_id UUID,
|
|
||||||
session_id UUID,
|
|
||||||
event_id UUID,
|
|
||||||
--sessions
|
|
||||||
hostname LowCardinality(String),
|
|
||||||
browser LowCardinality(String),
|
|
||||||
os LowCardinality(String),
|
|
||||||
device LowCardinality(String),
|
|
||||||
screen LowCardinality(String),
|
|
||||||
language LowCardinality(String),
|
|
||||||
country LowCardinality(String),
|
|
||||||
subdivision1 LowCardinality(String),
|
|
||||||
subdivision2 LowCardinality(String),
|
|
||||||
city String,
|
|
||||||
--pageviews
|
|
||||||
url_path String,
|
|
||||||
url_query String,
|
|
||||||
referrer_path String,
|
|
||||||
referrer_query String,
|
|
||||||
referrer_domain String,
|
|
||||||
page_title String,
|
|
||||||
--events
|
|
||||||
event_type UInt32,
|
|
||||||
event_name String,
|
|
||||||
created_at DateTime('UTC'),
|
|
||||||
--virtual columns
|
|
||||||
_error String,
|
|
||||||
_raw_message String
|
|
||||||
)
|
|
||||||
ENGINE = Kafka
|
|
||||||
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
|
|
||||||
kafka_topic_list = 'event',
|
|
||||||
kafka_group_name = 'event_consumer_group',
|
|
||||||
kafka_format = 'JSONEachRow',
|
|
||||||
kafka_max_block_size = 1048576,
|
|
||||||
kafka_handle_error_mode = 'stream';
|
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW umami.website_event_queue_mv TO umami.website_event AS
|
|
||||||
SELECT website_id,
|
|
||||||
session_id,
|
|
||||||
event_id,
|
|
||||||
hostname,
|
|
||||||
browser,
|
|
||||||
os,
|
|
||||||
device,
|
|
||||||
screen,
|
|
||||||
language,
|
|
||||||
country,
|
|
||||||
subdivision1,
|
|
||||||
subdivision2,
|
|
||||||
city,
|
|
||||||
url_path,
|
|
||||||
url_query,
|
|
||||||
referrer_path,
|
|
||||||
referrer_query,
|
|
||||||
referrer_domain,
|
|
||||||
page_title,
|
|
||||||
event_type,
|
|
||||||
event_name,
|
|
||||||
created_at
|
|
||||||
FROM umami.website_event_queue;
|
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW umami.website_event_errors_mv
|
|
||||||
(
|
|
||||||
error String,
|
|
||||||
raw String
|
|
||||||
)
|
)
|
||||||
ENGINE = MergeTree
|
ENGINE = MergeTree
|
||||||
ORDER BY (error, raw)
|
PARTITION BY toYYYYMM(created_at)
|
||||||
SETTINGS index_granularity = 8192 AS
|
ORDER BY (toStartOfHour(created_at), website_id, session_id, visit_id, created_at)
|
||||||
SELECT _error AS error,
|
PRIMARY KEY (toStartOfHour(created_at), website_id, session_id, visit_id)
|
||||||
_raw_message AS raw
|
SETTINGS index_granularity = 8192;
|
||||||
FROM umami.website_event_queue
|
|
||||||
WHERE length(_error) > 0;
|
|
||||||
|
|
||||||
CREATE TABLE umami.event_data
|
CREATE TABLE umami.event_data
|
||||||
(
|
(
|
||||||
|
|
@ -117,65 +42,153 @@ 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
|
|
||||||
ORDER BY (website_id, event_id, event_key, created_at)
|
|
||||||
SETTINGS index_granularity = 8192;
|
|
||||||
|
|
||||||
CREATE TABLE umami.event_data_queue (
|
|
||||||
website_id UUID,
|
|
||||||
session_id UUID,
|
|
||||||
event_id UUID,
|
|
||||||
url_path String,
|
|
||||||
event_name String,
|
|
||||||
event_key String,
|
|
||||||
string_value Nullable(String),
|
|
||||||
number_value Nullable(Decimal64(4)), --922337203685477.5625
|
|
||||||
date_value Nullable(DateTime('UTC')),
|
|
||||||
data_type UInt32,
|
|
||||||
created_at DateTime('UTC'),
|
|
||||||
--virtual columns
|
|
||||||
_error String,
|
|
||||||
_raw_message String
|
|
||||||
)
|
|
||||||
ENGINE = Kafka
|
|
||||||
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- input broker list
|
|
||||||
kafka_topic_list = 'event_data',
|
|
||||||
kafka_group_name = 'event_data_consumer_group',
|
|
||||||
kafka_format = 'JSONEachRow',
|
|
||||||
kafka_max_block_size = 1048576,
|
|
||||||
kafka_handle_error_mode = 'stream';
|
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW umami.event_data_queue_mv TO umami.event_data AS
|
|
||||||
SELECT website_id,
|
|
||||||
session_id,
|
|
||||||
event_id,
|
|
||||||
url_path,
|
|
||||||
event_name,
|
|
||||||
event_key,
|
|
||||||
string_value,
|
|
||||||
number_value,
|
|
||||||
date_value,
|
|
||||||
data_type,
|
|
||||||
created_at
|
|
||||||
FROM umami.event_data_queue;
|
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW umami.event_data_errors_mv
|
|
||||||
(
|
|
||||||
error String,
|
|
||||||
raw String
|
|
||||||
)
|
)
|
||||||
ENGINE = MergeTree
|
ENGINE = MergeTree
|
||||||
ORDER BY (error, raw)
|
ORDER BY (website_id, event_id, data_key, created_at)
|
||||||
SETTINGS index_granularity = 8192 AS
|
SETTINGS index_granularity = 8192;
|
||||||
SELECT _error AS error,
|
|
||||||
_raw_message AS raw
|
CREATE TABLE umami.session_data
|
||||||
FROM umami.event_data_queue
|
(
|
||||||
WHERE length(_error) > 0;
|
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')),
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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;
|
||||||
|
|
|
||||||
29
db/mysql/migrations/04_team_redesign/migration.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `team_website` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `team` ADD COLUMN `deleted_at` TIMESTAMP(0) NULL,
|
||||||
|
ADD COLUMN `logo_url` VARCHAR(2183) NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `user` ADD COLUMN `display_name` VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN `logo_url` VARCHAR(2183) NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `website` ADD COLUMN `created_by` VARCHAR(36) NULL,
|
||||||
|
ADD COLUMN `team_id` VARCHAR(36) NULL;
|
||||||
|
|
||||||
|
-- MigrateData
|
||||||
|
UPDATE `website` SET created_by = user_id WHERE team_id IS NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE `team_website`;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_team_id_idx` ON `website`(`team_id`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_created_by_idx` ON `website`(`created_by`);
|
||||||
22
db/mysql/migrations/05_add_visit_id/migration.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `website_event` ADD COLUMN `visit_id` VARCHAR(36) NULL;
|
||||||
|
|
||||||
|
UPDATE `website_event` we
|
||||||
|
JOIN (SELECT DISTINCT
|
||||||
|
s.session_id,
|
||||||
|
s.visit_time,
|
||||||
|
BIN_TO_UUID(RANDOM_BYTES(16) & 0xffffffffffff0fff3fffffffffffffff | 0x00000000000040008000000000000000) uuid
|
||||||
|
FROM (SELECT DISTINCT session_id,
|
||||||
|
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') visit_time
|
||||||
|
FROM `website_event`) s) a
|
||||||
|
ON we.session_id = a.session_id and DATE_FORMAT(we.created_at, '%Y-%m-%d %H:00:00') = a.visit_time
|
||||||
|
SET we.visit_id = a.uuid
|
||||||
|
WHERE we.visit_id IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE `website_event` MODIFY `visit_id` VARCHAR(36) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_visit_id_idx` ON `website_event`(`visit_id`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_event_website_id_visit_id_created_at_idx` ON `website_event`(`website_id`, `visit_id`, `created_at`);
|
||||||
20
db/mysql/migrations/06_session_data/migration.sql
Normal 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`);
|
||||||
|
|
@ -9,15 +9,18 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @map("user_id") @db.VarChar(36)
|
id String @id @unique @map("user_id") @db.VarChar(36)
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
role String @map("role") @db.VarChar(50)
|
role String @map("role") @db.VarChar(50)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
|
displayName String? @map("display_name") @db.VarChar(255)
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamp(0)
|
||||||
|
deletedAt DateTime? @map("deleted_at") @db.Timestamp(0)
|
||||||
|
|
||||||
website Website[]
|
websiteUser Website[] @relation("user")
|
||||||
|
websiteCreateUser Website[] @relation("createUser")
|
||||||
teamUser TeamUser[]
|
teamUser TeamUser[]
|
||||||
report Report[]
|
report Report[]
|
||||||
|
|
||||||
|
|
@ -64,19 +67,24 @@ model Website {
|
||||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
shareId String? @unique @map("share_id") @db.VarChar(50)
|
||||||
resetAt DateTime? @map("reset_at") @db.Timestamp(0)
|
resetAt DateTime? @map("reset_at") @db.Timestamp(0)
|
||||||
userId String? @map("user_id") @db.VarChar(36)
|
userId String? @map("user_id") @db.VarChar(36)
|
||||||
|
teamId String? @map("team_id") @db.VarChar(36)
|
||||||
|
createdBy String? @map("created_by") @db.VarChar(36)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
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)
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation("user", fields: [userId], references: [id])
|
||||||
teamWebsite TeamWebsite[]
|
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
eventData EventData[]
|
eventData EventData[]
|
||||||
report Report[]
|
report Report[]
|
||||||
sessionData SessionData[]
|
sessionData SessionData[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([shareId])
|
@@index([shareId])
|
||||||
|
@@index([createdBy])
|
||||||
@@map("website")
|
@@map("website")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +92,7 @@ model WebsiteEvent {
|
||||||
id String @id() @map("event_id") @db.VarChar(36)
|
id String @id() @map("event_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)
|
||||||
|
visitId String @map("visit_id") @db.VarChar(36)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
urlPath String @map("url_path") @db.VarChar(500)
|
urlPath String @map("url_path") @db.VarChar(500)
|
||||||
urlQuery String? @map("url_query") @db.VarChar(500)
|
urlQuery String? @map("url_query") @db.VarChar(500)
|
||||||
|
|
@ -99,6 +108,7 @@ model WebsiteEvent {
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
|
@@index([visitId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteId, createdAt])
|
@@index([websiteId, createdAt])
|
||||||
@@index([websiteId, createdAt, urlPath])
|
@@index([websiteId, createdAt, urlPath])
|
||||||
|
|
@ -107,6 +117,7 @@ model WebsiteEvent {
|
||||||
@@index([websiteId, createdAt, pageTitle])
|
@@index([websiteId, createdAt, pageTitle])
|
||||||
@@index([websiteId, createdAt, eventName])
|
@@index([websiteId, createdAt, eventName])
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
|
@@index([websiteId, visitId, createdAt])
|
||||||
@@map("website_event")
|
@@map("website_event")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +125,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)
|
||||||
|
|
@ -127,9 +138,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,7 +147,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)
|
||||||
|
|
@ -150,6 +160,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,11 +169,13 @@ model Team {
|
||||||
id String @id() @unique() @map("team_id") @db.VarChar(36)
|
id String @id() @unique() @map("team_id") @db.VarChar(36)
|
||||||
name String @db.VarChar(50)
|
name String @db.VarChar(50)
|
||||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||||
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
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)
|
||||||
|
|
||||||
|
website Website[]
|
||||||
teamUser TeamUser[]
|
teamUser TeamUser[]
|
||||||
teamWebsite TeamWebsite[]
|
|
||||||
|
|
||||||
@@index([accessCode])
|
@@index([accessCode])
|
||||||
@@map("team")
|
@@map("team")
|
||||||
|
|
@ -183,20 +197,6 @@ model TeamUser {
|
||||||
@@map("team_user")
|
@@map("team_user")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamWebsite {
|
|
||||||
id String @id() @unique() @map("team_website_id") @db.VarChar(36)
|
|
||||||
teamId String @map("team_id") @db.VarChar(36)
|
|
||||||
websiteId String @map("website_id") @db.VarChar(36)
|
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
|
||||||
|
|
||||||
team Team @relation(fields: [teamId], references: [id])
|
|
||||||
website Website @relation(fields: [websiteId], references: [id])
|
|
||||||
|
|
||||||
@@index([teamId])
|
|
||||||
@@index([websiteId])
|
|
||||||
@@map("team_website")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Report {
|
model Report {
|
||||||
id String @id() @unique() @map("report_id") @db.VarChar(36)
|
id String @id() @unique() @map("report_id") @db.VarChar(36)
|
||||||
userId String @map("user_id") @db.VarChar(36)
|
userId String @map("user_id") @db.VarChar(36)
|
||||||
|
|
|
||||||
29
db/postgresql/migrations/04_team_redesign/migration.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `team_website` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "team" ADD COLUMN "deleted_at" TIMESTAMPTZ(6),
|
||||||
|
ADD COLUMN "logo_url" VARCHAR(2183);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "user" ADD COLUMN "display_name" VARCHAR(255),
|
||||||
|
ADD COLUMN "logo_url" VARCHAR(2183);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website" ADD COLUMN "created_by" UUID,
|
||||||
|
ADD COLUMN "team_id" UUID;
|
||||||
|
|
||||||
|
-- MigrateData
|
||||||
|
UPDATE "website" SET created_by = user_id WHERE team_id IS NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "team_website";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_team_id_idx" ON "website"("team_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_created_by_idx" ON "website"("created_by");
|
||||||
22
db/postgresql/migrations/05_add_visit_id/migration.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website_event" ADD COLUMN "visit_id" UUID NULL;
|
||||||
|
|
||||||
|
UPDATE "website_event" we
|
||||||
|
SET visit_id = a.uuid
|
||||||
|
FROM (SELECT DISTINCT
|
||||||
|
s.session_id,
|
||||||
|
s.visit_time,
|
||||||
|
gen_random_uuid() uuid
|
||||||
|
FROM (SELECT DISTINCT session_id,
|
||||||
|
date_trunc('hour', created_at) visit_time
|
||||||
|
FROM "website_event") s) a
|
||||||
|
WHERE we.session_id = a.session_id
|
||||||
|
and date_trunc('hour', we.created_at) = a.visit_time;
|
||||||
|
|
||||||
|
ALTER TABLE "website_event" ALTER COLUMN "visit_id" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_visit_id_idx" ON "website_event"("visit_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_event_website_id_visit_id_created_at_idx" ON "website_event"("website_id", "visit_id", "created_at");
|
||||||
18
db/postgresql/migrations/06_session_data/migration.sql
Normal 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");
|
||||||
|
|
@ -9,17 +9,20 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @map("user_id") @db.Uuid
|
id String @id @unique @map("user_id") @db.Uuid
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
role String @map("role") @db.VarChar(50)
|
role String @map("role") @db.VarChar(50)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
displayName String? @map("display_name") @db.VarChar(255)
|
||||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
website Website[]
|
websiteUser Website[] @relation("user")
|
||||||
teamUser TeamUser[]
|
websiteCreateUser Website[] @relation("createUser")
|
||||||
report Report[]
|
teamUser TeamUser[]
|
||||||
|
report Report[]
|
||||||
|
|
||||||
@@map("user")
|
@@map("user")
|
||||||
}
|
}
|
||||||
|
|
@ -64,19 +67,24 @@ model Website {
|
||||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
shareId String? @unique @map("share_id") @db.VarChar(50)
|
||||||
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
||||||
userId String? @map("user_id") @db.Uuid
|
userId String? @map("user_id") @db.Uuid
|
||||||
|
teamId String? @map("team_id") @db.Uuid
|
||||||
|
createdBy String? @map("created_by") @db.Uuid
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
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)
|
||||||
|
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation("user", fields: [userId], references: [id])
|
||||||
teamWebsite TeamWebsite[]
|
createUser User? @relation("createUser", fields: [createdBy], references: [id])
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
eventData EventData[]
|
eventData EventData[]
|
||||||
report Report[]
|
report Report[]
|
||||||
sessionData SessionData[]
|
sessionData SessionData[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([shareId])
|
@@index([shareId])
|
||||||
|
@@index([createdBy])
|
||||||
@@map("website")
|
@@map("website")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,6 +92,7 @@ model WebsiteEvent {
|
||||||
id String @id() @map("event_id") @db.Uuid
|
id String @id() @map("event_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
|
||||||
|
visitId String @map("visit_id") @db.Uuid
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
urlPath String @map("url_path") @db.VarChar(500)
|
urlPath String @map("url_path") @db.VarChar(500)
|
||||||
urlQuery String? @map("url_query") @db.VarChar(500)
|
urlQuery String? @map("url_query") @db.VarChar(500)
|
||||||
|
|
@ -99,6 +108,7 @@ model WebsiteEvent {
|
||||||
|
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
|
@@index([visitId])
|
||||||
@@index([websiteId])
|
@@index([websiteId])
|
||||||
@@index([websiteId, createdAt])
|
@@index([websiteId, createdAt])
|
||||||
@@index([websiteId, createdAt, urlPath])
|
@@index([websiteId, createdAt, urlPath])
|
||||||
|
|
@ -107,6 +117,7 @@ model WebsiteEvent {
|
||||||
@@index([websiteId, createdAt, pageTitle])
|
@@index([websiteId, createdAt, pageTitle])
|
||||||
@@index([websiteId, createdAt, eventName])
|
@@index([websiteId, createdAt, eventName])
|
||||||
@@index([websiteId, sessionId, createdAt])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
|
@@index([websiteId, visitId, createdAt])
|
||||||
@@map("website_event")
|
@@map("website_event")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +125,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)
|
||||||
|
|
@ -128,7 +139,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,13 +147,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])
|
||||||
|
|
@ -150,6 +160,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,11 +169,13 @@ model Team {
|
||||||
id String @id() @unique() @map("team_id") @db.Uuid
|
id String @id() @unique() @map("team_id") @db.Uuid
|
||||||
name String @db.VarChar(50)
|
name String @db.VarChar(50)
|
||||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||||
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
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)
|
||||||
|
|
||||||
teamUser TeamUser[]
|
website Website[]
|
||||||
teamWebsite TeamWebsite[]
|
teamUser TeamUser[]
|
||||||
|
|
||||||
@@index([accessCode])
|
@@index([accessCode])
|
||||||
@@map("team")
|
@@map("team")
|
||||||
|
|
@ -183,20 +197,6 @@ model TeamUser {
|
||||||
@@map("team_user")
|
@@map("team_user")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamWebsite {
|
|
||||||
id String @id() @unique() @map("team_website_id") @db.Uuid
|
|
||||||
teamId String @map("team_id") @db.Uuid
|
|
||||||
websiteId String @map("website_id") @db.Uuid
|
|
||||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
|
||||||
|
|
||||||
team Team @relation(fields: [teamId], references: [id])
|
|
||||||
website Website @relation(fields: [websiteId], references: [id])
|
|
||||||
|
|
||||||
@@index([teamId])
|
|
||||||
@@index([websiteId])
|
|
||||||
@@map("team_website")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Report {
|
model Report {
|
||||||
id String @id() @unique() @map("report_id") @db.Uuid
|
id String @id() @unique() @map("report_id") @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
|
|
|
||||||
7
jest.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
roots: ['./src'],
|
||||||
|
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./src"
|
"baseUrl": "src"
|
||||||
}
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"metrics.referrers",
|
"metrics.referrers",
|
||||||
"message.powered-by"
|
"message.powered-by"
|
||||||
],
|
],
|
||||||
|
"mn-MN": ["label.max", "label.min", "label.os", "label.query", "label.url", "label.urls"],
|
||||||
"nb-NO": ["label.administrator", "label.dashboard"],
|
"nb-NO": ["label.administrator", "label.dashboard"],
|
||||||
"nl-NL": [
|
"nl-NL": [
|
||||||
"label.analytics",
|
"label.analytics",
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,25 @@ require('dotenv').config();
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const pkg = require('./package.json');
|
const pkg = require('./package.json');
|
||||||
|
|
||||||
|
const basePath = process.env.BASE_PATH;
|
||||||
|
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT;
|
||||||
|
const cloudMode = process.env.CLOUD_MODE;
|
||||||
|
const cloudUrl = process.env.CLOUD_URL;
|
||||||
|
const defaultLocale = process.env.DEFAULT_LOCALE;
|
||||||
|
const disableLogin = process.env.DISABLE_LOGIN;
|
||||||
|
const disableUI = process.env.DISABLE_UI;
|
||||||
|
const forceSSL = process.env.FORCE_SSL;
|
||||||
|
const frameAncestors = process.env.ALLOWED_FRAME_URLS;
|
||||||
|
const privateMode = process.env.PRIVATE_MODE;
|
||||||
|
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME;
|
||||||
|
|
||||||
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`,
|
`connect-src 'self' api.umami.is cloud.umami.is`,
|
||||||
`frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`,
|
`frame-ancestors 'self' ${frameAncestors}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
|
|
@ -26,7 +38,7 @@ const headers = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.FORCE_SSL) {
|
if (forceSSL) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: 'Strict-Transport-Security',
|
key: 'Strict-Transport-Security',
|
||||||
value: 'max-age=63072000; includeSubDomains; preload',
|
value: 'max-age=63072000; includeSubDomains; preload',
|
||||||
|
|
@ -35,15 +47,15 @@ if (process.env.FORCE_SSL) {
|
||||||
|
|
||||||
const rewrites = [];
|
const rewrites = [];
|
||||||
|
|
||||||
if (process.env.COLLECT_API_ENDPOINT) {
|
if (collectApiEndpoint) {
|
||||||
rewrites.push({
|
rewrites.push({
|
||||||
source: process.env.COLLECT_API_ENDPOINT,
|
source: collectApiEndpoint,
|
||||||
destination: '/api/send',
|
destination: '/api/send',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.TRACKER_SCRIPT_NAME) {
|
if (trackerScriptName) {
|
||||||
const names = process.env.TRACKER_SCRIPT_NAME?.split(',').map(name => name.trim());
|
const names = trackerScriptName?.split(',').map(name => name.trim());
|
||||||
|
|
||||||
if (names) {
|
if (names) {
|
||||||
names.forEach(name => {
|
names.forEach(name => {
|
||||||
|
|
@ -58,36 +70,56 @@ if (process.env.TRACKER_SCRIPT_NAME) {
|
||||||
const redirects = [
|
const redirects = [
|
||||||
{
|
{
|
||||||
source: '/settings',
|
source: '/settings',
|
||||||
destination: process.env.CLOUD_MODE
|
destination: '/settings/websites',
|
||||||
? `${process.env.CLOUD_URL}/settings/websites`
|
permanent: true,
|
||||||
: '/settings/websites',
|
},
|
||||||
|
{
|
||||||
|
source: '/teams/:id',
|
||||||
|
destination: '/teams/:id/dashboard',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/teams/:id/settings',
|
||||||
|
destination: '/teams/:id/settings/team',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.CLOUD_MODE && process.env.CLOUD_URL && process.env.DISABLE_LOGIN) {
|
if (cloudMode && cloudUrl) {
|
||||||
redirects.push({
|
redirects.push({
|
||||||
source: '/login',
|
source: '/settings/:path*',
|
||||||
destination: process.env.CLOUD_URL,
|
destination: `${cloudUrl}/settings/:path*`,
|
||||||
permanent: false,
|
permanent: false,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const basePath = process.env.BASE_PATH;
|
redirects.push({
|
||||||
|
source: '/teams/:id/settings/:path*',
|
||||||
|
destination: `${cloudUrl}/teams/:id/settings/:path*`,
|
||||||
|
permanent: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (disableLogin) {
|
||||||
|
redirects.push({
|
||||||
|
source: '/login',
|
||||||
|
destination: cloudUrl,
|
||||||
|
permanent: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
env: {
|
env: {
|
||||||
basePath: basePath || '',
|
basePath,
|
||||||
cloudMode: process.env.CLOUD_MODE || '',
|
cloudMode,
|
||||||
cloudUrl: process.env.CLOUD_URL || '',
|
cloudUrl,
|
||||||
configUrl: '/config',
|
configUrl: '/config',
|
||||||
currentVersion: pkg.version,
|
currentVersion: pkg.version,
|
||||||
defaultLocale: process.env.DEFAULT_LOCALE || '',
|
defaultLocale,
|
||||||
disableLogin: process.env.DISABLE_LOGIN || '',
|
disableLogin,
|
||||||
disableUI: process.env.DISABLE_UI || '',
|
disableUI,
|
||||||
hostUrl: process.env.HOST_URL || '',
|
privateMode,
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
|
@ -135,6 +167,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() {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
61
package.json
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "2.9.0",
|
"version": "2.13.2",
|
||||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://umami.is",
|
"homepage": "https://umami.is",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"check-db": "node scripts/check-db.js",
|
"check-db": "node scripts/check-db.js",
|
||||||
"check-env": "node scripts/check-env.js",
|
"check-env": "node scripts/check-env.js",
|
||||||
"copy-db-files": "node scripts/copy-db-files.js",
|
"copy-db-files": "node scripts/copy-db-files.js",
|
||||||
"extract-messages": "formatjs extract \"src/{pages,components}/**/*.js\" --out-file build/messages.json",
|
"extract-messages": "formatjs extract \"src/components/messages.ts\" --out-file build/extracted-messages.json",
|
||||||
"merge-messages": "node scripts/merge-messages.js",
|
"merge-messages": "node scripts/merge-messages.js",
|
||||||
"generate-lang": "npm-run-all extract-messages merge-messages",
|
"generate-lang": "npm-run-all extract-messages merge-messages",
|
||||||
"format-lang": "node scripts/format-lang.js",
|
"format-lang": "node scripts/format-lang.js",
|
||||||
|
|
@ -42,7 +42,10 @@
|
||||||
"change-password": "node scripts/change-password.js",
|
"change-password": "node scripts/change-password.js",
|
||||||
"lint": "next lint --quiet",
|
"lint": "next lint --quiet",
|
||||||
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install",
|
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install",
|
||||||
"postbuild": "node scripts/postbuild.js"
|
"postbuild": "node scripts/postbuild.js",
|
||||||
|
"test": "jest",
|
||||||
|
"cypress-open": "cypress open cypress run",
|
||||||
|
"cypress-run": "cypress run cypress run"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.{js,jsx,ts,tsx}": [
|
"**/*.{js,jsx,ts,tsx}": [
|
||||||
|
|
@ -61,16 +64,19 @@
|
||||||
".next/cache"
|
".next/cache"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clickhouse/client": "^0.2.2",
|
"@clickhouse/client": "^1.4.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.6.0",
|
"@prisma/client": "5.17",
|
||||||
"@prisma/extension-read-replicas": "^0.3.0",
|
"@prisma/extension-read-replicas": "^0.3.0",
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@tanstack/react-query": "^5.12.2",
|
"@tanstack/react-query": "^5.28.6",
|
||||||
"@umami/prisma-client": "^0.8.0",
|
"@umami/prisma-client": "^0.14.0",
|
||||||
"@umami/redis-client": "^0.18.0",
|
"@umami/redis-client": "^0.21.0",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.4.2",
|
||||||
"chartjs-adapter-date-fns": "^3.0.0",
|
"chartjs-adapter-date-fns": "^3.0.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"colord": "^2.9.2",
|
"colord": "^2.9.2",
|
||||||
|
|
@ -78,28 +84,29 @@
|
||||||
"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",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
"immer": "^9.0.12",
|
"immer": "^9.0.12",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
"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": "^3.4.5",
|
"isbot": "^5.1.16",
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
|
"md5": "^2.3.0",
|
||||||
"moment-timezone": "^0.5.35",
|
"moment-timezone": "^0.5.35",
|
||||||
"next": "14.0.4",
|
"next": "14.2.10",
|
||||||
"next-basics": "^0.39.0",
|
"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.6.0",
|
"prisma": "5.17",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.114.0",
|
"react-basics": "^0.125.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
|
|
@ -110,22 +117,22 @@
|
||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"yup": "^0.32.11",
|
"yup": "^0.32.11",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.5.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^4.2.29",
|
"@formatjs/cli": "^4.2.29",
|
||||||
"@netlify/plugin-nextjs": "^4.27.3",
|
"@netlify/plugin-nextjs": "^5.1.0",
|
||||||
"@rollup/plugin-alias": "^5.0.0",
|
"@rollup/plugin-alias": "^5.0.0",
|
||||||
"@rollup/plugin-buble": "^1.0.2",
|
|
||||||
"@rollup/plugin-commonjs": "^25.0.4",
|
"@rollup/plugin-commonjs": "^25.0.4",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.0",
|
"@rollup/plugin-node-resolve": "^15.2.0",
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@rollup/plugin-replace": "^5.0.2",
|
||||||
"@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/jest": "^29.5.12",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.0",
|
||||||
"@types/react": "^18.2.41",
|
"@types/react": "^18.2.41",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
|
|
@ -133,15 +140,20 @@
|
||||||
"@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",
|
||||||
"esbuild": "^0.17.17",
|
"esbuild": "^0.17.17",
|
||||||
"eslint": "^8.33.0",
|
"eslint": "^8.33.0",
|
||||||
"eslint-config-next": "^12.2.4",
|
"eslint-config-next": "^14.0.4",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-jest": "^27.9.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"lint-staged": "^14.0.1",
|
"lint-staged": "^14.0.1",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
|
|
@ -159,11 +171,12 @@
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-css-modules": "^4.1.0",
|
"stylelint-config-css-modules": "^4.4.0",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^9.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"tar": "^6.1.2",
|
"tar": "^6.1.2",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 806 B After Width: | Height: | Size: 806 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 420 B After Width: | Height: | Size: 420 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 635 B After Width: | Height: | Size: 635 B |
|
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 819 B |
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 680 B |
|
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 819 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 794 B |
|
Before Width: | Height: | Size: 632 B After Width: | Height: | Size: 632 B |
|
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 535 B After Width: | Height: | Size: 535 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 621 B |
|
Before Width: | Height: | Size: 106 B After Width: | Height: | Size: 106 B |
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 794 B |
|
Before Width: | Height: | Size: 235 B After Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 122 B After Width: | Height: | Size: 122 B |
|
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
|
Before Width: | Height: | Size: 183 B After Width: | Height: | Size: 183 B |
|
Before Width: | Height: | Size: 110 B After Width: | Height: | Size: 110 B |
|
Before Width: | Height: | Size: 201 B After Width: | Height: | Size: 201 B |
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 186 B |
|
Before Width: | Height: | Size: 141 B After Width: | Height: | Size: 141 B |
|
Before Width: | Height: | Size: 237 B After Width: | Height: | Size: 237 B |
|
Before Width: | Height: | Size: 102 B After Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 211 B |
|
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
|
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 163 B |
|
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 150 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 149 B After Width: | Height: | Size: 149 B |
|
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 129 B |
|
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 105 B |
|
Before Width: | Height: | Size: 140 B After Width: | Height: | Size: 140 B |
|
Before Width: | Height: | Size: 97 B After Width: | Height: | Size: 97 B |
|
Before Width: | Height: | Size: 153 B After Width: | Height: | Size: 153 B |
|
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 264 B |
|
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 353 B After Width: | Height: | Size: 353 B |
|
Before Width: | Height: | Size: 286 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 350 B After Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 260 B After Width: | Height: | Size: 260 B |
|
Before Width: | Height: | Size: 147 B After Width: | Height: | Size: 147 B |
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
|
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 150 B |
|
Before Width: | Height: | Size: 114 B After Width: | Height: | Size: 114 B |
|
Before Width: | Height: | Size: 145 B After Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 253 B After Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 171 B |