Merge branch 'dev' into codex/fix-umami-4062
This commit is contained in:
@@ -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'
|
||||
|
||||
+32
-18
@@ -23,6 +23,15 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- type: standard
|
||||
base_path: ""
|
||||
suffix: ""
|
||||
- type: base
|
||||
base_path: "/umami"
|
||||
suffix: "-base"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -55,6 +64,7 @@ jobs:
|
||||
REF_TYPE="${{ github.ref_type }}"
|
||||
REF_NAME="${{ github.ref_name }}"
|
||||
INCLUDE_LATEST="${{ github.event.inputs.include_latest }}"
|
||||
SUFFIX="${{ matrix.suffix }}"
|
||||
|
||||
# Determine version source
|
||||
if [[ -n "$INPUT" ]]; then
|
||||
@@ -74,30 +84,30 @@ jobs:
|
||||
|
||||
if [[ "$VERSION" == *-* ]]; then
|
||||
# prerelease: only version tag
|
||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION"
|
||||
DOCKER_TAGS="umamisoftware/umami:$VERSION"
|
||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION$SUFFIX"
|
||||
DOCKER_TAGS="umamisoftware/umami:$VERSION$SUFFIX"
|
||||
else
|
||||
# stable release: version + hierarchy
|
||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}.${MINOR}"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:postgresql-latest"
|
||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION$SUFFIX"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}.${MINOR}$SUFFIX"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}$SUFFIX"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:postgresql-latest$SUFFIX"
|
||||
|
||||
DOCKER_TAGS="umamisoftware/umami:$VERSION"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}.${MINOR}"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:postgresql-latest"
|
||||
DOCKER_TAGS="umamisoftware/umami:$VERSION$SUFFIX"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}.${MINOR}$SUFFIX"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}$SUFFIX"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:postgresql-latest$SUFFIX"
|
||||
|
||||
# Add latest tag based on trigger and input
|
||||
if [[ "$REF_TYPE" == "tag" ]] || [[ "$INCLUDE_LATEST" == "true" ]]; then
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:latest"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:latest"
|
||||
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:latest$SUFFIX"
|
||||
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:latest$SUFFIX"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Non-tag build (e.g. from main branch)
|
||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:${REF_NAME}"
|
||||
DOCKER_TAGS="umamisoftware/umami:${REF_NAME}"
|
||||
GHCR_TAGS="ghcr.io/${{ github.repository }}:${REF_NAME}$SUFFIX"
|
||||
DOCKER_TAGS="umamisoftware/umami:${REF_NAME}$SUFFIX"
|
||||
fi
|
||||
|
||||
echo "ghcr_tags=$GHCR_TAGS" >> $GITHUB_OUTPUT
|
||||
@@ -112,8 +122,10 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.compute.outputs.ghcr_tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BASE_PATH=${{ matrix.base_path }}
|
||||
cache-from: type=gha,scope=${{ matrix.type }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.type }}
|
||||
|
||||
- name: Build and push to Docker Hub
|
||||
if: github.repository == 'umami-software/umami'
|
||||
@@ -123,5 +135,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.compute.outputs.docker_tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BASE_PATH=${{ matrix.base_path }}
|
||||
cache-from: type=gha,scope=${{ matrix.type }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.type }}
|
||||
|
||||
+8
-1
@@ -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
-1
@@ -16,7 +16,7 @@ COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
COPY docker/middleware.ts ./src
|
||||
|
||||
ARG BASE_PATH
|
||||
ARG BASE_PATH=""
|
||||
|
||||
ENV BASE_PATH=$BASE_PATH
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
@@ -209,7 +215,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 +287,22 @@ 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;
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-6
@@ -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.
|
||||
+31
-9
@@ -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';
|
||||
@@ -84,11 +92,14 @@ const headers = [
|
||||
source: '/:path*',
|
||||
headers: defaultHeaders,
|
||||
},
|
||||
{
|
||||
];
|
||||
|
||||
if (isProd) {
|
||||
headers.push({
|
||||
source: TRACKER_SCRIPT,
|
||||
headers: trackerHeaders,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
const rewrites = [];
|
||||
|
||||
@@ -112,6 +123,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 +176,7 @@ if (trackerScriptName) {
|
||||
}
|
||||
}
|
||||
|
||||
if (cloudMode) {
|
||||
if (isProd && cloudMode) {
|
||||
rewrites.push({
|
||||
source: '/script.js',
|
||||
destination: 'https://cloud.umami.is/script.js',
|
||||
@@ -163,23 +184,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 +221,4 @@ export default {
|
||||
async redirects() {
|
||||
return [...redirects];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
+46
-49
@@ -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,8 +11,8 @@
|
||||
},
|
||||
"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",
|
||||
"start-docker": "npm-run-all check-db update-tracker start-server",
|
||||
@@ -20,10 +20,11 @@
|
||||
"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,13 +33,8 @@
|
||||
"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",
|
||||
@@ -61,74 +57,75 @@
|
||||
".next/cache"
|
||||
],
|
||||
"dependencies": {
|
||||
"@clickhouse/client": "^1.12.0",
|
||||
"@clickhouse/client": "^1.17.0",
|
||||
"@date-fns/utc": "^1.2.0",
|
||||
"@dicebear/collection": "^9.2.3",
|
||||
"@dicebear/core": "^9.2.3",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@dicebear/collection": "^9.3.1",
|
||||
"@dicebear/core": "^9.3.1",
|
||||
"@hello-pangea/dnd": "^17.0.0",
|
||||
"@prisma/adapter-pg": "^6.18.0",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"@prisma/extension-read-replicas": "^0.4.1",
|
||||
"@prisma/adapter-pg": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@prisma/extension-read-replicas": "^0.5.0",
|
||||
"@react-spring/web": "^10.0.3",
|
||||
"@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.90.21",
|
||||
"@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",
|
||||
"debug": "^4.4.3",
|
||||
"del": "^6.0.0",
|
||||
"detect-browser": "^5.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.3.1",
|
||||
"esbuild": "^0.25.11",
|
||||
"fs-extra": "^11.3.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"immer": "^10.2.0",
|
||||
"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",
|
||||
"isbot": "^5.1.35",
|
||||
"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",
|
||||
"maxmind": "^5.0.5",
|
||||
"next": "^16.1.6",
|
||||
"next-intl": "^4.8.3",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"papaparse": "^5.5.3",
|
||||
"pg": "^8.16.3",
|
||||
"prisma": "^6.18.0",
|
||||
"pg": "^8.18.0",
|
||||
"prisma": "^7.3.0",
|
||||
"pure-rand": "^7.0.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-error-boundary": "^4.0.4",
|
||||
"react-intl": "^7.1.14",
|
||||
"react-resizable-panels": "^4.6.4",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-use-measure": "^2.0.4",
|
||||
"react-window": "^1.8.6",
|
||||
"redis": "^4.5.1",
|
||||
"request-ip": "^3.3.0",
|
||||
"semver": "^7.7.3",
|
||||
"rrweb": "2.0.0-alpha.4",
|
||||
"rrweb-player": "1.0.0-alpha.4",
|
||||
"semver": "^7.7.4",
|
||||
"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"
|
||||
"ua-parser-js": "^2.0.9",
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.8",
|
||||
"@formatjs/cli": "^4.2.29",
|
||||
"@netlify/plugin-nextjs": "^5.15.1",
|
||||
"@biomejs/biome": "^2.4.2",
|
||||
"@netlify/plugin-nextjs": "^5.15.8",
|
||||
"@rollup/plugin-alias": "^5.0.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.4",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
@@ -137,14 +134,13 @@
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^24.10.13",
|
||||
"@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",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^16.2.6",
|
||||
@@ -153,19 +149,20 @@
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-preset-env": "7.8.3",
|
||||
"prompts": "2.4.2",
|
||||
"rollup": "^4.52.5",
|
||||
"rollup": "^4.57.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-delete": "^3.0.1",
|
||||
"rollup-plugin-delete": "^3.0.2",
|
||||
"rollup-plugin-dts": "^6.3.0",
|
||||
"rollup-plugin-node-externals": "^8.1.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-css-modules": "^4.6.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-recommended": "^14.0.0",
|
||||
"tar": "^6.1.2",
|
||||
"tar": "^7.5.9",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-morph": "^27.0.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.19.0",
|
||||
|
||||
Generated
+2900
-3134
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";
|
||||
@@ -0,0 +1,33 @@
|
||||
-- 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_board_id_key" ON "board"("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,45 @@
|
||||
-- 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");
|
||||
+106
-21
@@ -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,23 @@ 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[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
@@index([createdAt])
|
||||
@@index([shareId])
|
||||
@@index([createdBy])
|
||||
@@map("website")
|
||||
}
|
||||
@@ -120,6 +123,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 +195,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 +207,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 +230,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 +251,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 +266,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 +286,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 +307,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 +325,79 @@ 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
|
||||
slug String @unique() @db.VarChar(100)
|
||||
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([slug])
|
||||
@@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")
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
+420
-2212
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2226
File diff suppressed because it is too large
Load Diff
+420
-2228
File diff suppressed because it is too large
Load Diff
+420
-2226
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2212
File diff suppressed because it is too large
Load Diff
+420
-2208
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+429
-2226
File diff suppressed because it is too large
Load Diff
+422
-2226
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+420
-2200
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2204
File diff suppressed because it is too large
Load Diff
+420
-2228
File diff suppressed because it is too large
Load Diff
+420
-2204
File diff suppressed because it is too large
Load Diff
+420
-2212
File diff suppressed because it is too large
Load Diff
+420
-2234
File diff suppressed because it is too large
Load Diff
+420
-2224
File diff suppressed because it is too large
Load Diff
+420
-2184
File diff suppressed because it is too large
Load Diff
+420
-2224
File diff suppressed because it is too large
Load Diff
+420
-2210
File diff suppressed because it is too large
Load Diff
+420
-2210
File diff suppressed because it is too large
Load Diff
+420
-2166
File diff suppressed because it is too large
Load Diff
+420
-2345
File diff suppressed because it is too large
Load Diff
+420
-2234
File diff suppressed because it is too large
Load Diff
+420
-2212
File diff suppressed because it is too large
Load Diff
+420
-2200
File diff suppressed because it is too large
Load Diff
+420
-2224
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2204
File diff suppressed because it is too large
Load Diff
+420
-2224
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+420
-2188
File diff suppressed because it is too large
Load Diff
+420
-2226
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+424
-2094
File diff suppressed because it is too large
Load Diff
+420
-2224
File diff suppressed because it is too large
Load Diff
+420
-2216
File diff suppressed because it is too large
Load Diff
+420
-2216
File diff suppressed because it is too large
Load Diff
+420
-2176
File diff suppressed because it is too large
Load Diff
+420
-2196
File diff suppressed because it is too large
Load Diff
+420
-2220
File diff suppressed because it is too large
Load Diff
+426
-1856
File diff suppressed because it is too large
Load Diff
+426
-1858
File diff suppressed because it is too large
Load Diff
+420
-2212
File diff suppressed because it is too large
Load Diff
+420
-2198
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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 } }),
|
||||
],
|
||||
};
|
||||
@@ -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()));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
+1
-1
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]}")`);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import { weightedRandom, pickRandom, type WeightedOption } from '../utils.js';
|
||||
import { pickRandom, type WeightedOption, weightedRandom } from '../utils.js';
|
||||
|
||||
interface GeoLocation {
|
||||
country: string;
|
||||
|
||||
@@ -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,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,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,4 +1,4 @@
|
||||
import { uuid, randomFloat } from '../utils.js';
|
||||
import { randomFloat, uuid } from '../utils.js';
|
||||
import type { EventData } from './events.js';
|
||||
|
||||
export interface RevenueConfig {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { uuid } from '../utils.js';
|
||||
import { getRandomDevice } from '../distributions/devices.js';
|
||||
import { getRandomGeo, getRandomLanguage } from '../distributions/geographic.js';
|
||||
import { generateTimestampForDay } from '../distributions/temporal.js';
|
||||
import { uuid } from '../utils.js';
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
|
||||
+13
-13
@@ -1,35 +1,35 @@
|
||||
/* eslint-disable no-console */
|
||||
import 'dotenv/config';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient, Prisma } from '../../src/generated/prisma/client.js';
|
||||
import { uuid, generateDatesBetween, subDays, formatNumber, progressBar } from './utils.js';
|
||||
import { createSessions, type SessionData } from './generators/sessions.js';
|
||||
import { type Prisma, PrismaClient } from '../../src/generated/prisma/client.js';
|
||||
import { getSessionCountForDay } from './distributions/temporal.js';
|
||||
import {
|
||||
generateEventsForSession,
|
||||
type EventData,
|
||||
type EventDataEntry,
|
||||
generateEventsForSession,
|
||||
} from './generators/events.js';
|
||||
import {
|
||||
generateRevenueForEvents,
|
||||
type RevenueData,
|
||||
type RevenueConfig,
|
||||
type RevenueData,
|
||||
} from './generators/revenue.js';
|
||||
import { getSessionCountForDay } from './distributions/temporal.js';
|
||||
import { createSessions, type SessionData } from './generators/sessions.js';
|
||||
import {
|
||||
BLOG_WEBSITE_NAME,
|
||||
BLOG_WEBSITE_DOMAIN,
|
||||
BLOG_SESSIONS_PER_DAY,
|
||||
getBlogSiteConfig,
|
||||
BLOG_WEBSITE_DOMAIN,
|
||||
BLOG_WEBSITE_NAME,
|
||||
getBlogJourney,
|
||||
getBlogSiteConfig,
|
||||
} from './sites/blog.js';
|
||||
import {
|
||||
SAAS_WEBSITE_NAME,
|
||||
SAAS_WEBSITE_DOMAIN,
|
||||
SAAS_SESSIONS_PER_DAY,
|
||||
getSaasSiteConfig,
|
||||
getSaasJourney,
|
||||
getSaasSiteConfig,
|
||||
SAAS_SESSIONS_PER_DAY,
|
||||
SAAS_WEBSITE_DOMAIN,
|
||||
SAAS_WEBSITE_NAME,
|
||||
saasRevenueConfigs,
|
||||
} from './sites/saas.js';
|
||||
import { formatNumber, generateDatesBetween, progressBar, subDays, uuid } from './utils.js';
|
||||
|
||||
const BATCH_SIZE = 1000;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { weightedRandom, type WeightedOption } from '../utils.js';
|
||||
import type {
|
||||
SiteConfig,
|
||||
CustomEventConfig,
|
||||
JourneyConfig,
|
||||
PageConfig,
|
||||
CustomEventConfig,
|
||||
SiteConfig,
|
||||
} from '../generators/events.js';
|
||||
import { type WeightedOption, weightedRandom } from '../utils.js';
|
||||
|
||||
export const BLOG_WEBSITE_NAME = 'Demo Blog';
|
||||
export const BLOG_WEBSITE_DOMAIN = 'blog.example.com';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { weightedRandom, type WeightedOption } from '../utils.js';
|
||||
import type {
|
||||
SiteConfig,
|
||||
CustomEventConfig,
|
||||
JourneyConfig,
|
||||
PageConfig,
|
||||
CustomEventConfig,
|
||||
SiteConfig,
|
||||
} from '../generators/events.js';
|
||||
import type { RevenueConfig } from '../generators/revenue.js';
|
||||
import { type WeightedOption, weightedRandom } from '../utils.js';
|
||||
|
||||
export const SAAS_WEBSITE_NAME = 'Demo SaaS';
|
||||
export const SAAS_WEBSITE_DOMAIN = 'app.example.com';
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
||||
payload: {
|
||||
pixel: pixel.id,
|
||||
url: request.url,
|
||||
referrer: request.headers.get('referer'),
|
||||
referrer: request.headers.get("referer") || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
|
||||
payload: {
|
||||
link: link.id,
|
||||
url: request.url,
|
||||
referrer: request.headers.get('referer'),
|
||||
referrer: request.headers.get("referer") || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+26
-8
@@ -4,6 +4,7 @@ import Script from 'next/script';
|
||||
import { useEffect } from 'react';
|
||||
import { MobileNav } from '@/app/(main)/MobileNav';
|
||||
import { SideNav } from '@/app/(main)/SideNav';
|
||||
import { TopNav } from '@/app/(main)/TopNav';
|
||||
import { useConfig, useLoginQuery, useNavigation } from '@/components/hooks';
|
||||
import { LAST_TEAM_CONFIG } from '@/lib/constants';
|
||||
import { removeItem, setItem } from '@/lib/storage';
|
||||
@@ -39,24 +40,41 @@ export function App({ children }) {
|
||||
|
||||
return (
|
||||
<Grid
|
||||
columns={{ xs: '1fr', lg: 'auto 1fr' }}
|
||||
rows={{ xs: 'auto 1fr', lg: '1fr' }}
|
||||
height={{ xs: 'auto', lg: '100vh' }}
|
||||
width="100%"
|
||||
columns={{ base: '1fr', lg: 'auto 1fr' }}
|
||||
rows={{ base: 'auto 1fr', lg: '1fr' }}
|
||||
height="screen"
|
||||
>
|
||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||
<Row display={{ base: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||
<MobileNav />
|
||||
</Row>
|
||||
<Column display={{ xs: 'none', lg: 'flex' }}>
|
||||
<Column display={{ base: 'none', lg: 'flex' }} minHeight="0" style={{ overflow: 'hidden' }}>
|
||||
<SideNav />
|
||||
</Column>
|
||||
<Column alignItems="center" overflowY="auto" overflowX="hidden" position="relative">
|
||||
{children}
|
||||
<Column overflowX="hidden" minHeight="0" position="relative">
|
||||
<TopNav />
|
||||
<Column alignItems="center">{children}</Column>
|
||||
</Column>
|
||||
<UpdateNotice user={user} config={config} />
|
||||
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
|
||||
<Script src={`${process.env.basePath || ''}/telemetry.js`} />
|
||||
)}
|
||||
{process.env.selfTrack && (
|
||||
<Script
|
||||
async
|
||||
data-website-id={process.env.selfTrack}
|
||||
src={`${process.env.basePath || ''}/script.js`}
|
||||
data-cache="true"
|
||||
data-performance="true"
|
||||
/>
|
||||
)}
|
||||
{process.env.selfRecord && (
|
||||
<Script
|
||||
async
|
||||
data-website-id={process.env.selfRecord}
|
||||
data-sample-rate="1"
|
||||
src={`${process.env.basePath || ''}/recorder.js`}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,78 @@
|
||||
import { Grid, IconLabel, NavMenu, NavMenuItem, Row, Text } from '@umami/react-zen';
|
||||
import { Column, Grid, Row, Text } from '@umami/react-zen';
|
||||
import Link from 'next/link';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
import { IconLabel } from '@/components/common/IconLabel';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Globe, Grid2x2, LinkIcon } from '@/components/icons';
|
||||
import { Globe, Grid2x2, LayoutDashboard, LinkIcon } from '@/components/icons';
|
||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||
import { NavButton } from '@/components/input/NavButton';
|
||||
import { UserButton } from '@/components/input/UserButton';
|
||||
import { Logo } from '@/components/svg';
|
||||
import { AdminNav } from './admin/AdminNav';
|
||||
import { SettingsNav } from './settings/SettingsNav';
|
||||
|
||||
export function MobileNav() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { t, labels } = useMessages();
|
||||
const { pathname, websiteId, renderUrl } = useNavigation();
|
||||
const isAdmin = pathname.includes('/admin');
|
||||
const isSettings = pathname.includes('/settings');
|
||||
const isMain = !websiteId && !isAdmin && !isSettings;
|
||||
|
||||
const links = [
|
||||
{
|
||||
id: 'boards',
|
||||
label: t(labels.boards),
|
||||
path: '/boards',
|
||||
icon: <LayoutDashboard />,
|
||||
},
|
||||
{
|
||||
id: 'websites',
|
||||
label: formatMessage(labels.websites),
|
||||
label: t(labels.websites),
|
||||
path: '/websites',
|
||||
icon: <Globe />,
|
||||
},
|
||||
{
|
||||
id: 'links',
|
||||
label: formatMessage(labels.links),
|
||||
label: t(labels.links),
|
||||
path: '/links',
|
||||
icon: <LinkIcon />,
|
||||
},
|
||||
{
|
||||
id: 'pixels',
|
||||
label: formatMessage(labels.pixels),
|
||||
label: t(labels.pixels),
|
||||
path: '/pixels',
|
||||
icon: <Grid2x2 />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="surface-sunken" borderRadius>
|
||||
<MobileMenuButton>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<NavMenu padding="3" onItemClick={close} border="bottom">
|
||||
<NavButton />
|
||||
{links.map(link => {
|
||||
<Column
|
||||
gap="2"
|
||||
display="flex"
|
||||
flex-direction="column"
|
||||
padding="1"
|
||||
style={{ height: '100dvh' }}
|
||||
>
|
||||
{isMain &&
|
||||
links.map(link => {
|
||||
return (
|
||||
<Link key={link.id} href={renderUrl(link.path)}>
|
||||
<NavMenuItem>
|
||||
<Row key={link.id} padding>
|
||||
<Link href={renderUrl(link.path)} onClick={close}>
|
||||
<IconLabel icon={link.icon} label={link.label} />
|
||||
</NavMenuItem>
|
||||
</Link>
|
||||
</Link>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</NavMenu>
|
||||
{websiteId && <WebsiteNav websiteId={websiteId} onItemClick={close} />}
|
||||
{isAdmin && <AdminNav onItemClick={close} />}
|
||||
{isSettings && <SettingsNav onItemClick={close} />}
|
||||
</>
|
||||
<Row style={{ marginTop: 'auto' }}>
|
||||
<UserButton onClose={close} />
|
||||
</Row>
|
||||
</Column>
|
||||
);
|
||||
}}
|
||||
</MobileMenuButton>
|
||||
|
||||
+128
-57
@@ -1,87 +1,158 @@
|
||||
import {
|
||||
Button,
|
||||
type ButtonProps,
|
||||
Column,
|
||||
Focusable,
|
||||
Icon,
|
||||
Row,
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarItem,
|
||||
type SidebarProps,
|
||||
SidebarSection,
|
||||
ThemeButton,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
} from '@umami/react-zen';
|
||||
import Link from 'next/link';
|
||||
import type { Key } from 'react';
|
||||
import { SettingsNav } from '@/app/(main)/settings/SettingsNav';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
import { IconLabel } from '@/components/common/IconLabel';
|
||||
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Globe, Grid2x2, LinkIcon, PanelLeft } from '@/components/icons';
|
||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||
import { NavButton } from '@/components/input/NavButton';
|
||||
import { PanelButton } from '@/components/input/PanelButton';
|
||||
import {
|
||||
Globe,
|
||||
Grid2x2,
|
||||
LayoutDashboard,
|
||||
LinkIcon,
|
||||
PanelLeft,
|
||||
PanelsLeftBottom,
|
||||
} from '@/components/icons';
|
||||
import { UserButton } from '@/components/input/UserButton';
|
||||
import { Logo } from '@/components/svg';
|
||||
|
||||
export function SideNav(props: SidebarProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname, renderUrl, websiteId, router } = useNavigation();
|
||||
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
|
||||
const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings'));
|
||||
export function SideNav(props: any) {
|
||||
const { t, labels } = useMessages();
|
||||
const { pathname, renderUrl, websiteId, teamId } = useNavigation();
|
||||
const [isCollapsed] = useGlobalState('sidenav-collapsed', false);
|
||||
|
||||
const links = [
|
||||
...(!teamId
|
||||
? [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: t(labels.dashboard),
|
||||
path: '/dashboard',
|
||||
icon: <PanelsLeftBottom />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'boards',
|
||||
label: t(labels.boards),
|
||||
path: '/boards',
|
||||
icon: <LayoutDashboard />,
|
||||
},
|
||||
{
|
||||
id: 'websites',
|
||||
label: formatMessage(labels.websites),
|
||||
label: t(labels.websites),
|
||||
path: '/websites',
|
||||
icon: <Globe />,
|
||||
},
|
||||
{
|
||||
id: 'links',
|
||||
label: formatMessage(labels.links),
|
||||
label: t(labels.links),
|
||||
path: '/links',
|
||||
icon: <LinkIcon />,
|
||||
},
|
||||
{
|
||||
id: 'pixels',
|
||||
label: formatMessage(labels.pixels),
|
||||
label: t(labels.pixels),
|
||||
path: '/pixels',
|
||||
icon: <Grid2x2 />,
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = (id: Key) => {
|
||||
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sidebar {...props} isCollapsed={isCollapsed || hasNav} backgroundColor>
|
||||
<SidebarSection onClick={() => setIsCollapsed(false)}>
|
||||
<SidebarHeader
|
||||
label="umami"
|
||||
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}
|
||||
style={{ maxHeight: 40 }}
|
||||
>
|
||||
{!isCollapsed && !hasNav && <PanelButton />}
|
||||
</SidebarHeader>
|
||||
</SidebarSection>
|
||||
<SidebarSection paddingTop="0" paddingBottom="0" justifyContent="center">
|
||||
<NavButton showText={!hasNav && !isCollapsed} onAction={handleSelect} />
|
||||
</SidebarSection>
|
||||
<SidebarSection flexGrow={1}>
|
||||
{links.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||
<SidebarItem
|
||||
label={label}
|
||||
icon={icon}
|
||||
isSelected={pathname.includes(path)}
|
||||
role="button"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
<SidebarSection justifyContent="flex-start">
|
||||
<Row wrap="wrap">
|
||||
<LanguageButton />
|
||||
<ThemeButton />
|
||||
<Column
|
||||
{...props}
|
||||
backgroundColor="surface-base"
|
||||
border
|
||||
borderRadius
|
||||
paddingX="2"
|
||||
flexGrow="1"
|
||||
minHeight="0"
|
||||
margin="2"
|
||||
style={{
|
||||
width: isCollapsed ? '55px' : '240px',
|
||||
transition: 'width 0.2s ease-in-out',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Row
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
height="60px"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Row paddingX="3" alignItems="center" justifyContent="space-between" flexGrow="1">
|
||||
{!isCollapsed && (
|
||||
<IconLabel icon={<Logo />}>
|
||||
<Text weight="bold">umami</Text>
|
||||
</IconLabel>
|
||||
)}
|
||||
<PanelButton />
|
||||
</Row>
|
||||
</SidebarSection>
|
||||
</Sidebar>
|
||||
</Row>
|
||||
<Column flexGrow="1" minHeight="0" style={{ overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
{websiteId ? (
|
||||
<WebsiteNav websiteId={websiteId} isCollapsed={isCollapsed} />
|
||||
) : pathname.includes('/settings') ? (
|
||||
<SettingsNav isCollapsed={isCollapsed} />
|
||||
) : (
|
||||
<Column gap="2">
|
||||
{links.map(({ id, path, label, icon }) => {
|
||||
const isSelected = pathname.startsWith(renderUrl(path, false));
|
||||
return (
|
||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
|
||||
<Focusable>
|
||||
<Row
|
||||
alignItems="center"
|
||||
hover={{ backgroundColor: 'surface-sunken' }}
|
||||
backgroundColor={isSelected ? 'surface-sunken' : undefined}
|
||||
borderRadius
|
||||
minHeight="40px"
|
||||
>
|
||||
<IconLabel
|
||||
icon={icon}
|
||||
label={isCollapsed ? '' : label}
|
||||
weight={isSelected ? 'bold' : undefined}
|
||||
padding
|
||||
/>
|
||||
</Row>
|
||||
</Focusable>
|
||||
<Tooltip placement="right">{label}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
)}
|
||||
</Column>
|
||||
<Row marginBottom="4" paddingTop="2">
|
||||
<UserButton showText={!isCollapsed} />
|
||||
</Row>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const PanelButton = (props: ButtonProps) => {
|
||||
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed', false);
|
||||
return (
|
||||
<Button
|
||||
onPress={() => setIsCollapsed(!isCollapsed)}
|
||||
variant="zero"
|
||||
{...props}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<Icon strokeColor="muted">
|
||||
<PanelLeft />
|
||||
</Icon>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
+95
-11
@@ -1,26 +1,110 @@
|
||||
import { Row, ThemeButton } from '@umami/react-zen';
|
||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||
import { ProfileButton } from '@/components/input/ProfileButton';
|
||||
'use client';
|
||||
import { Icon, Row } from '@umami/react-zen';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
import { Slash } from '@/components/icons';
|
||||
import { BoardSelect } from '@/components/input/BoardSelect';
|
||||
import { LinkSelect } from '@/components/input/LinkSelect';
|
||||
import { PixelSelect } from '@/components/input/PixelSelect';
|
||||
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||
|
||||
export function TopNav() {
|
||||
const { websiteId, linkId, pixelId, boardId, teamId, router, renderUrl } = useNavigation();
|
||||
|
||||
const handleWebsiteChange = (value: string) => {
|
||||
router.push(renderUrl(`/websites/${value}`));
|
||||
};
|
||||
|
||||
const handleLinkChange = (value: string) => {
|
||||
router.push(renderUrl(`/links/${value}`));
|
||||
};
|
||||
|
||||
const handlePixelChange = (value: string) => {
|
||||
router.push(renderUrl(`/pixels/${value}`));
|
||||
};
|
||||
|
||||
const handleBoardChange = (value: string) => {
|
||||
router.push(renderUrl(`/boards/${value}`));
|
||||
};
|
||||
|
||||
return (
|
||||
<Row
|
||||
position="absolute"
|
||||
position="sticky"
|
||||
top="0"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
justifyContent="flex-start"
|
||||
paddingY="2"
|
||||
paddingX="3"
|
||||
paddingRight="5"
|
||||
width="100%"
|
||||
style={{ position: 'sticky', top: 0 }}
|
||||
zIndex={1}
|
||||
zIndex={100}
|
||||
backgroundColor="surface-raised"
|
||||
>
|
||||
<Row alignItems="center" justifyContent="flex-end" backgroundColor="2" borderRadius>
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<ProfileButton />
|
||||
<Row alignItems="center">
|
||||
<TeamsButton />
|
||||
{(websiteId || linkId || pixelId || boardId) && (
|
||||
<>
|
||||
<Icon size="sm" color="muted" style={{ opacity: 0.7, margin: '0 6px' }}>
|
||||
<Slash />
|
||||
</Icon>
|
||||
{websiteId && (
|
||||
<WebsiteSelect
|
||||
websiteId={websiteId}
|
||||
teamId={teamId}
|
||||
onChange={handleWebsiteChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{linkId && (
|
||||
<LinkSelect
|
||||
linkId={linkId}
|
||||
teamId={teamId}
|
||||
onChange={handleLinkChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pixelId && (
|
||||
<PixelSelect
|
||||
pixelId={pixelId}
|
||||
teamId={teamId}
|
||||
onChange={handlePixelChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{boardId && (
|
||||
<BoardSelect
|
||||
boardId={boardId}
|
||||
teamId={teamId}
|
||||
onChange={handleBoardChange}
|
||||
buttonProps={{
|
||||
variant: 'quiet',
|
||||
style: { minHeight: 40, minWidth: 200, maxWidth: 200 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: -16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 16,
|
||||
background: 'linear-gradient(to bottom, var(--surface-raised), transparent)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user