Merge branch 'dev' into fix/postgres-implicit-alias-syntax-error

This commit is contained in:
Mike Cao
2026-04-16 15:44:26 -04:00
committed by GitHub
685 changed files with 42523 additions and 145387 deletions
+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
}
}
+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;
+26 -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,
@@ -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;
+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.
+31 -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';
@@ -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];
},
};
});
+50 -66
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,83 +47,78 @@
"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",
"@clickhouse/client": "^1.18.2",
"@date-fns/utc": "^1.2.0",
"@dicebear/collection": "^9.2.3",
"@dicebear/core": "^9.2.3",
"@fontsource/inter": "^5.2.8",
"@dicebear/collection": "^9.4.2",
"@dicebear/core": "^9.4.2",
"@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.6.0",
"@prisma/client": "^7.6.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.96.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",
"debug": "^4.4.3",
"del": "^6.0.0",
"detect-browser": "^5.2.0",
"dotenv": "^17.2.3",
"esbuild": "^0.25.11",
"fs-extra": "^11.3.2",
"immer": "^10.2.0",
"dotenv": "^17.3.1",
"esbuild": "^0.27.4",
"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",
"isbot": "^5.1.37",
"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.7.0",
"maxmind": "^5.0.5",
"next": "16.2.4",
"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.20.0",
"prisma": "^7.6.0",
"pure-rand": "^7.0.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-error-boundary": "^4.0.4",
"react-intl": "^7.1.14",
"react-resizable-panels": "^4.8.0",
"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.12"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@formatjs/cli": "^4.2.29",
"@netlify/plugin-nextjs": "^5.15.1",
"@biomejs/biome": "^2.4.10",
"@netlify/plugin-nextjs": "^5.15.9",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-json": "^6.0.0",
@@ -137,35 +127,29 @@
"@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.12.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",
"dotenv-cli": "^11.0.0",
"jest": "^29.7.0",
"lint-staged": "^16.2.6",
"postcss": "^8.5.6",
"postcss": "^8.5.8",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^15.1.0",
"postcss-preset-env": "7.8.3",
"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-delete": "^3.0.2",
"rollup-plugin-dts": "^6.4.1",
"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-prettier": "^9.0.3",
"stylelint-config-recommended": "^14.0.0",
"tar": "^6.1.2",
"tar": "^7.5.13",
"ts-jest": "^29.4.6",
"ts-morph": "^27.0.2",
"ts-node": "^10.9.1",
"tsup": "^8.5.0",
"tsx": "^4.19.0",
+2444 -5213
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");
+104 -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,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,77 @@ 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")
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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}`);
+1 -1
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}`);
}
}
+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 {
+1 -1
View File
@@ -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
View File
@@ -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;
+3 -3
View File
@@ -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';
+3 -3
View File
@@ -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';
+4 -1
View File
@@ -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,
},
};
@@ -63,6 +63,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug
headers: {
'Content-Type': 'image/gif',
'Content-Length': image.length.toString(),
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
},
});
}
+1 -1
View File
@@ -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,
},
};

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