mirror of
https://github.com/umami-software/umami.git
synced 2026-05-30 06:47:25 +00:00
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
npx lint-staged
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": ["stylelint-config-recommended", "stylelint-config-css-modules"],
|
||||
"rules": {
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -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;
|
||||
@@ -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];
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
+50
-66
@@ -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",
|
||||
|
||||
Generated
+2444
-5213
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,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
@@ -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
+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
+432
-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",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
User-agent: *
|
||||
Allow: /q/
|
||||
Disallow: /
|
||||
|
||||
@@ -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';
|
||||
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', '');
|
||||
|
||||
@@ -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', '');
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user