Merge branch 'dev' into search-formatted-metrics
28
.github/workflows/cd-cloud.yml
vendored
Normal 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 }}
|
19
.github/workflows/cd-manual.yml
vendored
@ -20,11 +20,26 @@ jobs:
|
||||
steps:
|
||||
- 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
|
||||
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
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 }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
@ -36,7 +51,7 @@ jobs:
|
||||
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
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 }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
14
.github/workflows/cd.yml
vendored
@ -17,14 +17,21 @@ jobs:
|
||||
|
||||
- name: Set env
|
||||
run: |
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate tags
|
||||
id: generate_tags
|
||||
run: |
|
||||
echo "tag_patch=$(echo ${{ matrix.db-type }})-${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
echo "tag_minor=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1,2)" >> $GITHUB_ENV
|
||||
echo "tag_major=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1)" >> $GITHUB_ENV
|
||||
echo "tag_latest=$(echo ${{ matrix.db-type }})-latest" >> $GITHUB_ENV
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
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 }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
@ -32,12 +39,11 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
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 }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
@ -37,7 +37,7 @@ RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN set -x \
|
||||
&& 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
|
||||
COPY --from=builder /app/next.config.js .
|
||||
|
112
README.md
@ -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
|
||||
|
||||
- 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 [MariaDB](https://www.mariadb.org/) (minimum v10.5), [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
|
||||
|
||||
### Install Yarn
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install -g yarn
|
||||
```
|
||||
|
||||
### Get the source code and install packages
|
||||
### Get the Source Code and Install Packages
|
||||
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/umami-software/umami.git
|
||||
cd umami
|
||||
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
|
||||
```
|
||||
|
||||
The connection url is in the following format:
|
||||
The connection URL format:
|
||||
|
||||
```
|
||||
```bash
|
||||
postgresql://username:mypassword@localhost:5432/mydb
|
||||
|
||||
mysql://username:mypassword@localhost:3306/mydb
|
||||
```
|
||||
|
||||
### Build the application
|
||||
### Build the Application
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
The build step will also create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**.
|
||||
*The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**.*
|
||||
|
||||
### Start the application
|
||||
### Start the Application
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
By default this will launch the application on `http://localhost:3000`. You will need to either
|
||||
[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server
|
||||
or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
||||
*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.*
|
||||
|
||||
## 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
|
||||
docker compose up -d
|
||||
@ -72,16 +96,18 @@ docker compose up -d
|
||||
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/umami-software/umami:postgresql-latest
|
||||
docker pull docker.umami.is/umami-software/umami:postgresql-latest
|
||||
```
|
||||
|
||||
Or with MySQL support:
|
||||
|
||||
```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:
|
||||
|
||||
@ -98,6 +124,36 @@ docker compose pull
|
||||
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
|
||||
|
77
db/clickhouse/migrations/04_add_tag.sql
Normal 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);
|
@ -26,11 +26,14 @@ CREATE TABLE umami.website_event
|
||||
--events
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
tag String,
|
||||
created_at DateTime('UTC'),
|
||||
job_id Nullable(UUID)
|
||||
)
|
||||
engine = MergeTree
|
||||
ORDER BY (website_id, session_id, created_at)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(created_at)
|
||||
ORDER BY (toStartOfHour(created_at), website_id, session_id, visit_id, created_at)
|
||||
PRIMARY KEY (toStartOfHour(created_at), website_id, session_id, visit_id)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
CREATE TABLE umami.event_data
|
||||
@ -48,7 +51,7 @@ CREATE TABLE umami.event_data
|
||||
created_at DateTime('UTC'),
|
||||
job_id Nullable(UUID)
|
||||
)
|
||||
engine = MergeTree
|
||||
ENGINE = MergeTree
|
||||
ORDER BY (website_id, event_id, data_key, created_at)
|
||||
SETTINGS index_granularity = 8192;
|
||||
|
||||
@ -64,6 +67,132 @@ CREATE TABLE umami.session_data
|
||||
created_at DateTime('UTC'),
|
||||
job_id Nullable(UUID)
|
||||
)
|
||||
engine = MergeTree
|
||||
ORDER BY (website_id, session_id, data_key, created_at)
|
||||
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;
|
||||
|
@ -11,7 +11,7 @@ CREATE TABLE `user` (
|
||||
UNIQUE INDEX `user_user_id_key`(`user_id`),
|
||||
UNIQUE INDEX `user_username_key`(`username`),
|
||||
PRIMARY KEY (`user_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `session` (
|
||||
@ -33,7 +33,7 @@ CREATE TABLE `session` (
|
||||
INDEX `session_created_at_idx`(`created_at`),
|
||||
INDEX `session_website_id_idx`(`website_id`),
|
||||
PRIMARY KEY (`session_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `website` (
|
||||
@ -53,7 +53,7 @@ CREATE TABLE `website` (
|
||||
INDEX `website_created_at_idx`(`created_at`),
|
||||
INDEX `website_share_id_idx`(`share_id`),
|
||||
PRIMARY KEY (`website_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
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_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
|
||||
PRIMARY KEY (`event_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
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_id_website_event_id_created_at_idx`(`website_id`, `website_event_id`, `created_at`),
|
||||
PRIMARY KEY (`event_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `team` (
|
||||
@ -109,7 +109,7 @@ CREATE TABLE `team` (
|
||||
UNIQUE INDEX `team_access_code_key`(`access_code`),
|
||||
INDEX `team_access_code_idx`(`access_code`),
|
||||
PRIMARY KEY (`team_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `team_user` (
|
||||
@ -124,7 +124,7 @@ CREATE TABLE `team_user` (
|
||||
INDEX `team_user_team_id_idx`(`team_id`),
|
||||
INDEX `team_user_user_id_idx`(`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
|
||||
CREATE TABLE `team_website` (
|
||||
@ -137,7 +137,7 @@ CREATE TABLE `team_website` (
|
||||
INDEX `team_website_team_id_idx`(`team_id`),
|
||||
INDEX `team_website_website_id_idx`(`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
|
||||
INSERT INTO user (user_id, username, role, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' , 'admin', 'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa');
|
@ -21,7 +21,7 @@ CREATE TABLE `session_data` (
|
||||
INDEX `session_data_website_id_idx`(`website_id`),
|
||||
INDEX `session_data_session_id_idx`(`session_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
|
||||
CREATE TABLE `report` (
|
||||
@ -41,7 +41,7 @@ CREATE TABLE `report` (
|
||||
INDEX `report_type_idx`(`type`),
|
||||
INDEX `report_name_idx`(`name`),
|
||||
PRIMARY KEY (`report_id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- EventData migration
|
||||
UPDATE event_data
|
||||
|
5
db/mysql/migrations/07_add_tag/migration.sql
Normal 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`);
|
@ -102,6 +102,7 @@ model WebsiteEvent {
|
||||
pageTitle String? @map("page_title") @db.VarChar(500)
|
||||
eventType Int @default(1) @map("event_type") @db.UnsignedInt
|
||||
eventName String? @map("event_name") @db.VarChar(50)
|
||||
tag String? @db.VarChar(50)
|
||||
|
||||
eventData EventData[]
|
||||
session Session @relation(fields: [sessionId], references: [id])
|
||||
@ -116,6 +117,7 @@ model WebsiteEvent {
|
||||
@@index([websiteId, createdAt, referrerDomain])
|
||||
@@index([websiteId, createdAt, pageTitle])
|
||||
@@index([websiteId, createdAt, eventName])
|
||||
@@index([websiteId, createdAt, tag])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@index([websiteId, visitId, createdAt])
|
||||
@@map("website_event")
|
||||
|
5
db/postgresql/migrations/07_add_tag/migration.sql
Normal 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");
|
@ -102,6 +102,7 @@ model WebsiteEvent {
|
||||
pageTitle String? @map("page_title") @db.VarChar(500)
|
||||
eventType Int @default(1) @map("event_type") @db.Integer
|
||||
eventName String? @map("event_name") @db.VarChar(50)
|
||||
tag String? @db.VarChar(50)
|
||||
|
||||
eventData EventData[]
|
||||
session Session @relation(fields: [sessionId], references: [id])
|
||||
@ -111,11 +112,13 @@ model WebsiteEvent {
|
||||
@@index([visitId])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, createdAt])
|
||||
|
||||
@@index([websiteId, createdAt, urlPath])
|
||||
@@index([websiteId, createdAt, urlQuery])
|
||||
@@index([websiteId, createdAt, referrerDomain])
|
||||
@@index([websiteId, createdAt, pageTitle])
|
||||
@@index([websiteId, createdAt, eventName])
|
||||
@@index([websiteId, createdAt, tag])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@index([websiteId, visitId, createdAt])
|
||||
@@map("website_event")
|
||||
|
2
next-env.d.ts
vendored
@ -3,4 +3,4 @@
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// 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.
|
||||
|
107
next.config.js
@ -3,29 +3,31 @@ require('dotenv').config();
|
||||
const path = require('path');
|
||||
const pkg = require('./package.json');
|
||||
|
||||
const basePath = process.env.BASE_PATH || '';
|
||||
const forceSSL = process.env.FORCE_SSL || '';
|
||||
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
||||
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
||||
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
|
||||
const cloudMode = process.env.CLOUD_MODE || '';
|
||||
const cloudUrl = process.env.CLOUD_URL || '';
|
||||
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||
const disableLogin = process.env.DISABLE_LOGIN || '';
|
||||
const disableUI = process.env.DISABLE_UI || '';
|
||||
const hostURL = process.env.HOST_URL || '';
|
||||
const privateMode = process.env.PRIVATE_MODE || '';
|
||||
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 = [
|
||||
`default-src 'self'`,
|
||||
`img-src *`,
|
||||
`img-src * data:`,
|
||||
`script-src 'self' 'unsafe-eval' 'unsafe-inline'`,
|
||||
`style-src 'self' 'unsafe-inline'`,
|
||||
`connect-src 'self' api.umami.is cloud.umami.is`,
|
||||
`frame-ancestors 'self' ${frameAncestors}`,
|
||||
];
|
||||
|
||||
const headers = [
|
||||
const defaultHeaders = [
|
||||
{
|
||||
key: 'X-DNS-Prefetch-Control',
|
||||
value: 'on',
|
||||
@ -40,14 +42,43 @@ const headers = [
|
||||
];
|
||||
|
||||
if (forceSSL) {
|
||||
headers.push({
|
||||
defaultHeaders.push({
|
||||
key: 'Strict-Transport-Security',
|
||||
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 = [];
|
||||
|
||||
if (trackerScriptURL) {
|
||||
rewrites.push({
|
||||
source: TRACKER_SCRIPT,
|
||||
destination: trackerScriptURL,
|
||||
});
|
||||
}
|
||||
|
||||
if (collectApiEndpoint) {
|
||||
rewrites.push({
|
||||
source: collectApiEndpoint,
|
||||
@ -55,19 +86,6 @@ if (collectApiEndpoint) {
|
||||
});
|
||||
}
|
||||
|
||||
if (trackerScriptName) {
|
||||
const names = trackerScriptName?.split(',').map(name => name.trim());
|
||||
|
||||
if (names) {
|
||||
names.forEach(name => {
|
||||
rewrites.push({
|
||||
source: `/${name.replace(/^\/+/, '')}`,
|
||||
destination: '/script.js',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const redirects = [
|
||||
{
|
||||
source: '/settings',
|
||||
@ -86,6 +104,27 @@ const redirects = [
|
||||
},
|
||||
];
|
||||
|
||||
// Adding rewrites + headers for all alternative tracker script names.
|
||||
if (trackerScriptName) {
|
||||
const names = trackerScriptName?.split(',').map(name => name.trim());
|
||||
|
||||
if (names) {
|
||||
names.forEach(name => {
|
||||
const normalizedSource = `/${name.replace(/^\/+/, '')}`;
|
||||
|
||||
rewrites.push({
|
||||
source: normalizedSource,
|
||||
destination: TRACKER_SCRIPT,
|
||||
});
|
||||
|
||||
headers.push({
|
||||
source: normalizedSource,
|
||||
headers: trackerHeaders,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (cloudMode && cloudUrl) {
|
||||
redirects.push({
|
||||
source: '/settings/:path*',
|
||||
@ -120,7 +159,6 @@ const config = {
|
||||
defaultLocale,
|
||||
disableLogin,
|
||||
disableUI,
|
||||
hostURL,
|
||||
privateMode,
|
||||
},
|
||||
basePath,
|
||||
@ -155,12 +193,7 @@ const config = {
|
||||
return config;
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers,
|
||||
},
|
||||
];
|
||||
return headers;
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
@ -169,6 +202,10 @@ const config = {
|
||||
source: '/telemetry.js',
|
||||
destination: '/api/scripts/telemetry',
|
||||
},
|
||||
{
|
||||
source: '/teams/:teamId/:path((?!settings).*)*',
|
||||
destination: '/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
|
@ -11,6 +11,7 @@
|
||||
"@tanstack/react-query": "^4.33.0",
|
||||
"classnames": "^2.3.1",
|
||||
"colord": "^2.9.2",
|
||||
"date-fns-tz": "^1.1.4",
|
||||
"immer": "^9.0.12",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "^13.4.0",
|
||||
|
30
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "2.12.0",
|
||||
"version": "2.15.0",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||
"license": "MIT",
|
||||
@ -63,15 +63,21 @@
|
||||
"cacheDirectories": [
|
||||
".next/cache"
|
||||
],
|
||||
"resolutions": {
|
||||
"jackspeak": "2.1.1"
|
||||
},
|
||||
"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",
|
||||
"@prisma/client": "5.12.1",
|
||||
"@prisma/client": "5.22.0",
|
||||
"@prisma/extension-read-replicas": "^0.3.0",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@umami/prisma-client": "^0.14.0",
|
||||
"@umami/redis-client": "^0.18.0",
|
||||
"@umami/redis-client": "^0.21.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^4.4.2",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
@ -81,7 +87,6 @@
|
||||
"cross-spawn": "^7.0.3",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.1.4",
|
||||
"dateformat": "^5.0.3",
|
||||
"debug": "^4.3.4",
|
||||
"del": "^6.0.0",
|
||||
"detect-browser": "^5.2.0",
|
||||
@ -93,18 +98,17 @@
|
||||
"is-ci": "^3.0.1",
|
||||
"is-docker": "^3.0.0",
|
||||
"is-localhost-ip": "^1.4.0",
|
||||
"isbot": "^5.1.1",
|
||||
"isbot": "^5.1.16",
|
||||
"kafkajs": "^2.1.0",
|
||||
"maxmind": "^4.3.6",
|
||||
"md5": "^2.3.0",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "14.1.4",
|
||||
"next": "15.0.3",
|
||||
"next-basics": "^0.39.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "5.12.1",
|
||||
"prisma": "5.22.0",
|
||||
"react": "^18.2.0",
|
||||
"react-basics": "^0.123.0",
|
||||
"react-basics": "^0.125.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
@ -117,11 +121,11 @@
|
||||
"thenby": "^1.3.4",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11",
|
||||
"zustand": "^4.3.8"
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^4.2.29",
|
||||
"@netlify/plugin-nextjs": "^4.41.3",
|
||||
"@netlify/plugin-nextjs": "^5.8.1",
|
||||
"@rollup/plugin-alias": "^5.0.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.4",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
@ -175,6 +179,6 @@
|
||||
"tar": "^6.1.2",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.4.3"
|
||||
"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.0 KiB After Width: | Height: | Size: 1.0 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.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 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.0 KiB After Width: | Height: | Size: 2.0 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 |
Before Width: | Height: | Size: 211 B After Width: | Height: | Size: 211 B |
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
Before Width: | Height: | Size: 206 B After Width: | Height: | Size: 206 B |
Before Width: | Height: | Size: 164 B After Width: | Height: | Size: 164 B |
Before Width: | Height: | Size: 129 B After Width: | Height: | Size: 129 B |
Before Width: | Height: | Size: 114 B After Width: | Height: | Size: 114 B |
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 230 B |
Before Width: | Height: | Size: 144 B After Width: | Height: | Size: 144 B |
Before Width: | Height: | Size: 135 B After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 144 B After Width: | Height: | Size: 144 B |
Before Width: | Height: | Size: 112 B After Width: | Height: | Size: 112 B |
Before Width: | Height: | Size: 142 B After Width: | Height: | Size: 142 B |
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 163 B |
Before Width: | Height: | Size: 146 B After Width: | Height: | Size: 146 B |