Merge branch 'dev' into fix/pages-url-query

This commit is contained in:
Francis Cao authored and GitHub committed 2026-05-07 16:15:46 -07:00
commit 73c045118b
789 files changed
+53871 -147160

No files matched your search

+5 -5
View File
@@ -24,13 +24,13 @@ body:
render: shell
- type: input
attributes:
label: Which Umami version are you using? (if relevant)
label: Which Umami version are you using?
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
- type: input
attributes:
label: Which browser are you using? (if relevant)
description: 'For example: Chrome, Edge, Firefox, etc'
label: How are you deploying your application?
description: 'For example: Vercel, Railway, Docker, etc'
- type: input
attributes:
label: How are you deploying your application? (if relevant)
description: 'For example: Vercel, Railway, Docker, etc'
label: Which browser are you using?
description: 'For example: Chrome, Edge, Firefox, etc'
+1 -1
View File
@@ -124,4 +124,4 @@ jobs:
push: true
tags: ${{ steps.compute.outputs.docker_tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: type=gha,mode=max
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
with:
version: 10
run_install: false
- name: Use Node.js 18.18
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 18.18
node-version: 22
cache: "pnpm"
- run: npm install --global pnpm
- run: pnpm install
+8 -1
View File
@@ -11,16 +11,19 @@ package-lock.json
/coverage
# next.js
next-env.d.ts
/.next
/out
# production
/build
/public/script.js
/public/recorder.js
/geo
/dist
/generated
/src/generated
pm2.yml
# misc
.DS_Store
@@ -30,6 +33,11 @@ package-lock.json
*.log
.vscode
.tool-versions
.claude
.agents
tmpclaude*
CLAUDE.md
nul
# debug
npm-debug.log*
@@ -42,4 +50,3 @@ yarn-error.log*
*.env.*
*.dev.yml
-1
View File
@@ -1 +0,0 @@
npx lint-staged
-6
View File
@@ -1,6 +0,0 @@
{
"extends": ["stylelint-config-recommended", "stylelint-config-css-modules"],
"rules": {
"no-descending-specificity": null
}
}
+46
View File
@@ -0,0 +1,46 @@
# Contributing to Umami
Thanks for your interest in contributing to Umami! This document outlines the process for contributing code.
## Branching
Umami uses the following long-lived branches:
- `master` — stable, released code. **Do not open PRs against `master`.**
- `dev` — active development. **All pull requests should target `dev`.**
Feature branches and fixes are merged into `dev`, and `dev` is periodically merged into `master` for releases.
## Submitting a Pull Request
1. Fork the repository and create your branch from `dev`:
```bash
git checkout dev
git pull origin dev
git checkout -b my-feature
```
2. Make your changes. Keep PRs focused — one logical change per PR.
3. Ensure the project builds and lints cleanly:
```bash
pnpm install
pnpm build
pnpm lint
```
4. Push your branch and open a pull request **against the `dev` branch**.
5. Fill in the PR description with what changed and why. Link any related issues.
PRs opened against `master` will be asked to retarget `dev`.
## Reporting Issues
- Search [existing issues](https://github.com/umami-software/umami/issues) before opening a new one.
- For bugs, include reproduction steps, expected vs. actual behavior, and your environment (Umami version, database, browser).
- For feature requests, describe the use case before the proposed solution.
## Development Setup
See the [README](./README.md) for instructions on installing dependencies, configuring the database, and running Umami locally.
## License
By contributing, you agree that your contributions will be licensed under the [MIT License](./LICENSE).
+5 -3
View File
@@ -14,7 +14,7 @@ FROM node:${NODE_IMAGE_VERSION} AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
COPY docker/middleware.ts ./src
COPY docker/proxy.ts ./src
ARG BASE_PATH
@@ -28,7 +28,7 @@ RUN npm run build-docker
FROM node:${NODE_IMAGE_VERSION} AS runner
WORKDIR /app
ARG PRISMA_VERSION="6.19.0"
ARG PRISMA_VERSION="7.3.0"
ARG NODE_OPTIONS
ENV NODE_ENV=production
@@ -44,10 +44,12 @@ RUN set -x \
# Script dependencies
RUN pnpm --allow-build='@prisma/engines' add npm-run-all dotenv chalk semver \
prisma@${PRISMA_VERSION} \
@prisma/client@${PRISMA_VERSION} \
@prisma/adapter-pg@${PRISMA_VERSION}
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/generated ./generated
@@ -63,4 +65,4 @@ EXPOSE 3000
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
CMD ["pnpm", "start-docker"]
CMD ["pnpm", "start-docker"]
@@ -0,0 +1,105 @@
-- Add performance columns to website_event
ALTER TABLE umami.website_event ADD COLUMN lcp Nullable(Decimal(10, 1)) AFTER twclid;
ALTER TABLE umami.website_event ADD COLUMN inp Nullable(Decimal(10, 1)) AFTER lcp;
ALTER TABLE umami.website_event ADD COLUMN cls Nullable(Decimal(10, 4)) AFTER inp;
ALTER TABLE umami.website_event ADD COLUMN fcp Nullable(Decimal(10, 1)) AFTER cls;
ALTER TABLE umami.website_event ADD COLUMN ttfb Nullable(Decimal(10, 1)) AFTER fcp;
-- Update materialized view to exclude performance events from view counts
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,
hostnames as hostname,
browser,
os,
device,
screen,
language,
country,
region,
city,
entry_url,
exit_url,
url_paths as url_path,
url_query,
utm_source,
utm_medium,
utm_campaign,
utm_content,
utm_term,
referrer_domain,
page_title,
gclid,
fbclid,
msclkid,
ttclid,
li_fat_id,
twclid,
event_type,
event_name,
views,
min_time,
max_time,
tag,
distinct_id,
timestamp as created_at
FROM (SELECT
website_id,
session_id,
visit_id,
arrayFilter(x -> x != '', groupArray(hostname)) hostnames,
browser,
os,
device,
screen,
language,
country,
region,
city,
argMinState(url_path, created_at) entry_url,
argMaxState(url_path, created_at) exit_url,
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
arrayFilter(x -> x != '', groupArray(utm_source)) utm_source,
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
arrayFilter(x -> x != '' and x != hostname, groupArray(referrer_domain)) referrer_domain,
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type NOT IN (2, 5)) views,
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
distinct_id,
toStartOfHour(created_at) timestamp
FROM umami.website_event
GROUP BY website_id,
session_id,
visit_id,
hostname,
browser,
os,
device,
screen,
language,
country,
region,
city,
event_type,
distinct_id,
timestamp);
@@ -0,0 +1,18 @@
-- Create session_replay
CREATE TABLE umami.session_replay
(
replay_id UUID,
website_id UUID,
session_id UUID,
visit_id UUID,
chunk_index UInt32,
events String CODEC(ZSTD(3)),
event_count UInt32,
started_at DateTime64(6),
ended_at DateTime64(6),
created_at DateTime64(6) DEFAULT now64(6)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (replay_id, website_id, session_id, visit_id, chunk_index)
SETTINGS index_granularity = 8192;
@@ -0,0 +1,75 @@
CREATE TABLE IF NOT EXISTS umami.event_data_pivot
(
website_id UUID,
session_id UUID,
event_id UUID,
event_name LowCardinality(String),
url_path String,
created_at DateTime('UTC'),
property_keys AggregateFunction(groupArray, String),
property_values AggregateFunction(groupArray, String),
property_types AggregateFunction(groupArray, UInt32)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (website_id, event_name, created_at, event_id)
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW IF NOT EXISTS umami.event_data_pivot_mv
TO umami.event_data_pivot
AS SELECT
website_id,
session_id,
event_id,
event_name,
url_path,
created_at,
groupArrayState(data_key) AS property_keys,
groupArrayState(multiIf(
data_type IN (1, 3, 5), ifNull(string_value, ''),
data_type = 2, toString(ifNull(number_value, 0)),
data_type = 4, toString(ifNull(date_value, toDateTime(0))),
''
)) AS property_values,
groupArrayState(data_type) AS property_types
FROM umami.event_data
GROUP BY website_id, session_id, event_id, event_name, url_path, created_at;
-- Backfill existing event data
INSERT INTO umami.event_data_pivot
SELECT
website_id,
session_id,
event_id,
event_name,
url_path,
created_at,
groupArrayState(data_key),
groupArrayState(multiIf(
data_type IN (1, 3, 5), ifNull(string_value, ''),
data_type = 2, toString(ifNull(number_value, 0)),
data_type = 4, toString(ifNull(date_value, toDateTime(0))),
''
)),
groupArrayState(data_type)
FROM umami.event_data
GROUP BY website_id, session_id, event_id, event_name, url_path, created_at;
ALTER TABLE umami.session_data
MODIFY SETTING deduplicate_merge_projection_mode = 'drop';
ALTER TABLE umami.session_data
ADD PROJECTION session_data_property_filter_projection (
SELECT *
ORDER BY (
website_id,
data_key,
data_type,
string_value,
number_value,
date_value,
session_id
)
);
ALTER TABLE umami.session_data MATERIALIZE PROJECTION session_data_property_filter_projection;
@@ -0,0 +1,22 @@
-- Create heatmap_event
CREATE TABLE umami.heatmap_event
(
heatmap_event_id UUID,
website_id UUID,
session_id UUID,
visit_id UUID,
url_path String,
event_type UInt8,
node_id Nullable(Int32),
x Nullable(Int32),
y Nullable(Int32),
viewport_w Nullable(Int32),
viewport_h Nullable(Int32),
page_h Nullable(Int32),
scroll_pct Nullable(UInt8),
created_at DateTime('UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (website_id, url_path, event_type, created_at)
SETTINGS index_granularity = 8192;
+142 -1
View File
@@ -34,6 +34,12 @@ CREATE TABLE umami.website_event
ttclid String,
li_fat_id String,
twclid String,
--performance
lcp Nullable(Decimal(10, 1)),
inp Nullable(Decimal(10, 1)),
cls Nullable(Decimal(10, 4)),
fcp Nullable(Decimal(10, 1)),
ttfb Nullable(Decimal(10, 1)),
--events
event_type UInt32,
event_name String,
@@ -84,6 +90,25 @@ ENGINE = ReplacingMergeTree
ORDER BY (website_id, session_id, data_key)
SETTINGS index_granularity = 8192;
ALTER TABLE umami.session_data
MODIFY SETTING deduplicate_merge_projection_mode = 'drop';
ALTER TABLE umami.session_data
ADD PROJECTION session_data_property_filter_projection (
SELECT *
ORDER BY (
website_id,
data_key,
data_type,
string_value,
number_value,
date_value,
session_id
)
);
ALTER TABLE umami.session_data MATERIALIZE PROJECTION session_data_property_filter_projection;
-- stats hourly
CREATE TABLE umami.website_event_stats_hourly
(
@@ -209,7 +234,7 @@ FROM (SELECT
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
event_type,
if(event_type = 2, groupArray(event_name), []) event_name,
sumIf(1, event_type != 2) views,
sumIf(1, event_type NOT IN (2, 5)) views,
min(created_at) min_time,
max(created_at) max_time,
arrayFilter(x -> x != '', groupArray(tag)) tag,
@@ -281,3 +306,119 @@ JOIN (SELECT event_id, string_value as currency
WHERE positionCaseInsensitive(data_key, 'currency') > 0) c
ON c.event_id = ed.event_id
WHERE positionCaseInsensitive(data_key, 'revenue') > 0;
-- Create session_replay
CREATE TABLE umami.session_replay
(
replay_id UUID,
website_id UUID,
session_id UUID,
visit_id UUID,
chunk_index UInt32,
events String CODEC(ZSTD(3)),
event_count UInt32,
started_at DateTime64(6),
ended_at DateTime64(6),
created_at DateTime64(6) DEFAULT now64(6)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (replay_id, website_id, session_id, visit_id, chunk_index)
SETTINGS index_granularity = 8192;
-- Create event_data_pivot
CREATE TABLE IF NOT EXISTS umami.event_data_pivot
(
website_id UUID,
session_id UUID,
event_id UUID,
event_name LowCardinality(String),
url_path String,
created_at DateTime('UTC'),
property_keys AggregateFunction(groupArray, String),
property_values AggregateFunction(groupArray, String),
property_types AggregateFunction(groupArray, UInt32)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (website_id, event_name, created_at, event_id)
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW IF NOT EXISTS umami.event_data_pivot_mv
TO umami.event_data_pivot
AS SELECT
website_id,
session_id,
event_id,
event_name,
url_path,
created_at,
groupArrayState(data_key) AS property_keys,
groupArrayState(multiIf(
data_type IN (1, 3, 5), ifNull(string_value, ''),
data_type = 2, toString(ifNull(number_value, 0)),
data_type = 4, toString(ifNull(date_value, toDateTime(0))),
''
)) AS property_values,
groupArrayState(data_type) AS property_types
FROM umami.event_data
GROUP BY website_id, session_id, event_id, event_name, url_path, created_at;
-- Create session_data_pivot
CREATE TABLE IF NOT EXISTS umami.session_data_pivot
(
website_id UUID,
session_id UUID,
distinct_id String,
created_year_month UInt32,
created_at AggregateFunction(max, DateTime('UTC')),
property_keys AggregateFunction(groupArray, String),
property_values AggregateFunction(groupArray, String),
property_types AggregateFunction(groupArray, UInt32)
)
ENGINE = AggregatingMergeTree()
PARTITION BY created_year_month
ORDER BY (website_id, session_id, distinct_id)
SETTINGS index_granularity = 8192;
CREATE MATERIALIZED VIEW IF NOT EXISTS umami.session_data_pivot_mv
TO umami.session_data_pivot
AS SELECT
website_id,
session_id,
ifNull(distinct_id, '') AS distinct_id,
toYYYYMM(max(session_data.created_at)) AS created_year_month,
maxState(session_data.created_at) AS created_at,
groupArrayState(data_key) AS property_keys,
groupArrayState(multiIf(
data_type IN (1, 3, 5), ifNull(string_value, ''),
data_type = 2, toString(ifNull(number_value, 0)),
data_type = 4, toString(ifNull(date_value, toDateTime(0))),
''
)) AS property_values,
groupArrayState(data_type) AS property_types
FROM umami.session_data
GROUP BY website_id, session_id, distinct_id;
-- Create heatmap_event
CREATE TABLE umami.heatmap_event
(
heatmap_event_id UUID,
website_id UUID,
session_id UUID,
visit_id UUID,
url_path String,
event_type UInt8,
node_id Nullable(Int32),
x Nullable(Int32),
y Nullable(Int32),
viewport_w Nullable(Int32),
viewport_h Nullable(Int32),
page_h Nullable(Int32),
scroll_pct Nullable(UInt8),
created_at DateTime('UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(created_at)
ORDER BY (website_id, url_path, event_type, created_at)
SETTINGS index_granularity = 8192;
+6 -4
View File
@@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from 'next/server';
import { matchesConfiguredPath } from '@/lib/match-configured-path';
export const config = {
matcher: '/:path*',
@@ -7,6 +8,7 @@ export const config = {
const TRACKER_PATH = '/script.js';
const COLLECT_PATH = '/api/send';
const LOGIN_PATH = '/login';
const BASE_PATH = process.env.BASE_PATH || '';
const apiHeaders = {
'Access-Control-Allow-Origin': '*',
@@ -27,7 +29,7 @@ function customCollectEndpoint(request: NextRequest) {
if (collectEndpoint) {
const url = request.nextUrl.clone();
if (url.pathname.endsWith(collectEndpoint)) {
if (matchesConfiguredPath(url.pathname, collectEndpoint, BASE_PATH)) {
url.pathname = COLLECT_PATH;
return NextResponse.rewrite(url, { headers: apiHeaders });
}
@@ -41,7 +43,7 @@ function customScriptName(request: NextRequest) {
const url = request.nextUrl.clone();
const names = scriptName.split(',').map(name => name.trim().replace(/^\/+/, ''));
if (names.find(name => url.pathname.endsWith(name))) {
if (names.find(name => matchesConfiguredPath(url.pathname, name, BASE_PATH))) {
url.pathname = TRACKER_PATH;
return NextResponse.rewrite(url, { headers: trackerHeaders });
}
@@ -51,7 +53,7 @@ function customScriptName(request: NextRequest) {
function customScriptUrl(request: NextRequest) {
const scriptUrl = process.env.TRACKER_SCRIPT_URL;
if (scriptUrl && request.nextUrl.pathname.endsWith(TRACKER_PATH)) {
if (scriptUrl && matchesConfiguredPath(request.nextUrl.pathname, TRACKER_PATH, BASE_PATH)) {
return NextResponse.rewrite(scriptUrl, { headers: trackerHeaders });
}
}
@@ -59,7 +61,7 @@ function customScriptUrl(request: NextRequest) {
function disableLogin(request: NextRequest) {
const loginDisabled = process.env.DISABLE_LOGIN;
if (loginDisabled && request.nextUrl.pathname.endsWith(LOGIN_PATH)) {
if (loginDisabled && matchesConfiguredPath(request.nextUrl.pathname, LOGIN_PATH, BASE_PATH)) {
return new NextResponse('Access denied', { status: 403 });
}
}
-6
View File
@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+32 -9
View File
@@ -1,18 +1,26 @@
import 'dotenv/config';
import createNextIntlPlugin from 'next-intl/plugin';
import pkg from './package.json' with { type: 'json' };
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const TRACKER_SCRIPT = '/script.js';
const isProd = process.env.NODE_ENV === 'production';
const basePath = process.env.BASE_PATH || '';
const cloudMode = process.env.CLOUD_MODE || '';
const cloudUrl = process.env.CLOUD_URL || '';
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
const corsMaxAge = process.env.CORS_MAX_AGE || '';
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
const defaultLocale = process.env.DEFAULT_LOCALE || '';
const forceSSL = process.env.FORCE_SSL || '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
const selfTrack = process.env.UMAMI_SELF_TRACK || '';
const selfRecord = process.env.UMAMI_SELF_RECORD || '';
const contentSecurityPolicy = `
default-src 'self';
@@ -20,6 +28,7 @@ const contentSecurityPolicy = `
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' https:;
frame-src 'self' http: https:;
frame-ancestors 'self' ${frameAncestors};
`;
@@ -84,11 +93,14 @@ const headers = [
source: '/:path*',
headers: defaultHeaders,
},
{
];
if (isProd) {
headers.push({
source: TRACKER_SCRIPT,
headers: trackerHeaders,
},
];
});
}
const rewrites = [];
@@ -112,6 +124,16 @@ if (collectApiEndpoint) {
}
const redirects = [
{
source: '/teams/:id/dashboard/edit',
destination: '/dashboard/edit',
permanent: false,
},
{
source: '/teams/:id/dashboard',
destination: '/dashboard',
permanent: false,
},
{
source: '/settings',
destination: '/settings/preferences',
@@ -155,7 +177,7 @@ if (trackerScriptName) {
}
}
if (cloudMode) {
if (isProd && cloudMode) {
rewrites.push({
source: '/script.js',
destination: 'https://cloud.umami.is/script.js',
@@ -163,23 +185,24 @@ if (cloudMode) {
}
/** @type {import('next').NextConfig} */
export default {
export default withNextIntl({
reactStrictMode: false,
env: {
basePath,
cloudMode,
cloudUrl,
currentVersion: pkg.version,
defaultCurrency,
defaultLocale,
selfTrack,
selfRecord,
},
basePath,
output: 'standalone',
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
devIndicators: false,
async headers() {
return headers;
},
@@ -199,4 +222,4 @@ export default {
async redirects() {
return [...redirects];
},
};
});
+79 -93
View File
@@ -1,6 +1,6 @@
{
"name": "umami",
"version": "3.0.3",
"version": "3.1.0",
"description": "A modern, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT",
@@ -11,19 +11,20 @@
},
"type": "module",
"scripts": {
"dev": "next dev -p 3001 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"dev": "dotenv next dev --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-recorder build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
"build-docker": "npm-run-all build-db build-tracker build-recorder build-geo build-app",
"start-docker": "npm-run-all check-db update-tracker start-server",
"start-env": "node scripts/start-env.js",
"start-server": "node server.js",
"build-app": "next build --turbo",
"build-icons": "svgr ./src/assets --out-dir src/components/svg --typescript",
"build-components": "tsup",
"build-components": "node scripts/bump-components.js && tsup",
"build-tracker": "rollup -c rollup.tracker.config.js",
"build-recorder": "rollup -c rollup.recorder.config.js",
"build-prisma-client": "node scripts/build-prisma-client.js",
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names clean-lang",
"build-lang": "npm-run-all download-country-names download-language-names",
"build-geo": "node scripts/build-geo.js",
"build-db": "npm-run-all build-db-client build-prisma-client",
"build-db-schema": "prisma db pull",
@@ -32,17 +33,11 @@
"update-db": "prisma migrate deploy",
"check-db": "node scripts/check-db.js",
"check-env": "node scripts/check-env.js",
"check-missing-messages": "node scripts/check-missing-messages.js",
"copy-db-files": "node scripts/copy-db-files.js",
"extract-messages": "formatjs extract \"src/components/messages.ts\" --out-file build/extracted-messages.json",
"merge-messages": "node scripts/merge-messages.js",
"generate-lang": "npm-run-all extract-messages merge-messages",
"format-lang": "node scripts/format-lang.js",
"compile-lang": "formatjs compile-folder --ast build/messages public/intl/messages",
"clean-lang": "prettier --write ./public/intl/**/*.json",
"download-country-names": "node scripts/download-country-names.js",
"download-language-names": "node scripts/download-language-names.js",
"change-password": "node scripts/change-password.js",
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install",
"postbuild": "node scripts/postbuild.js",
"test": "jest",
"cypress-open": "cypress open cypress run",
@@ -52,123 +47,114 @@
"format": "biome format --write .",
"check": "biome check --write"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json,css}": [
"biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
]
},
"cacheDirectories": [
".next/cache"
],
"dependencies": {
"@clickhouse/client": "^1.12.0",
"@date-fns/utc": "^1.2.0",
"@dicebear/collection": "^9.2.3",
"@dicebear/core": "^9.2.3",
"@fontsource/inter": "^5.2.8",
"@hello-pangea/dnd": "^17.0.0",
"@prisma/adapter-pg": "^6.18.0",
"@prisma/client": "^6.18.0",
"@prisma/extension-read-replicas": "^0.4.1",
"@react-spring/web": "^10.0.3",
"@clickhouse/client": "^1.18.2",
"@date-fns/utc": "^2.1.1",
"@dicebear/collection": "^9.4.2",
"@dicebear/core": "^9.4.2",
"@hello-pangea/dnd": "^18.0.1",
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"@prisma/extension-read-replicas": "^0.5.0",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.90.11",
"@umami/react-zen": "^0.211.0",
"@umami/redis-client": "^0.29.0",
"@tanstack/react-query": "^5.99.0",
"@umami/react-zen": "^0.245.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.2",
"chart.js": "^4.5.1",
"chartjs-adapter-date-fns": "^3.0.0",
"classnames": "^2.3.1",
"colord": "^2.9.2",
"cors": "^2.8.5",
"cors": "^2.8.6",
"cross-spawn": "^7.0.3",
"date-fns": "^2.23.0",
"date-fns-tz": "^1.1.4",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"debug": "^4.4.3",
"del": "^6.0.0",
"del": "^8.0.1",
"detect-browser": "^5.2.0",
"dotenv": "^17.2.3",
"esbuild": "^0.25.11",
"fs-extra": "^11.3.2",
"immer": "^10.2.0",
"dotenv": "^17.4.2",
"esbuild": "^0.28.0",
"immer": "^11.1.4",
"ipaddr.js": "^2.3.0",
"is-ci": "^3.0.1",
"is-docker": "^3.0.0",
"is-localhost-ip": "^2.0.0",
"isbot": "^5.1.31",
"jsonwebtoken": "^9.0.2",
"is-ci": "^4.1.0",
"is-docker": "^4.0.0",
"is-localhost-ip": "^3.0.1",
"isbot": "^5.1.39",
"jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
"kafkajs": "^2.1.0",
"lucide-react": "^0.543.0",
"maxmind": "^5.0.0",
"next": "^15.5.9",
"lucide-react": "^1.8.0",
"maxmind": "^5.0.6",
"motion": "^12.38.0",
"next": "16.2.4",
"next-intl": "4.9.1",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"papaparse": "^5.5.3",
"pg": "^8.16.3",
"prisma": "^6.18.0",
"pure-rand": "^7.0.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-error-boundary": "^4.0.4",
"react-intl": "^7.1.14",
"react-simple-maps": "^2.3.0",
"pg": "^8.20.0",
"prisma": "^7.7.0",
"prop-types": "^15.8.1",
"pure-rand": "^8.4.0",
"react": "^19.2.5",
"react-aria-components": "^1.17.0",
"react-dom": "^19.2.5",
"react-error-boundary": "^6.1.1",
"react-resizable-panels": "^4.10.0",
"react-simple-maps": "^3.0.0",
"react-use-measure": "^2.0.4",
"react-window": "^1.8.6",
"react-window": "^2.2.7",
"redis": "^5.12.1",
"request-ip": "^3.3.0",
"semver": "^7.7.3",
"serialize-error": "^12.0.0",
"thenby": "^1.3.4",
"ua-parser-js": "^2.0.6",
"uuid": "^11.1.0",
"zod": "^4.1.13",
"zustand": "^5.0.9"
"rrweb": "2.0.0-alpha.4",
"rrweb-player": "1.0.0-alpha.4",
"semver": "^7.7.4",
"serialize-error": "^13.0.1",
"thenby": "^1.4.0",
"ua-parser-js": "^2.0.9",
"uuid": "^14.0.0",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^5.15.1",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4",
"@biomejs/biome": "^2.4.12",
"@netlify/plugin-nextjs": "^5.15.9",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.0",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.9.2",
"@types/react": "^19.2.7",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.2",
"@types/react-window": "^1.8.8",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cross-env": "^10.1.0",
"cypress": "^13.6.6",
"extract-react-intl-messages": "^4.1.1",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^16.2.6",
"postcss": "^8.5.6",
"cypress": "^15.14.0",
"dotenv-cli": "^11.0.0",
"jest": "^30.3.0",
"postcss": "^8.5.10",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^15.1.0",
"postcss-preset-env": "7.8.3",
"postcss-import": "^16.1.1",
"postcss-preset-env": "11.2.1",
"prompts": "2.4.2",
"rollup": "^4.52.5",
"rollup": "^4.60.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^3.0.1",
"rollup-plugin-dts": "^6.3.0",
"rollup-plugin-node-externals": "^8.1.1",
"rollup-plugin-delete": "^3.0.2",
"rollup-plugin-dts": "^6.4.1",
"rollup-plugin-node-externals": "^9.0.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"stylelint": "^15.10.1",
"stylelint-config-css-modules": "^4.5.1",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^14.0.0",
"tar": "^6.1.2",
"ts-jest": "^29.4.6",
"tar": "^7.5.13",
"ts-jest": "^29.4.9",
"ts-morph": "^28.0.0",
"ts-node": "^10.9.1",
"tsup": "^8.5.0",
"tsx": "^4.19.0",
"typescript": "^5.9.3"
"typescript": "^6.0.3"
}
}
+4771 -6318
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,40 @@
-- CreateTable
CREATE TABLE "share" (
"share_id" UUID NOT NULL,
"entity_id" UUID NOT NULL,
"name" VARCHAR(200) NOT NULL,
"share_type" INTEGER NOT NULL,
"slug" VARCHAR(100) NOT NULL,
"parameters" JSONB NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6),
CONSTRAINT "share_pkey" PRIMARY KEY ("share_id")
);
-- CreateIndex
CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
-- CreateIndex
CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
-- MigrateData
INSERT INTO "share" (share_id, entity_id, name, share_type, slug, parameters, created_at)
SELECT gen_random_uuid(),
website_id,
name,
1,
share_id,
'{"overview":true}'::jsonb,
now()
FROM "website"
WHERE share_id IS NOT NULL;
-- DropIndex
DROP INDEX "website_share_id_idx";
-- DropIndex
DROP INDEX "website_share_id_key";
-- AlterTable
ALTER TABLE "website" DROP COLUMN "share_id";
+30
View File
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "board" (
"board_id" UUID NOT NULL,
"type" VARCHAR(50) NOT NULL,
"name" VARCHAR(200) NOT NULL,
"description" VARCHAR(500) NOT NULL,
"parameters" JSONB NOT NULL,
"slug" VARCHAR(100) NOT NULL,
"user_id" UUID,
"team_id" UUID,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6),
CONSTRAINT "board_pkey" PRIMARY KEY ("board_id")
);
-- CreateIndex
CREATE UNIQUE INDEX "board_slug_key" ON "board"("slug");
-- CreateIndex
CREATE INDEX "board_slug_idx" ON "board"("slug");
-- CreateIndex
CREATE INDEX "board_user_id_idx" ON "board"("user_id");
-- CreateIndex
CREATE INDEX "board_team_id_idx" ON "board"("team_id");
-- CreateIndex
CREATE INDEX "board_created_at_idx" ON "board"("created_at");
@@ -0,0 +1,29 @@
-- DropIndex
DROP INDEX "link_link_id_key";
-- DropIndex
DROP INDEX "pixel_pixel_id_key";
-- DropIndex
DROP INDEX "report_report_id_key";
-- DropIndex
DROP INDEX "revenue_revenue_id_key";
-- DropIndex
DROP INDEX "segment_segment_id_key";
-- DropIndex
DROP INDEX "session_session_id_key";
-- DropIndex
DROP INDEX "team_team_id_key";
-- DropIndex
DROP INDEX "team_user_team_user_id_key";
-- DropIndex
DROP INDEX "user_user_id_key";
-- DropIndex
DROP INDEX "website_website_id_key";
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "website_event" ADD COLUMN "cls" DECIMAL(10,4),
ADD COLUMN "fcp" DECIMAL(10,1),
ADD COLUMN "inp" DECIMAL(10,1),
ADD COLUMN "lcp" DECIMAL(10,1),
ADD COLUMN "ttfb" DECIMAL(10,1);
@@ -0,0 +1,50 @@
-- AlterTable board: drop slug
DROP INDEX IF EXISTS "board_slug_key";
DROP INDEX IF EXISTS "board_slug_idx";
ALTER TABLE "board" DROP COLUMN IF EXISTS "slug";
-- AlterTable
ALTER TABLE "website" ADD COLUMN "replay_enabled" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "website" ADD COLUMN "replay_config" JSONB;
-- CreateTable
CREATE TABLE "session_replay" (
"replay_id" UUID NOT NULL,
"website_id" UUID NOT NULL,
"session_id" UUID NOT NULL,
"visit_id" UUID NOT NULL,
"chunk_index" INTEGER NOT NULL,
"events" BYTEA NOT NULL,
"event_count" INTEGER NOT NULL,
"started_at" TIMESTAMPTZ(6) NOT NULL,
"ended_at" TIMESTAMPTZ(6) NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "session_replay_pkey" PRIMARY KEY ("replay_id")
);
-- CreateIndex
CREATE INDEX "session_replay_website_id_idx" ON "session_replay"("website_id");
CREATE INDEX "session_replay_session_id_idx" ON "session_replay"("session_id");
CREATE INDEX "session_replay_website_id_session_id_idx" ON "session_replay"("website_id", "session_id");
CREATE INDEX "session_replay_website_id_visit_id_idx" ON "session_replay"("website_id", "visit_id");
CREATE INDEX "session_replay_website_id_created_at_idx" ON "session_replay"("website_id", "created_at");
CREATE INDEX "session_replay_session_id_chunk_index_idx" ON "session_replay"("session_id", "chunk_index");
-- CreateTable
CREATE TABLE "session_replay_saved" (
"saved_replay_id" UUID NOT NULL,
"name" VARCHAR(100) NOT NULL,
"website_id" UUID NOT NULL,
"visit_id" UUID NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6),
CONSTRAINT "session_replay_saved_pkey" PRIMARY KEY ("saved_replay_id"),
CONSTRAINT "session_replay_saved_website_id_visit_id_key" UNIQUE ("website_id", "visit_id")
);
-- CreateIndex
CREATE INDEX "session_replay_saved_website_id_idx" ON "session_replay_saved"("website_id");
CREATE INDEX "session_replay_saved_visit_id_idx" ON "session_replay_saved"("visit_id");
CREATE INDEX "session_replay_saved_website_id_created_at_idx" ON "session_replay_saved"("website_id", "created_at");
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "heatmap_event" (
"heatmap_event_id" UUID NOT NULL,
"website_id" UUID NOT NULL,
"session_id" UUID NOT NULL,
"visit_id" UUID NOT NULL,
"url_path" VARCHAR(500) NOT NULL,
"event_type" INTEGER NOT NULL,
"node_id" INTEGER,
"x" INTEGER,
"y" INTEGER,
"viewport_w" INTEGER,
"viewport_h" INTEGER,
"page_h" INTEGER,
"scroll_pct" INTEGER,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "heatmap_event_pkey" PRIMARY KEY ("heatmap_event_id")
);
-- CreateIndex
CREATE INDEX "heatmap_event_website_id_idx" ON "heatmap_event"("website_id");
CREATE INDEX "heatmap_event_visit_id_idx" ON "heatmap_event"("visit_id");
CREATE INDEX "heatmap_event_website_id_created_at_idx" ON "heatmap_event"("website_id", "created_at");
CREATE INDEX "heatmap_event_website_id_url_path_event_type_created_at_idx" ON "heatmap_event"("website_id", "url_path", "event_type", "created_at");
+130 -21
View File
@@ -6,12 +6,11 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
model User {
id String @id @unique @map("user_id") @db.Uuid
id String @id() @map("user_id") @db.Uuid
username String @unique @db.VarChar(255)
password String @db.VarChar(60)
role String @map("role") @db.VarChar(50)
@@ -27,12 +26,13 @@ model User {
pixels Pixel[] @relation("user")
teams TeamUser[]
reports Report[]
boards Board[] @relation("user")
@@map("user")
}
model Session {
id String @id @unique @map("session_id") @db.Uuid
id String @id() @map("session_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
@@ -64,10 +64,9 @@ model Session {
}
model Website {
id String @id @unique @map("website_id") @db.Uuid
id String @id() @map("website_id") @db.Uuid
name String @db.VarChar(100)
domain String? @db.VarChar(500)
shareId String? @unique @map("share_id") @db.VarChar(50)
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
userId String? @map("user_id") @db.Uuid
teamId String? @map("team_id") @db.Uuid
@@ -76,19 +75,24 @@ model Website {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
user User? @relation("user", fields: [userId], references: [id])
createUser User? @relation("createUser", fields: [createdBy], references: [id])
team Team? @relation(fields: [teamId], references: [id])
eventData EventData[]
reports Report[]
revenue Revenue[]
segments Segment[]
sessionData SessionData[]
replayEnabled Boolean @default(false) @map("replay_enabled")
replayConfig Json? @map("replay_config")
user User? @relation("user", fields: [userId], references: [id])
createUser User? @relation("createUser", fields: [createdBy], references: [id])
team Team? @relation(fields: [teamId], references: [id])
eventData EventData[]
reports Report[]
revenue Revenue[]
segments Segment[]
sessionData SessionData[]
sessionReplays SessionReplay[]
sessionReplaysSaved SessionReplaySaved[]
heatmapEvents HeatmapEvent[]
@@index([userId])
@@index([teamId])
@@index([createdAt])
@@index([shareId])
@@index([createdBy])
@@map("website")
}
@@ -120,6 +124,11 @@ model WebsiteEvent {
eventName String? @map("event_name") @db.VarChar(50)
tag String? @db.VarChar(50)
hostname String? @db.VarChar(100)
lcp Decimal? @db.Decimal(10, 1)
inp Decimal? @db.Decimal(10, 1)
cls Decimal? @db.Decimal(10, 4)
fcp Decimal? @db.Decimal(10, 1)
ttfb Decimal? @db.Decimal(10, 1)
eventData EventData[]
session Session @relation(fields: [sessionId], references: [id])
@@ -187,7 +196,7 @@ model SessionData {
}
model Team {
id String @id() @unique() @map("team_id") @db.Uuid
id String @id() @map("team_id") @db.Uuid
name String @db.VarChar(50)
accessCode String? @unique @map("access_code") @db.VarChar(50)
logoUrl String? @map("logo_url") @db.VarChar(2183)
@@ -199,13 +208,14 @@ model Team {
members TeamUser[]
links Link[]
pixels Pixel[]
boards Board[]
@@index([accessCode])
@@map("team")
}
model TeamUser {
id String @id() @unique() @map("team_user_id") @db.Uuid
id String @id() @map("team_user_id") @db.Uuid
teamId String @map("team_id") @db.Uuid
userId String @map("user_id") @db.Uuid
role String @db.VarChar(50)
@@ -221,7 +231,7 @@ model TeamUser {
}
model Report {
id String @id() @unique() @map("report_id") @db.Uuid
id String @id() @map("report_id") @db.Uuid
userId String @map("user_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
type String @db.VarChar(50)
@@ -242,7 +252,7 @@ model Report {
}
model Segment {
id String @id() @unique() @map("segment_id") @db.Uuid
id String @id() @map("segment_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
type String @db.VarChar(50)
name String @db.VarChar(200)
@@ -257,7 +267,7 @@ model Segment {
}
model Revenue {
id String @id() @unique() @map("revenue_id") @db.Uuid
id String @id() @map("revenue_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
eventId String @map("event_id") @db.Uuid
@@ -277,7 +287,7 @@ model Revenue {
}
model Link {
id String @id() @unique() @map("link_id") @db.Uuid
id String @id() @map("link_id") @db.Uuid
name String @db.VarChar(100)
url String @db.VarChar(500)
slug String @unique() @db.VarChar(100)
@@ -298,7 +308,7 @@ model Link {
}
model Pixel {
id String @id() @unique() @map("pixel_id") @db.Uuid
id String @id() @map("pixel_id") @db.Uuid
name String @db.VarChar(100)
slug String @unique() @db.VarChar(100)
userId String? @map("user_id") @db.Uuid
@@ -316,3 +326,102 @@ model Pixel {
@@index([createdAt])
@@map("pixel")
}
model Board {
id String @id() @map("board_id") @db.Uuid
type String @db.VarChar(50)
name String @db.VarChar(200)
description String @db.VarChar(500)
parameters Json
userId String? @map("user_id") @db.Uuid
teamId String? @map("team_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
user User? @relation("user", fields: [userId], references: [id])
team Team? @relation(fields: [teamId], references: [id])
@@index([userId])
@@index([teamId])
@@index([createdAt])
@@map("board")
}
model Share {
id String @id() @map("share_id") @db.Uuid
entityId String @map("entity_id") @db.Uuid
name String @db.VarChar(200)
shareType Int @map("share_type") @db.Integer
slug String @unique() @db.VarChar(100)
parameters Json
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
@@index([entityId])
@@map("share")
}
model SessionReplay {
id String @id() @map("replay_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
visitId String @map("visit_id") @db.Uuid
chunkIndex Int @map("chunk_index") @db.Integer
events Bytes @map("events")
eventCount Int @map("event_count") @db.Integer
startedAt DateTime @map("started_at") @db.Timestamptz(6)
endedAt DateTime @map("ended_at") @db.Timestamptz(6)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id])
@@index([websiteId])
@@index([sessionId])
@@index([visitId])
@@index([websiteId, sessionId])
@@index([websiteId, visitId])
@@index([websiteId, createdAt])
@@index([sessionId, chunkIndex])
@@map("session_replay")
}
model SessionReplaySaved {
id String @id() @map("saved_replay_id") @db.Uuid
name String @db.VarChar(100)
websiteId String @map("website_id") @db.Uuid
visitId String @map("visit_id") @db.Uuid
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id])
@@unique([websiteId, visitId])
@@index([websiteId])
@@index([visitId])
@@index([websiteId, createdAt])
@@map("session_replay_saved")
}
model HeatmapEvent {
id String @id() @map("heatmap_event_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
visitId String @map("visit_id") @db.Uuid
urlPath String @map("url_path") @db.VarChar(500)
eventType Int @map("event_type") @db.Integer
nodeId Int? @map("node_id") @db.Integer
x Int? @db.Integer
y Int? @db.Integer
viewportW Int? @map("viewport_w") @db.Integer
viewportH Int? @map("viewport_h") @db.Integer
pageH Int? @map("page_h") @db.Integer
scrollPct Int? @map("scroll_pct") @db.Integer
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
website Website @relation(fields: [websiteId], references: [id])
@@index([websiteId])
@@index([visitId])
@@index([websiteId, createdAt])
@@index([websiteId, urlPath, eventType, createdAt])
@@map("heatmap_event")
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -227,7 +227,7 @@
"TO": "Tonga",
"TT": "Trinidad & Tobago",
"TN": "Tunisia",
"TR": "Turkey",
"TR": "T\u00fcrkiye",
"TM": "Turkmenistan",
"TC": "Turks & Caicos Islands",
"TV": "Tuvalu",
+1 -1
View File
@@ -227,7 +227,7 @@
"TO": "Tonga",
"TT": "Trinidad & Tobago",
"TN": "Tunisia",
"TR": "Turkey",
"TR": "T\u00fcrkiye",
"TM": "Turkmenistan",
"TC": "Turks & Caicos Islands",
"TV": "Tuvalu",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+7 -7
View File
@@ -6,13 +6,13 @@
"AD-06": "Sant Julia de Loria",
"AD-07": "Andorra la Vella",
"AD-08": "Escaldes-Engordany",
"AE-AJ": "'Ajman",
"AE-AZ": "Abu Zaby",
"AE-DU": "Dubayy",
"AE-FU": "Al Fujayrah",
"AE-RK": "Ra's al Khaymah",
"AE-SH": "Ash Shariqah",
"AE-UQ": "Umm al Qaywayn",
"AE-AJ": "Ajman",
"AE-AZ": "Abu Dhabi",
"AE-DU": "Dubai",
"AE-FU": "Al Fujairah",
"AE-RK": "Ras al Khaimah",
"AE-SH": "Sharjah",
"AE-UQ": "Umm al Quwain",
"AF-BAL": "Balkh",
"AF-BAM": "Bamyan",
"AF-BDG": "Badghis",
+1
View File
@@ -1,2 +1,3 @@
User-agent: *
Allow: /q/
Disallow: /
+24
View File
@@ -0,0 +1,24 @@
import 'dotenv/config';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import terser from '@rollup/plugin-terser';
export default {
input: 'src/recorder/index.js',
output: {
file: 'public/recorder.js',
format: 'iife',
},
plugins: [
resolve({ browser: true }),
commonjs(),
replace({
__COLLECT_API_HOST__: process.env.COLLECT_API_HOST || '',
__COLLECT_REPLAY_ENDPOINT__: process.env.COLLECT_REPLAY_ENDPOINT || '/api/record',
delimiters: ['', ''],
preventAssignment: true,
}),
terser({ compress: { evaluate: false } }),
],
};
+7 -2
View File
@@ -3,9 +3,14 @@ import 'dotenv/config';
import fs from 'node:fs';
import path from 'node:path';
import https from 'https';
import tar from 'tar';
import { list } from 'tar';
import zlib from 'zlib';
if (process.env.SKIP_BUILD_GEO) {
console.log('SKIP_BUILD_GEO is set. Skipping geo setup.');
process.exit(0);
}
if (process.env.VERCEL && !process.env.BUILD_GEO) {
console.log('Vercel environment detected. Skipping geo setup.');
process.exit(0);
@@ -40,7 +45,7 @@ const isDirectMmdb = url.endsWith('.mmdb');
const downloadCompressed = url =>
new Promise(resolve => {
https.get(url, res => {
resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
resolve(res.pipe(zlib.createGunzip({})).pipe(list()));
});
});
+49
View File
@@ -0,0 +1,49 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
const distDir = path.resolve(process.cwd(), 'dist');
const packageFile = path.join(distDir, 'package.json');
const defaultPackage = {
name: '@umami/components',
version: '0.0.0',
description: 'Umami React components.',
author: 'Mike Cao <mike@mikecao.com>',
license: 'MIT',
type: 'module',
main: './index.js',
types: './index.d.ts',
dependencies: {
'chart.js': '^4.5.0',
'chartjs-adapter-date-fns': '^3.0.0',
colord: '^2.9.2',
jsonwebtoken: '^9.0.2',
'lucide-react': '^0.542.0',
'pure-rand': '^7.0.1',
'react-simple-maps': '^2.3.0',
'react-use-measure': '^2.0.4',
'react-window': '^1.8.6',
'serialize-error': '^12.0.0',
thenby: '^1.3.4',
uuid: '^11.1.0',
},
};
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir);
}
const pkg = fs.existsSync(packageFile)
? JSON.parse(fs.readFileSync(packageFile, 'utf8'))
: defaultPackage;
const published = execSync(`npm view ${pkg.name} version`, { encoding: 'utf8' }).trim();
const [major, minor] = published.split('.').map(Number);
const next = `${major}.${minor + 1}.0`;
pkg.version = next;
fs.writeFileSync(packageFile, `${JSON.stringify(pkg, null, 2)}\n`);
console.log(`Bumped ${pkg.name} version: ${published} -> ${next}`);
+3 -2
View File
@@ -48,7 +48,7 @@ async function checkConnection() {
success('Database connection successful.');
} catch (e) {
throw new Error('Unable to connect to the database: ' + e.message);
throw new Error(`Unable to connect to the database: ${e.message}`);
}
}
@@ -67,7 +67,8 @@ async function checkDatabaseVersion() {
async function applyMigration() {
if (!process.env.SKIP_DB_MIGRATION) {
console.log(execSync('prisma migrate deploy').toString());
const directUrl = process.env.DIRECT_DATABASE_URL || process.env.DATABASE_URL;
console.log(execSync('prisma migrate deploy', { env: { ...process.env, DATABASE_URL: directUrl } }).toString());
success('Database is up to date.');
}
+60
View File
@@ -0,0 +1,60 @@
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const messagesDir = join(__dirname, '..', 'public', 'intl', 'messages');
const en = JSON.parse(readFileSync(join(messagesDir, 'en-US.json'), 'utf8'));
// Flatten nested structure: { label: { foo: 'bar' } } -> { 'label.foo': 'bar' }
function flatten(obj, prefix = '') {
const result = {};
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (typeof v === 'object' && v !== null) {
Object.assign(result, flatten(v, key));
} else {
result[key] = v;
}
}
return result;
}
const enFlat = flatten(en);
const enKeys = Object.keys(enFlat);
console.log(`en-US.json has ${enKeys.length} keys`);
const files = readdirSync(messagesDir)
.filter(f => f.endsWith('.json') && f !== 'en-US.json')
.sort();
const allMissing = {};
let total = 0;
for (const fname of files) {
const data = JSON.parse(readFileSync(join(messagesDir, fname), 'utf8'));
const flat = flatten(data);
const missing = enKeys.filter(k => !(k in flat));
if (missing.length) {
allMissing[fname] = missing;
console.log(`${fname}: ${missing.length} missing`);
total += missing.length;
}
}
console.log(`\nTotal missing across all locales: ${total}`);
const keyCounts = {};
for (const missing of Object.values(allMissing)) {
for (const k of missing) {
keyCounts[k] = (keyCounts[k] || 0) + 1;
}
}
const sorted = Object.entries(keyCounts).sort((a, b) => b[1] - a[1]);
if (sorted.length) {
console.log('\nMost commonly missing keys:');
for (const [k, count] of sorted) {
console.log(` "${k}": missing from ${count} files (en value: "${enFlat[k]}")`);
}
}
+4 -4
View File
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
import https from 'node:https';
import path from 'node:path';
import chalk from 'chalk';
import fs from 'fs-extra';
import https from 'https';
import fs from 'node:fs';
const src = path.resolve(process.cwd(), 'src/lang');
const src = path.resolve(process.cwd(), 'public/intl/messages');
const dest = path.resolve(process.cwd(), 'public/intl/country');
const files = fs.readdirSync(src);
@@ -43,7 +43,7 @@ const downloadFile = (url, filepath) =>
});
const download = async files => {
await fs.ensureDir(dest);
fs.mkdirSync(dest, { recursive: true });
await asyncForEach(files, async file => {
const locale = file.replace('-', '_').replace('.json', '');
+4 -4
View File
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
import https from 'node:https';
import path from 'node:path';
import chalk from 'chalk';
import fs from 'fs-extra';
import https from 'https';
import fs from 'node:fs';
const src = path.resolve(process.cwd(), 'src/lang');
const src = path.resolve(process.cwd(), 'public/intl/messages');
const dest = path.resolve(process.cwd(), 'public/intl/language');
const files = fs.readdirSync(src);
@@ -43,7 +43,7 @@ const downloadFile = (url, filepath) =>
});
const download = async files => {
await fs.ensureDir(dest);
fs.mkdirSync(dest, { recursive: true });
await asyncForEach(files, async file => {
const locale = file.replace('-', '_').replace('.json', '');
-35
View File
@@ -1,35 +0,0 @@
import path from 'node:path';
import del from 'del';
import fs from 'fs-extra';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const src = path.resolve(process.cwd(), 'src/lang');
const dest = path.resolve(process.cwd(), 'build/messages');
const files = fs.readdirSync(src);
del.sync([path.join(dest)]);
/*
This script takes the files from the `lang` folder and formats them into
the format that format-js expects.
*/
async function run() {
await fs.ensureDir(dest);
files.forEach(file => {
const lang = require(path.resolve(process.cwd(), `src/lang/${file}`));
const keys = Object.keys(lang).sort();
const formatted = keys.reduce((obj, key) => {
obj[key] = { defaultMessage: lang[key] };
return obj;
}, {});
const json = JSON.stringify(formatted, null, 2);
fs.writeFileSync(path.resolve(dest, file), json);
});
}
run();
-43
View File
@@ -1,43 +0,0 @@
/* eslint-disable no-console */
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'module';
import prettier from 'prettier';
const require = createRequire(import.meta.url);
const messages = require('../build/extracted-messages.json');
const dest = path.resolve(process.cwd(), 'src/lang');
const files = fs.readdirSync(dest);
const keys = Object.keys(messages).sort();
/*
This script takes extracted messages and merges them
with the existing files under `lang`. Any newly added
keys will be printed to the console.
*/
files.forEach(file => {
const lang = require(path.resolve(process.cwd(), `src/lang/${file}`));
console.log(`Merging ${file}`);
const merged = keys.reduce((obj, key) => {
const message = lang[key];
if (file === 'en-US.json') {
obj[key] = messages[key].defaultMessage;
} else {
obj[key] = message || messages[key].defaultMessage;
}
if (!message) {
console.log(`* Added key ${key}`);
}
return obj;
}, {});
const json = prettier.format(JSON.stringify(merged), { parser: 'json' });
fs.writeFileSync(path.resolve(dest, file), json);
});
+1 -1
View File
@@ -16,7 +16,7 @@
* npm run seed-data -- --verbose # Show detailed progress
*/
import { seed, type SeedConfig } from './seed/index.js';
import { type SeedConfig, seed } from './seed/index.js';
function parseArgs(): SeedConfig {
const args = process.argv.slice(2);
+1 -1
View File
@@ -1,4 +1,4 @@
import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js';
import { pickRandom, type WeightedOption, weightedRandom } from '../utils.js';
export type DeviceType = 'desktop' | 'mobile' | 'tablet';
+1 -1
View File
@@ -1,4 +1,4 @@
import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js';
import { pickRandom, type WeightedOption, weightedRandom } from '../utils.js';
interface GeoLocation {
country: string;
+1 -1
View File
@@ -1,4 +1,4 @@
import { weightedRandom, pickRandom, randomInt, type WeightedOption } from '../utils.js';
import { pickRandom, randomInt, type WeightedOption, weightedRandom } from '../utils.js';
export type ReferrerType = 'direct' | 'organic' | 'social' | 'paid' | 'referral';
+1 -1
View File
@@ -1,4 +1,4 @@
import { weightedRandom, randomInt, type WeightedOption } from '../utils.js';
import { randomInt, type WeightedOption, weightedRandom } from '../utils.js';
const hourlyWeights: WeightedOption<number>[] = [
{ value: 0, weight: 0.02 },
+1 -1
View File
@@ -1,5 +1,5 @@
import { uuid, addSeconds, randomInt } from '../utils.js';
import { getRandomReferrer } from '../distributions/referrers.js';
import { addSeconds, randomInt, uuid } from '../utils.js';
import type { SessionData } from './sessions.js';
export const EVENT_TYPE = {
+1 -1
View File
@@ -1,4 +1,4 @@
import { uuid, randomFloat } from '../utils.js';
import { randomFloat, uuid } from '../utils.js';
import type { EventData } from './events.js';
export interface RevenueConfig {
Loaded 100 of 789 files, more files were not shown because too many files have changed in this diff. Show more