Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Didier Krux 2025-01-24 10:53:36 +00:00
commit 6d8be5a555
1090 changed files with 70594 additions and 17012 deletions

View file

@ -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",

View file

@ -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)

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

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

View file

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

View file

@ -16,13 +16,22 @@ 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 "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
- name: Generate tags
id: generate_tags
run: |
echo "tag_patch=$(echo ${{ matrix.db-type }})-${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
echo "tag_minor=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1,2)" >> $GITHUB_ENV
echo "tag_major=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1)" >> $GITHUB_ENV
echo "tag_latest=$(echo ${{ matrix.db-type }})-latest" >> $GITHUB_ENV
- uses: mr-smithers-excellent/docker-build-push@v6 - uses: mr-smithers-excellent/docker-build-push@v6
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }} name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
with: with:
image: umami image: umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io registry: ghcr.io
multiPlatform: true multiPlatform: true
@ -34,7 +43,7 @@ jobs:
name: Build & push Docker image to docker.io for ${{ matrix.db-type }} name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
with: with:
image: umamisoftware/umami image: umamisoftware/umami
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }} buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}

View file

@ -16,20 +16,21 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- node-version: 18.x - node-version: 18.18
db-type: postgresql db-type: postgresql
- node-version: 18.x - node-version: 18.18
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
View file

@ -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

View file

@ -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"]
} }

View file

@ -36,8 +36,8 @@ RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
RUN set -x \ RUN set -x \
&& apk add --no-cache curl \ && apk add --no-cache curl openssl \
&& 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 .

114
README.md
View file

@ -1,69 +1,93 @@
# umami <p align="center">
<img src="https://content.umami.is/website/images/umami-logo.png" alt="Umami Logo" width="100">
</p>
Umami is a simple, fast, privacy-focused alternative to Google Analytics. <h1 align="center">Umami</h1>
## Getting started <p align="center">
<i>Umami is a simple, fast, privacy-focused alternative to Google Analytics.</i>
</p>
A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/) <p align="center">
<a href="https://github.com/umami-software/umami/releases">
<img src="https://img.shields.io/github/release/umami-software/umami.svg" alt="GitHub Release" />
</a>
<a href="https://github.com/umami-software/umami/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/umami-software/umami.svg" alt="MIT License" />
</a>
<a href="https://github.com/umami-software/umami/actions">
<img src="https://img.shields.io/github/actions/workflow/status/umami-software/umami/ci.yml" alt="Build Status" />
</a>
<a href="https://analytics.umami.is/share/LGazGOecbDtaIwDr/umami.is" style="text-decoration: none;">
<img src="https://img.shields.io/badge/Try%20Demo%20Now-Click%20Here-brightgreen" alt="Umami Demo" />
</a>
</p>
## Installing from source ---
## 🚀 Getting Started
A detailed getting started guide can be found at [umami.is/docs](https://umami.is/docs/).
---
## 🛠 Installing from Source
### Requirements ### Requirements
- A server with Node.js version 16.13 or newer - A server with Node.js version 18.18 or newer
- A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases. - A database. Umami supports [MariaDB](https://www.mariadb.org/) (minimum v10.5), [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
### Install Yarn ### Install Yarn
``` ```bash
npm install -g yarn npm install -g yarn
``` ```
### Get the source code and install packages ### Get the Source Code and Install Packages
``` ```bash
git clone https://github.com/umami-software/umami.git git clone https://github.com/umami-software/umami.git
cd umami cd umami
yarn install yarn install
``` ```
### Configure umami ### Configure Umami
Create an `.env` file with the following Create an `.env` file with the following:
``` ```bash
DATABASE_URL=connection-url DATABASE_URL=connection-url
``` ```
The connection url is in the following format: The connection URL format:
``` ```bash
postgresql://username:mypassword@localhost:5432/mydb postgresql://username:mypassword@localhost:5432/mydb
mysql://username:mypassword@localhost:3306/mydb mysql://username:mypassword@localhost:3306/mydb
``` ```
### Build the application ### Build the Application
```bash ```bash
yarn build yarn build
``` ```
The build step will also create tables in your database if you 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
View 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',
},
});

View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts", "../cypress.config.ts"]
}

View 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
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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`);

View 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`);

View file

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

View file

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

View file

@ -1,5 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
} }
datasource db { datasource db {
@ -13,11 +14,14 @@ model User {
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)
logoUrl String? @map("logo_url") @db.VarChar(2183)
displayName String? @map("display_name") @db.VarChar(255)
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)
website Website[] websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[] teamUser TeamUser[]
report Report[] report Report[]
@ -64,19 +68,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 +93,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)
@ -93,12 +103,14 @@ model WebsiteEvent {
pageTitle String? @map("page_title") @db.VarChar(500) pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.UnsignedInt eventType Int @default(1) @map("event_type") @db.UnsignedInt
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
eventData EventData[] eventData EventData[]
session Session @relation(fields: [sessionId], references: [id]) session Session @relation(fields: [sessionId], references: [id])
@@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])
@ -106,7 +118,9 @@ model WebsiteEvent {
@@index([websiteId, createdAt, referrerDomain]) @@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle]) @@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName]) @@index([websiteId, createdAt, eventName])
@@index([websiteId, createdAt, tag])
@@index([websiteId, sessionId, createdAt]) @@index([websiteId, sessionId, createdAt])
@@index([websiteId, visitId, createdAt])
@@map("website_event") @@map("website_event")
} }
@ -114,7 +128,7 @@ model EventData {
id String @id() @map("event_data_id") @db.VarChar(36) id String @id() @map("event_data_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36)
websiteEventId String @map("website_event_id") @db.VarChar(36) websiteEventId String @map("website_event_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0) dateValue DateTime? @map("date_value") @db.Timestamp(0)
@ -127,9 +141,8 @@ model EventData {
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@index([websiteEventId]) @@index([websiteEventId])
@@index([websiteId, websiteEventId, createdAt])
@@index([websiteId, createdAt]) @@index([websiteId, createdAt])
@@index([websiteId, createdAt, eventKey]) @@index([websiteId, createdAt, dataKey])
@@map("event_data") @@map("event_data")
} }
@ -137,7 +150,7 @@ model SessionData {
id String @id() @map("session_data_id") @db.VarChar(36) id String @id() @map("session_data_id") @db.VarChar(36)
websiteId String @map("website_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36)
sessionId String @map("session_id") @db.VarChar(36) sessionId String @map("session_id") @db.VarChar(36)
eventKey String @map("event_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamp(0) dateValue DateTime? @map("date_value") @db.Timestamp(0)
@ -150,6 +163,8 @@ model SessionData {
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@index([sessionId]) @@index([sessionId])
@@index([sessionId, createdAt])
@@index([websiteId, createdAt, dataKey])
@@map("session_data") @@map("session_data")
} }
@ -157,11 +172,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 +200,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)

View 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");

View 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");

View file

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

View file

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

View file

@ -1,5 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
} }
datasource db { datasource db {
@ -13,11 +14,14 @@ model User {
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)
logoUrl String? @map("logo_url") @db.VarChar(2183)
displayName String? @map("display_name") @db.VarChar(255)
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)
website Website[] websiteUser Website[] @relation("user")
websiteCreateUser Website[] @relation("createUser")
teamUser TeamUser[] teamUser TeamUser[]
report Report[] report Report[]
@ -64,19 +68,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 +93,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)
@ -93,12 +103,14 @@ model WebsiteEvent {
pageTitle String? @map("page_title") @db.VarChar(500) pageTitle String? @map("page_title") @db.VarChar(500)
eventType Int @default(1) @map("event_type") @db.Integer eventType Int @default(1) @map("event_type") @db.Integer
eventName String? @map("event_name") @db.VarChar(50) eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
eventData EventData[] eventData EventData[]
session Session @relation(fields: [sessionId], references: [id]) session Session @relation(fields: [sessionId], references: [id])
@@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])
@ -106,7 +118,9 @@ model WebsiteEvent {
@@index([websiteId, createdAt, referrerDomain]) @@index([websiteId, createdAt, referrerDomain])
@@index([websiteId, createdAt, pageTitle]) @@index([websiteId, createdAt, pageTitle])
@@index([websiteId, createdAt, eventName]) @@index([websiteId, createdAt, eventName])
@@index([websiteId, createdAt, tag])
@@index([websiteId, sessionId, createdAt]) @@index([websiteId, sessionId, createdAt])
@@index([websiteId, visitId, createdAt])
@@map("website_event") @@map("website_event")
} }
@ -114,7 +128,7 @@ model EventData {
id String @id() @map("event_data_id") @db.Uuid id String @id() @map("event_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
websiteEventId String @map("website_event_id") @db.Uuid websiteEventId String @map("website_event_id") @db.Uuid
eventKey String @map("event_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6) dateValue DateTime? @map("date_value") @db.Timestamptz(6)
@ -128,7 +142,7 @@ model EventData {
@@index([websiteId]) @@index([websiteId])
@@index([websiteEventId]) @@index([websiteEventId])
@@index([websiteId, createdAt]) @@index([websiteId, createdAt])
@@index([websiteId, createdAt, eventKey]) @@index([websiteId, createdAt, dataKey])
@@map("event_data") @@map("event_data")
} }
@ -136,13 +150,12 @@ model SessionData {
id String @id() @map("session_data_id") @db.Uuid id String @id() @map("session_data_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid sessionId String @map("session_id") @db.Uuid
sessionKey String @map("session_key") @db.VarChar(500) dataKey String @map("data_key") @db.VarChar(500)
stringValue String? @map("string_value") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500)
numberValue Decimal? @map("number_value") @db.Decimal(19, 4) numberValue Decimal? @map("number_value") @db.Decimal(19, 4)
dateValue DateTime? @map("date_value") @db.Timestamptz(6) dateValue DateTime? @map("date_value") @db.Timestamptz(6)
dataType Int @map("data_type") @db.Integer dataType Int @map("data_type") @db.Integer
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
deletedAt DateTime? @default(now()) @map("deleted_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id]) website Website @relation(fields: [websiteId], references: [id])
session Session @relation(fields: [sessionId], references: [id]) session Session @relation(fields: [sessionId], references: [id])
@ -150,6 +163,8 @@ model SessionData {
@@index([createdAt]) @@index([createdAt])
@@index([websiteId]) @@index([websiteId])
@@index([sessionId]) @@index([sessionId])
@@index([sessionId, createdAt])
@@index([websiteId, createdAt, dataKey])
@@map("session_data") @@map("session_data")
} }
@ -157,11 +172,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)
website Website[]
teamUser TeamUser[] teamUser TeamUser[]
teamWebsite TeamWebsite[]
@@index([accessCode]) @@index([accessCode])
@@map("team") @@map("team")
@ -183,20 +200,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

View file

@ -12,6 +12,7 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
init: true
restart: always restart: always
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"] test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]

7
jest.config.ts Normal file
View file

@ -0,0 +1,7 @@
export default {
roots: ['./src'],
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
};

View file

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "./src" "baseUrl": "src"
} },
"include": ["src"]
} }

View file

@ -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",

2
next-env.d.ts vendored
View file

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

View file

@ -3,16 +3,31 @@ require('dotenv').config();
const path = require('path'); const path = require('path');
const pkg = require('./package.json'); const pkg = require('./package.json');
const TRACKER_SCRIPT = '/script.js';
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 trackerScriptURL = process.env.TRACKER_SCRIPT_URL;
const contentSecurityPolicy = [ const contentSecurityPolicy = [
`default-src 'self'`, `default-src 'self'`,
`img-src *`, `img-src * data:`,
`script-src 'self' 'unsafe-eval' 'unsafe-inline'`, `script-src 'self' 'unsafe-eval' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`, `style-src 'self' 'unsafe-inline'`,
`connect-src 'self' api.umami.is`, `connect-src 'self' api.umami.is cloud.umami.is`,
`frame-ancestors 'self' ${process.env.ALLOWED_FRAME_URLS || ''}`, `frame-ancestors 'self' ${frameAncestors}`,
]; ];
const headers = [ const defaultHeaders = [
{ {
key: 'X-DNS-Prefetch-Control', key: 'X-DNS-Prefetch-Control',
value: 'on', value: 'on',
@ -26,68 +41,125 @@ const headers = [
}, },
]; ];
if (process.env.FORCE_SSL) { if (forceSSL) {
headers.push({ defaultHeaders.push({
key: 'Strict-Transport-Security', key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload', value: 'max-age=63072000; includeSubDomains; preload',
}); });
} }
const trackerHeaders = [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Cache-Control',
value: 'public, max-age=86400, must-revalidate',
},
];
const headers = [
{
source: '/:path*',
headers: defaultHeaders,
},
{
source: TRACKER_SCRIPT,
headers: trackerHeaders,
},
];
const rewrites = []; const rewrites = [];
if (process.env.COLLECT_API_ENDPOINT) { if (trackerScriptURL) {
rewrites.push({ rewrites.push({
source: process.env.COLLECT_API_ENDPOINT, source: TRACKER_SCRIPT,
destination: trackerScriptURL,
});
}
if (collectApiEndpoint) {
rewrites.push({
source: collectApiEndpoint,
destination: '/api/send', destination: '/api/send',
}); });
} }
if (process.env.TRACKER_SCRIPT_NAME) {
const names = process.env.TRACKER_SCRIPT_NAME?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
rewrites.push({
source: `/${name.replace(/^\/+/, '')}`,
destination: '/script.js',
});
});
}
}
const redirects = [ const redirects = [
{ {
source: '/settings', source: '/settings',
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) { // Adding rewrites + headers for all alternative tracker script names.
if (trackerScriptName) {
const names = trackerScriptName?.split(',').map(name => name.trim());
if (names) {
names.forEach(name => {
const normalizedSource = `/${name.replace(/^\/+/, '')}`;
rewrites.push({
source: normalizedSource,
destination: TRACKER_SCRIPT,
});
headers.push({
source: normalizedSource,
headers: trackerHeaders,
});
});
}
}
if (cloudMode && cloudUrl) {
redirects.push({
source: '/settings/:path*',
destination: `${cloudUrl}/settings/:path*`,
permanent: false,
});
redirects.push({
source: '/teams/:id/settings/:path*',
destination: `${cloudUrl}/teams/:id/settings/:path*`,
permanent: false,
});
if (disableLogin) {
redirects.push({ redirects.push({
source: '/login', source: '/login',
destination: process.env.CLOUD_URL, destination: cloudUrl,
permanent: false, permanent: false,
}); });
} }
}
const basePath = process.env.BASE_PATH;
/** @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',
@ -121,12 +193,7 @@ const config = {
return config; return config;
}, },
async headers() { async headers() {
return [ return headers;
{
source: '/:path*',
headers,
},
];
}, },
async rewrites() { async rewrites() {
return [ return [
@ -135,6 +202,10 @@ const config = {
source: '/telemetry.js', source: '/telemetry.js',
destination: '/api/scripts/telemetry', destination: '/api/scripts/telemetry',
}, },
{
source: '/teams/:teamId/:path((?!settings).*)*',
destination: '/:path*',
},
]; ];
}, },
async redirects() { async redirects() {

View file

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

View file

@ -1,8 +1,8 @@
{ {
"name": "umami", "name": "umami",
"version": "2.9.0", "version": "2.15.1",
"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}": [
@ -60,17 +63,23 @@
"cacheDirectories": [ "cacheDirectories": [
".next/cache" ".next/cache"
], ],
"resolutions": {
"jackspeak": "2.1.1"
},
"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.22.0",
"@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.24.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,30 +87,30 @@
"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",
"moment-timezone": "^0.5.35", "md5": "^2.3.0",
"next": "14.0.4", "next": "15.0.4",
"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.22.0",
"react": "^18.2.0", "react": "^19.0.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": "^19.0.0",
"react-error-boundary": "^4.0.4", "react-error-boundary": "^4.0.4",
"react-intl": "^6.5.5", "react-intl": "^6.5.5",
"react-simple-maps": "^2.3.0", "react-simple-maps": "^2.3.0",
@ -110,22 +119,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.8.1",
"@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.14",
"@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 +142,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 +173,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"
} }
} }

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 806 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 420 B

After

Width:  |  Height:  |  Size: 420 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 635 B

After

Width:  |  Height:  |  Size: 635 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 819 B

After

Width:  |  Height:  |  Size: 819 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 680 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 819 B

After

Width:  |  Height:  |  Size: 819 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 811 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 811 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 835 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 835 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 426 B

After

Width:  |  Height:  |  Size: 426 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 794 B

After

Width:  |  Height:  |  Size: 794 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 829 B

After

Width:  |  Height:  |  Size: 829 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 535 B

After

Width:  |  Height:  |  Size: 535 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 106 B

After

Width:  |  Height:  |  Size: 106 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 794 B

After

Width:  |  Height:  |  Size: 794 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 235 B

After

Width:  |  Height:  |  Size: 235 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 122 B

After

Width:  |  Height:  |  Size: 122 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 296 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 281 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 183 B

After

Width:  |  Height:  |  Size: 183 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 110 B

After

Width:  |  Height:  |  Size: 110 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 201 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 186 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 141 B

After

Width:  |  Height:  |  Size: 141 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 237 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 102 B

After

Width:  |  Height:  |  Size: 102 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 211 B

After

Width:  |  Height:  |  Size: 211 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 139 B

After

Width:  |  Height:  |  Size: 139 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 163 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 150 B

After

Width:  |  Height:  |  Size: 150 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 129 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 105 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 140 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 97 B

After

Width:  |  Height:  |  Size: 97 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 153 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 107 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 353 B

After

Width:  |  Height:  |  Size: 353 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 286 B

After

Width:  |  Height:  |  Size: 286 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 350 B

After

Width:  |  Height:  |  Size: 350 B

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more