mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Compare commits
56 commits
d227bc369f
...
aefc36b476
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aefc36b476 | ||
|
|
5213e04f44 | ||
|
|
17d24bf8e2 | ||
|
|
767fda21cd | ||
|
|
c122fb718b | ||
|
|
f105a52fc2 | ||
|
|
5506314e54 | ||
|
|
e90b2201ca | ||
|
|
d8dcf05a20 | ||
|
|
2e62a06aa4 | ||
|
|
75ae7528fb | ||
|
|
58880b6a5f | ||
|
|
491716f4dd | ||
|
|
42d0594118 | ||
|
|
34c31ca63c | ||
|
|
dacf13475a | ||
|
|
8286af1453 | ||
|
|
34677bca8f | ||
|
|
b0aa6fd6ef | ||
|
|
97c26bc075 | ||
|
|
4eddac21c7 | ||
|
|
5e3e6b3edd | ||
|
|
612b00179b | ||
|
|
783098fadc | ||
|
|
6859f00bf6 | ||
|
|
b0ed4bddb6 | ||
|
|
ad264f941d | ||
|
|
4c0c9e6aa0 | ||
|
|
687318bd09 | ||
|
|
912d2d544d | ||
|
|
3072f02f1b | ||
|
|
86d2672c47 | ||
|
|
741c6039e6 | ||
|
|
37b6194c5f | ||
|
|
b75c15dc43 | ||
|
|
d04fff65fe | ||
|
|
437c168e6f | ||
|
|
886544f297 | ||
|
|
53dfc5e76a | ||
|
|
5fbef149d0 | ||
|
|
860e6390f1 | ||
|
|
9b0d1b092e | ||
|
|
2a71cc721b | ||
|
|
7bea47d9e8 | ||
|
|
e9cdabab5a | ||
|
|
5b97fb908a | ||
|
|
b088a2ee6e | ||
|
|
8cc571f548 | ||
|
|
59a16e719b | ||
|
|
220c2af344 | ||
|
|
7c804b10ea | ||
|
|
83b03d682c | ||
|
|
aaa8b5b6c9 | ||
|
|
81e27fc18c | ||
|
|
2b771ff2b4 | ||
|
|
7e42b5b35e |
64 changed files with 699 additions and 319 deletions
10
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
|
|
@ -24,13 +24,13 @@ body:
|
||||||
render: shell
|
render: shell
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
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'
|
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Which browser are you using? (if relevant)
|
label: How are you deploying your application?
|
||||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
description: 'For example: Vercel, Railway, Docker, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: How are you deploying your application? (if relevant)
|
label: Which browser are you using?
|
||||||
description: 'For example: Vercel, Railway, Docker, etc'
|
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||||
|
|
|
||||||
87
.github/workflows/cd.yml
vendored
87
.github/workflows/cd.yml
vendored
|
|
@ -3,13 +3,18 @@ name: Create docker images
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "v*.*.*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Optional image version (e.g. 3.0.0, v3.0.0, or 3.0.0-beta.1)'
|
description: "Optional image version (e.g. 3.0.0, v3.0.0, or 3.0.0-beta.1)"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ""
|
||||||
|
include_latest:
|
||||||
|
description: "Include latest tag"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -22,6 +27,9 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|
@ -46,6 +54,7 @@ jobs:
|
||||||
INPUT="${{ github.event.inputs.version }}"
|
INPUT="${{ github.event.inputs.version }}"
|
||||||
REF_TYPE="${{ github.ref_type }}"
|
REF_TYPE="${{ github.ref_type }}"
|
||||||
REF_NAME="${{ github.ref_name }}"
|
REF_NAME="${{ github.ref_name }}"
|
||||||
|
INCLUDE_LATEST="${{ github.event.inputs.include_latest }}"
|
||||||
|
|
||||||
# Determine version source
|
# Determine version source
|
||||||
if [[ -n "$INPUT" ]]; then
|
if [[ -n "$INPUT" ]]; then
|
||||||
|
|
@ -56,7 +65,8 @@ jobs:
|
||||||
VERSION=""
|
VERSION=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TAGS=""
|
GHCR_TAGS=""
|
||||||
|
DOCKER_TAGS=""
|
||||||
|
|
||||||
if [[ -n "$VERSION" ]]; then
|
if [[ -n "$VERSION" ]]; then
|
||||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||||
|
|
@ -64,37 +74,54 @@ jobs:
|
||||||
|
|
||||||
if [[ "$VERSION" == *-* ]]; then
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
# prerelease: only version tag
|
# prerelease: only version tag
|
||||||
TAGS="$VERSION"
|
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION"
|
||||||
|
DOCKER_TAGS="umamisoftware/umami:$VERSION"
|
||||||
else
|
else
|
||||||
# stable release: version + hierarchy + latest
|
# stable release: version + hierarchy
|
||||||
TAGS="$VERSION,${MAJOR}.${MINOR},${MAJOR},postgresql-latest,latest"
|
GHCR_TAGS="ghcr.io/${{ github.repository }}:$VERSION"
|
||||||
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}.${MINOR}"
|
||||||
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:${MAJOR}"
|
||||||
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:postgresql-latest"
|
||||||
|
|
||||||
|
DOCKER_TAGS="umamisoftware/umami:$VERSION"
|
||||||
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}.${MINOR}"
|
||||||
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:${MAJOR}"
|
||||||
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:postgresql-latest"
|
||||||
|
|
||||||
|
# Add latest tag based on trigger and input
|
||||||
|
if [[ "$REF_TYPE" == "tag" ]] || [[ "$INCLUDE_LATEST" == "true" ]]; then
|
||||||
|
GHCR_TAGS="$GHCR_TAGS,ghcr.io/${{ github.repository }}:latest"
|
||||||
|
DOCKER_TAGS="$DOCKER_TAGS,umamisoftware/umami:latest"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Non-tag build (e.g. from main branch)
|
# Non-tag build (e.g. from main branch)
|
||||||
TAGS="${REF_NAME}"
|
GHCR_TAGS="ghcr.io/${{ github.repository }}:${REF_NAME}"
|
||||||
|
DOCKER_TAGS="umamisoftware/umami:${REF_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
echo "ghcr_tags=$GHCR_TAGS" >> $GITHUB_OUTPUT
|
||||||
echo "Computed tags: $TAGS"
|
echo "docker_tags=$DOCKER_TAGS" >> $GITHUB_OUTPUT
|
||||||
|
echo "Computed GHCR tags: $GHCR_TAGS"
|
||||||
|
echo "Computed Docker Hub tags: $DOCKER_TAGS"
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push to GHCR
|
||||||
run: |
|
uses: docker/build-push-action@v5
|
||||||
TAGS="${{ steps.compute.outputs.tags }}"
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.compute.outputs.ghcr_tags }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
# Set image targets conditionally
|
- name: Build and push to Docker Hub
|
||||||
if [[ "${{ github.repository }}" == "umami-software/umami" ]]; then
|
if: github.repository == 'umami-software/umami'
|
||||||
IMAGES=("umamisoftware/umami" "ghcr.io/${{ github.repository }}")
|
uses: docker/build-push-action@v5
|
||||||
else
|
with:
|
||||||
IMAGES=("ghcr.io/${{ github.repository }}")
|
context: .
|
||||||
fi
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
for IMAGE in "${IMAGES[@]}"; do
|
tags: ${{ steps.compute.outputs.docker_tags }}
|
||||||
echo "Building and pushing $IMAGE with tags: $TAGS"
|
cache-from: type=gha
|
||||||
docker buildx build \
|
cache-to: type=gha,mode=max
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--push \
|
|
||||||
$(echo "$TAGS" | tr ',' '\n' | sed "s|^|--tag ${IMAGE}:|") \
|
|
||||||
--cache-from type=gha \
|
|
||||||
--cache-to type=gha,mode=max \
|
|
||||||
.
|
|
||||||
done
|
|
||||||
|
|
|
||||||
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Node.js CI
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DATABASE_TYPE: postgresql
|
DATABASE_URL: "postgresql://user:pass@localhost:5432/dummy"
|
||||||
SKIP_DB_CHECK: 1
|
SKIP_DB_CHECK: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -11,27 +11,18 @@ jobs:
|
||||||
if: github.repository == 'umami-software/umami'
|
if: github.repository == 'umami-software/umami'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- node-version: 18.18
|
|
||||||
pnpm-version: 10
|
|
||||||
db-type: postgresql
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4 # required so that setup-node will work
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.pnpm-version }}
|
version: 10
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js 18.18
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: 18.18
|
||||||
cache: 'pnpm'
|
cache: "pnpm"
|
||||||
env:
|
- run: npm install --global pnpm
|
||||||
DATABASE_TYPE: ${{ matrix.db-type }}
|
- run: pnpm install
|
||||||
- run: npm install --global pnpm
|
- run: pnpm test
|
||||||
- run: pnpm install
|
- run: pnpm build
|
||||||
- run: pnpm test
|
|
||||||
- run: pnpm build
|
|
||||||
|
|
|
||||||
91
.gitignore
vendored
91
.gitignore
vendored
|
|
@ -1,45 +1,46 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next
|
/.next
|
||||||
/out
|
/out
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/public/script.js
|
/public/script.js
|
||||||
/geo
|
/geo
|
||||||
/dist
|
/dist
|
||||||
/generated
|
/generated
|
||||||
/src/generated
|
/src/generated
|
||||||
|
pm2.yml
|
||||||
# misc
|
|
||||||
.DS_Store
|
# misc
|
||||||
.idea
|
.DS_Store
|
||||||
.yarn
|
.idea
|
||||||
*.iml
|
.yarn
|
||||||
*.log
|
*.iml
|
||||||
.vscode
|
*.log
|
||||||
.tool-versions
|
.vscode
|
||||||
|
.tool-versions
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
# debug
|
||||||
yarn-debug.log*
|
npm-debug.log*
|
||||||
yarn-error.log*
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
# local env files
|
|
||||||
.env
|
# local env files
|
||||||
.env.*
|
.env
|
||||||
*.env.*
|
.env.*
|
||||||
|
*.env.*
|
||||||
*.dev.yml
|
|
||||||
|
*.dev.yml
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ Docker image:
|
||||||
docker pull docker.umami.is/umami-software/umami:latest
|
docker pull docker.umami.is/umami-software/umami:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker compose to run Umami with a Postgres database, run:
|
Docker compose (Runs Umami with a PostgreSQL database):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const cloudMode = process.env.CLOUD_MODE || '';
|
||||||
const cloudUrl = process.env.CLOUD_URL || '';
|
const cloudUrl = process.env.CLOUD_URL || '';
|
||||||
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
||||||
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
||||||
|
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
|
||||||
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
||||||
const forceSSL = process.env.FORCE_SSL || '';
|
const forceSSL = process.env.FORCE_SSL || '';
|
||||||
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||||
|
|
@ -170,6 +171,7 @@ export default {
|
||||||
cloudMode,
|
cloudMode,
|
||||||
cloudUrl,
|
cloudUrl,
|
||||||
currentVersion: pkg.version,
|
currentVersion: pkg.version,
|
||||||
|
defaultCurrency,
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"description": "A modern, privacy-focused alternative to Google Analytics.",
|
"description": "A modern, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -102,15 +102,15 @@
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"lucide-react": "^0.543.0",
|
"lucide-react": "^0.543.0",
|
||||||
"maxmind": "^5.0.0",
|
"maxmind": "^5.0.0",
|
||||||
"next": "^15.5.7",
|
"next": "^15.5.9",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"prisma": "^7.1.0",
|
"prisma": "^7.1.0",
|
||||||
"pure-rand": "^7.0.1",
|
"pure-rand": "^7.0.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.3",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
"react-intl": "^7.1.14",
|
"react-intl": "^7.1.14",
|
||||||
"react-simple-maps": "^2.3.0",
|
"react-simple-maps": "^2.3.0",
|
||||||
|
|
|
||||||
BIN
public/images/country/t1.png
Normal file
BIN
public/images/country/t1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -5,6 +5,18 @@
|
||||||
"value": "访问代码"
|
"value": "访问代码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "账户"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.action": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "行为"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.actions": [
|
"label.actions": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -35,12 +47,24 @@
|
||||||
"value": "添加描述"
|
"value": "添加描述"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.add-link": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "添加链接"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "添加成员"
|
"value": "添加成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.add-pixel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "添加像素"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.add-step": [
|
"label.add-step": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -83,12 +107,24 @@
|
||||||
"value": "所有时间段"
|
"value": "所有时间段"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.analysis": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "分析"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.analytics": [
|
"label.analytics": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "分析"
|
"value": "分析"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.application": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "应用"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.apply": [
|
"label.apply": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -107,6 +143,12 @@
|
||||||
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
|
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.audience": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "受众"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.average": [
|
"label.average": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -125,6 +167,12 @@
|
||||||
"value": "之前"
|
"value": "之前"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.behavior": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "行为"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.boards": [
|
"label.boards": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -173,12 +221,24 @@
|
||||||
"value": "修改密码"
|
"value": "修改密码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.channel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "渠道"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.channels": [
|
"label.channels": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "渠道"
|
"value": "渠道"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.chart": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "图表"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.cities": [
|
"label.cities": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -203,6 +263,12 @@
|
||||||
"value": "队列"
|
"value": "队列"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.cohorts": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "队列"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.compare": [
|
"label.compare": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -317,6 +383,12 @@
|
||||||
"value": "创建者"
|
"value": "创建者"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.criteria": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "条件"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.currency": [
|
"label.currency": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -419,6 +491,12 @@
|
||||||
"value": "台式机"
|
"value": "台式机"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.destination-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "目标URL"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.details": [
|
"label.details": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -455,6 +533,12 @@
|
||||||
"value": "唯一ID"
|
"value": "唯一ID"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.documentation": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "文档"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.does-not-contain": [
|
"label.does-not-contain": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -479,6 +563,12 @@
|
||||||
"value": "域名"
|
"value": "域名"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.download": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "下载"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.dropoff": [
|
"label.dropoff": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -506,7 +596,7 @@
|
||||||
"label.email": [
|
"label.email": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Email"
|
"value": "邮箱"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
|
|
@ -527,6 +617,12 @@
|
||||||
"value": "入口 URL"
|
"value": "入口 URL"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.environment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "环境"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.event": [
|
"label.event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -671,6 +767,12 @@
|
||||||
"value": "分组"
|
"value": "分组"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.growth": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "增长"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.hostname": [
|
"label.hostname": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -701,6 +803,12 @@
|
||||||
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
|
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.invalid-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "无效URL"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -863,12 +971,24 @@
|
||||||
"value": "少于等于"
|
"value": "少于等于"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.link": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "链接"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.links": [
|
"label.links": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "链接"
|
"value": "链接"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.location": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "位置"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.login": [
|
"label.login": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1020,7 +1140,7 @@
|
||||||
"label.online": [
|
"label.online": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Online"
|
"value": "在线"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.organic-search": [
|
"label.organic-search": [
|
||||||
|
|
@ -1165,6 +1285,12 @@
|
||||||
"value": "路径"
|
"value": "路径"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.pixel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "像素"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.pixels": [
|
"label.pixels": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1185,6 +1311,12 @@
|
||||||
"value": " 提供支持"
|
"value": " 提供支持"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.preferences": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "偏好"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.previous": [
|
"label.previous": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1209,6 +1341,12 @@
|
||||||
"value": "个人资料"
|
"value": "个人资料"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.profiles": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "个人资料"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.properties": [
|
"label.properties": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1248,7 +1386,7 @@
|
||||||
"label.referral": [
|
"label.referral": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Referral"
|
"value": "来源"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.referrer": [
|
"label.referrer": [
|
||||||
|
|
@ -1371,6 +1509,24 @@
|
||||||
"value": "保存"
|
"value": "保存"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.save-cohort": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "保存为群组"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.save-segment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "保存为细分"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.screen": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "屏幕"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.screens": [
|
"label.screens": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1383,6 +1539,18 @@
|
||||||
"value": "搜索"
|
"value": "搜索"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.segment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "细分"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.segments": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "细分"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.select": [
|
"label.select": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1485,6 +1653,24 @@
|
||||||
"value": "总和"
|
"value": "总和"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.support": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "支持"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.switch-account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "切换账户"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.table": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "表格"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.tablet": [
|
"label.tablet": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1635,6 +1821,12 @@
|
||||||
"value": "跟踪代码"
|
"value": "跟踪代码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.traffic": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "流量"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.transactions": [
|
"label.transactions": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1846,7 +2038,7 @@
|
||||||
"message.bad-request": [
|
"message.bad-request": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Bad request"
|
"value": "请求错误"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.collected-data": [
|
"message.collected-data": [
|
||||||
|
|
@ -1946,7 +2138,7 @@
|
||||||
"message.forbidden": [
|
"message.forbidden": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Forbidden"
|
"value": "禁止访问"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.go-to-settings": [
|
"message.go-to-settings": [
|
||||||
|
|
@ -2046,13 +2238,13 @@
|
||||||
"message.not-found": [
|
"message.not-found": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Not found"
|
"value": "未找到"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.nothing-selected": [
|
"message.nothing-selected": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Nothing selected."
|
"value": "未选择"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.page-not-found": [
|
"message.page-not-found": [
|
||||||
|
|
@ -2090,7 +2282,7 @@
|
||||||
"message.sever-error": [
|
"message.sever-error": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Server error"
|
"value": "服务器错误"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.share-url": [
|
"message.share-url": [
|
||||||
|
|
@ -2158,7 +2350,7 @@
|
||||||
"message.unauthorized": [
|
"message.unauthorized": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Unauthorized"
|
"value": "未授权"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
"AD-06": "Sant Julia de Loria",
|
"AD-06": "Sant Julia de Loria",
|
||||||
"AD-07": "Andorra la Vella",
|
"AD-07": "Andorra la Vella",
|
||||||
"AD-08": "Escaldes-Engordany",
|
"AD-08": "Escaldes-Engordany",
|
||||||
"AE-AJ": "'Ajman",
|
"AE-AJ": "Ajman",
|
||||||
"AE-AZ": "Abu Zaby",
|
"AE-AZ": "Abu Dhabi",
|
||||||
"AE-DU": "Dubayy",
|
"AE-DU": "Dubai",
|
||||||
"AE-FU": "Al Fujayrah",
|
"AE-FU": "Al Fujairah",
|
||||||
"AE-RK": "Ra's al Khaymah",
|
"AE-RK": "Ras al Khaimah",
|
||||||
"AE-SH": "Ash Shariqah",
|
"AE-SH": "Sharjah",
|
||||||
"AE-UQ": "Umm al Qaywayn",
|
"AE-UQ": "Umm al Quwain",
|
||||||
"AF-BAL": "Balkh",
|
"AF-BAL": "Balkh",
|
||||||
"AF-BAM": "Bamyan",
|
"AF-BAM": "Bamyan",
|
||||||
"AF-BDG": "Badghis",
|
"AF-BDG": "Badghis",
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import {
|
||||||
Form,
|
Form,
|
||||||
FormField,
|
FormField,
|
||||||
FormSubmitButton,
|
FormSubmitButton,
|
||||||
|
Grid,
|
||||||
Icon,
|
Icon,
|
||||||
Label,
|
Label,
|
||||||
Loading,
|
Loading,
|
||||||
Row,
|
Row,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
import { RefreshCw } from '@/components/icons';
|
import { RefreshCw } from '@/components/icons';
|
||||||
|
|
@ -42,7 +43,7 @@ export function LinkEditForm({
|
||||||
const { linksUrl } = useConfig();
|
const { linksUrl } = useConfig();
|
||||||
const hostUrl = linksUrl || LINKS_URL;
|
const hostUrl = linksUrl || LINKS_URL;
|
||||||
const { data, isLoading } = useLinkQuery(linkId);
|
const { data, isLoading } = useLinkQuery(linkId);
|
||||||
const [slug, setSlug] = useState(generateId());
|
const [defaultSlug] = useState(generateId());
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
await mutateAsync(data, {
|
await mutateAsync(data, {
|
||||||
|
|
@ -55,14 +56,6 @@ export function LinkEditForm({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSlug = () => {
|
|
||||||
const slug = generateId();
|
|
||||||
|
|
||||||
setSlug(slug);
|
|
||||||
|
|
||||||
return slug;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkUrl = (url: string) => {
|
const checkUrl = (url: string) => {
|
||||||
if (!isValidUrl(url)) {
|
if (!isValidUrl(url)) {
|
||||||
return formatMessage(labels.invalidUrl);
|
return formatMessage(labels.invalidUrl);
|
||||||
|
|
@ -70,19 +63,19 @@ export function LinkEditForm({
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setSlug(data.slug);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (linkId && isLoading) {
|
if (linkId && isLoading) {
|
||||||
return <Loading placement="absolute" />;
|
return <Loading placement="absolute" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}>
|
<Form
|
||||||
{({ setValue }) => {
|
onSubmit={handleSubmit}
|
||||||
|
error={getErrorMessage(error)}
|
||||||
|
defaultValues={{ slug: defaultSlug, ...data }}
|
||||||
|
>
|
||||||
|
{({ setValue, watch }) => {
|
||||||
|
const slug = watch('slug');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -101,15 +94,25 @@ export function LinkEditForm({
|
||||||
<TextField placeholder="https://example.com" autoComplete="off" />
|
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<Grid columns="1fr auto" alignItems="end" gap>
|
||||||
name="slug"
|
<FormField
|
||||||
rules={{
|
name="slug"
|
||||||
required: formatMessage(labels.required),
|
label={formatMessage({ id: 'label.slug', defaultMessage: 'Slug' })}
|
||||||
}}
|
rules={{
|
||||||
style={{ display: 'none' }}
|
required: formatMessage(labels.required),
|
||||||
>
|
}}
|
||||||
<input type="hidden" />
|
>
|
||||||
</FormField>
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
<Button
|
||||||
|
variant="quiet"
|
||||||
|
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
|
||||||
|
>
|
||||||
|
<Icon>
|
||||||
|
<RefreshCw />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Column>
|
<Column>
|
||||||
<Label>{formatMessage(labels.link)}</Label>
|
<Label>{formatMessage(labels.link)}</Label>
|
||||||
|
|
@ -121,14 +124,6 @@ export function LinkEditForm({
|
||||||
allowCopy
|
allowCopy
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
variant="quiet"
|
|
||||||
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
|
|
||||||
>
|
|
||||||
<Icon>
|
|
||||||
<RefreshCw />
|
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { getLink } from '@/queries/prisma';
|
||||||
import { LinkPage } from './LinkPage';
|
import { LinkPage } from './LinkPage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
|
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
|
||||||
const { linkId } = await params;
|
const { linkId } = await params;
|
||||||
|
const link = await getLink(linkId);
|
||||||
|
|
||||||
|
if (!link || link?.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <LinkPage linkId={linkId} />;
|
return <LinkPage linkId={linkId} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { getPixel } from '@/queries/prisma';
|
||||||
import { PixelPage } from './PixelPage';
|
import { PixelPage } from './PixelPage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: { pixelId: string } }) {
|
export default async function ({ params }: { params: { pixelId: string } }) {
|
||||||
const { pixelId } = await params;
|
const { pixelId } = await params;
|
||||||
|
const pixel = await getPixel(pixelId);
|
||||||
|
|
||||||
|
if (!pixel || pixel?.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <PixelPage pixelId={pixelId} />;
|
return <PixelPage pixelId={pixelId} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { DateRangeSetting } from './DateRangeSetting';
|
||||||
import { LanguageSetting } from './LanguageSetting';
|
import { LanguageSetting } from './LanguageSetting';
|
||||||
import { ThemeSetting } from './ThemeSetting';
|
import { ThemeSetting } from './ThemeSetting';
|
||||||
import { TimezoneSetting } from './TimezoneSetting';
|
import { TimezoneSetting } from './TimezoneSetting';
|
||||||
|
import { VersionSetting } from './VersionSetting';
|
||||||
|
|
||||||
export function PreferenceSettings() {
|
export function PreferenceSettings() {
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
|
|
@ -31,6 +32,10 @@ export function PreferenceSettings() {
|
||||||
<Label>{formatMessage(labels.theme)}</Label>
|
<Label>{formatMessage(labels.theme)}</Label>
|
||||||
<ThemeSetting />
|
<ThemeSetting />
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column>
|
||||||
|
<Label>{formatMessage(labels.version)}</Label>
|
||||||
|
<VersionSetting />
|
||||||
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
src/app/(main)/settings/preferences/VersionSetting.tsx
Normal file
8
src/app/(main)/settings/preferences/VersionSetting.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Text } from '@umami/react-zen';
|
||||||
|
import { CURRENT_VERSION } from '@/lib/constants';
|
||||||
|
|
||||||
|
export function VersionSetting() {
|
||||||
|
return <Text>{CURRENT_VERSION}</Text>;
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,10 @@ import { ListTable } from '@/components/metrics/ListTable';
|
||||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
import { renderDateLabels } from '@/lib/charts';
|
import { renderDateLabels } from '@/lib/charts';
|
||||||
import { CHART_COLORS } from '@/lib/constants';
|
import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
|
||||||
import { generateTimeSeries } from '@/lib/date';
|
import { generateTimeSeries } from '@/lib/date';
|
||||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
|
import { getItem, setItem } from '@/lib/storage';
|
||||||
|
|
||||||
export interface RevenueProps {
|
export interface RevenueProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -24,7 +25,15 @@ export interface RevenueProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
const [currency, setCurrency] = useState('USD');
|
const [currency, setCurrency] = useState(
|
||||||
|
getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCurrencyChange = (value: string) => {
|
||||||
|
setCurrency(value);
|
||||||
|
setItem(CURRENCY_CONFIG, value);
|
||||||
|
};
|
||||||
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { locale, dateLocale } = useLocale();
|
const { locale, dateLocale } = useLocale();
|
||||||
const { countryNames } = useCountryNames(locale);
|
const { countryNames } = useCountryNames(locale);
|
||||||
|
|
@ -107,7 +116,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<Grid columns="280px" gap>
|
<Grid columns="280px" gap>
|
||||||
<CurrencySelect value={currency} onChange={setCurrency} />
|
<CurrencySelect value={currency} onChange={handleCurrencyChange} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
|
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
|
||||||
|
import { getWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
export default async function ({
|
export default async function ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -9,6 +10,11 @@ export default async function ({
|
||||||
params: Promise<{ websiteId: string }>;
|
params: Promise<{ websiteId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
|
if (!website || website?.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
|
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,6 @@ export async function GET(request: Request) {
|
||||||
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
||||||
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
||||||
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
||||||
|
currentVersion: !!process.env.currentVersion,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);
|
const data = await getAttribution(websiteId, parameters as AttributionParameters, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);
|
const data = await getBreakdown(websiteId, parameters as BreakdownParameters, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);
|
const data = await getFunnel(websiteId, parameters as FunnelParameters, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getGoal(websiteId, parameters as GoalParameters, filters);
|
const data = await getGoal(websiteId, parameters as GoalParameters, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
|
|
||||||
const data = await getRetention(websiteId, parameters as RetentionParameters, filters);
|
const data = await getRetention(websiteId, parameters as RetentionParameters, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);
|
const data = await getRevenue(websiteId, parameters as RevenuParameters, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(body.filters, websiteId);
|
const filters = await getQueryFilters(body.filters, websiteId, auth.user?.id);
|
||||||
const parameters = await setWebsiteDate(websiteId, body.parameters);
|
const parameters = await setWebsiteDate(websiteId, auth.user.id, body.parameters);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
utm_source: [],
|
utm_source: [],
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getEventDataEvents(websiteId, {
|
const data = await getEventDataEvents(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getEventDataFields(websiteId, filters);
|
const data = await getEventDataFields(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getEventDataProperties(websiteId, filters);
|
const data = await getEventDataProperties(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getEventDataStats(websiteId, filters);
|
const data = await getEventDataStats(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export async function GET(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { propertyName } = query;
|
const { propertyName } = query;
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getEventDataValues(websiteId, {
|
const data = await getEventDataValues(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getWebsiteEvents(websiteId, filters);
|
const data = await getWebsiteEvents(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getEventStats(websiteId, filters);
|
const data = await getEventStats(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
|
const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([
|
||||||
getEventMetrics(websiteId, { type: 'event' }, filters),
|
getEventMetrics(websiteId, { type: 'event' }, filters),
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export async function GET(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, limit, offset, search } = query;
|
const { type, limit, offset, search } = query;
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
filters[type] = `c.${search}`;
|
filters[type] = `c.${search}`;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export async function GET(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, limit, offset, search } = query;
|
const { type, limit, offset, search } = query;
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
filters[type] = `c.${search}`;
|
filters[type] = `c.${search}`;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const [pageviews, sessions] = await Promise.all([
|
const [pageviews, sessions] = await Promise.all([
|
||||||
getPageviewStats(websiteId, filters),
|
getPageviewStats(websiteId, filters),
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getSessionDataProperties(websiteId, filters);
|
const data = await getSessionDataProperties(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export async function GET(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { propertyName } = query;
|
const { propertyName } = query;
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getSessionDataValues(websiteId, {
|
const data = await getSessionDataValues(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getSessionActivity(websiteId, sessionId, filters);
|
const data = await getSessionActivity(websiteId, sessionId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getWebsiteSessions(websiteId, filters);
|
const data = await getWebsiteSessions(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const metrics = await getWebsiteSessionStats(websiteId, filters);
|
const metrics = await getWebsiteSessionStats(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getWeeklyTraffic(websiteId, filters);
|
const data = await getWeeklyTraffic(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
|
|
||||||
const data = await getWebsiteStats(websiteId, filters);
|
const data = await getWebsiteStats(websiteId, filters);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export async function GET(
|
||||||
value: segment.name,
|
value: segment.name,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
const filters = await getQueryFilters(query, websiteId);
|
const filters = await getQueryFilters(query, websiteId, auth.user?.id);
|
||||||
values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
|
values = await getValues(websiteId, FILTER_COLUMNS[type], filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import redis from '@/lib/redis';
|
import { fetchAccount } from '@/lib/load';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { pagingParams, searchParams } from '@/lib/schema';
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
|
|
@ -52,7 +52,7 @@ export async function POST(request: Request) {
|
||||||
const { id, name, domain, shareId, teamId } = body;
|
const { id, name, domain, shareId, teamId } = body;
|
||||||
|
|
||||||
if (process.env.CLOUD_MODE && !teamId) {
|
if (process.env.CLOUD_MODE && !teamId) {
|
||||||
const account = await redis.client.get(`account:${auth.user.id}`);
|
const account = await fetchAccount(auth.user.id);
|
||||||
|
|
||||||
if (!account?.hasSubscription) {
|
if (!account?.hasSubscription) {
|
||||||
const count = await getWebsiteCount(auth.user.id);
|
const count = await getWebsiteCount(auth.user.id);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function useEventDataValuesQuery(
|
||||||
return useQuery<any>({
|
return useQuery<any>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'websites:event-data:values',
|
'websites:event-data:values',
|
||||||
{ websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
|
{ websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName },
|
||||||
],
|
],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
get(`/websites/${websiteId}/event-data/values`, {
|
get(`/websites/${websiteId}/event-data/values`, {
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export function WebsiteSelect({
|
||||||
renderValue={renderValue}
|
renderValue={renderValue}
|
||||||
listProps={{
|
listProps={{
|
||||||
renderEmptyState: () => <Empty message={formatMessage(messages.noResultsFound)} />,
|
renderEmptyState: () => <Empty message={formatMessage(messages.noResultsFound)} />,
|
||||||
style: { maxHeight: '400px' },
|
style: { maxHeight: 'calc(42vh - 65px)' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ id, name }: any) => <ListItem key={id}>{name}</ListItem>}
|
{({ id, name }: any) => <ListItem key={id}>{name}</ListItem>}
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,7 @@ export const labels = defineMessages({
|
||||||
growth: { id: 'label.growth', defaultMessage: 'Growth' },
|
growth: { id: 'label.growth', defaultMessage: 'Growth' },
|
||||||
account: { id: 'label.account', defaultMessage: 'Account' },
|
account: { id: 'label.account', defaultMessage: 'Account' },
|
||||||
application: { id: 'label.application', defaultMessage: 'Application' },
|
application: { id: 'label.application', defaultMessage: 'Application' },
|
||||||
|
version: { id: 'label.version', defaultMessage: 'Version' },
|
||||||
saveSegment: { id: 'label.save-segment', defaultMessage: 'Save as segment' },
|
saveSegment: { id: 'label.save-segment', defaultMessage: 'Save as segment' },
|
||||||
saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' },
|
saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' },
|
||||||
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
|
analysis: { id: 'label.analysis', defaultMessage: 'Analysis' },
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@
|
||||||
"label.event-name": "Име на събитие",
|
"label.event-name": "Име на събитие",
|
||||||
"label.events": "Събития",
|
"label.events": "Събития",
|
||||||
"label.exists": "Съществува",
|
"label.exists": "Съществува",
|
||||||
"label.exit": "Exit URL",
|
"label.exit": "URL за изход",
|
||||||
"label.false": "Грешно",
|
"label.false": "Грешно",
|
||||||
"label.field": "Поле",
|
"label.field": "Поле",
|
||||||
"label.fields": "Полета",
|
"label.fields": "Полета",
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
"label.last-days": "Последните {x} дни",
|
"label.last-days": "Последните {x} дни",
|
||||||
"label.last-hours": "Последните {x} часа",
|
"label.last-hours": "Последните {x} часа",
|
||||||
"label.last-months": "Последните {x} месеца",
|
"label.last-months": "Последните {x} месеца",
|
||||||
"label.last-seen": "Last seen",
|
"label.last-seen": "Последно видяно",
|
||||||
"label.leave": "Напусни",
|
"label.leave": "Напусни",
|
||||||
"label.leave-team": "Напусни екип",
|
"label.leave-team": "Напусни екип",
|
||||||
"label.less-than": "По-малко от",
|
"label.less-than": "По-малко от",
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
"label.none": "Няма",
|
"label.none": "Няма",
|
||||||
"label.number-of-records": "{x} {x, plural, one {един} other {други}}",
|
"label.number-of-records": "{x} {x, plural, one {един} other {други}}",
|
||||||
"label.ok": "Добре",
|
"label.ok": "Добре",
|
||||||
"label.online": "Online",
|
"label.online": "Онлайн",
|
||||||
"label.organic-search": "Органично търсене",
|
"label.organic-search": "Органично търсене",
|
||||||
"label.organic-shopping": "Органично пазаруване",
|
"label.organic-shopping": "Органично пазаруване",
|
||||||
"label.organic-social": "Органични социални мрежи",
|
"label.organic-social": "Органични социални мрежи",
|
||||||
|
|
@ -185,9 +185,9 @@
|
||||||
"label.paths": "Пътища",
|
"label.paths": "Пътища",
|
||||||
"label.pixels": "Пиксели",
|
"label.pixels": "Пиксели",
|
||||||
"label.powered-by": "Поддържано от {name}",
|
"label.powered-by": "Поддържано от {name}",
|
||||||
"label.previous": "Previous",
|
"label.previous": "Предишен",
|
||||||
"label.previous-period": "Previous period",
|
"label.previous-period": "Предишен период",
|
||||||
"label.previous-year": "Previous year",
|
"label.previous-year": "Предишна година",
|
||||||
"label.profile": "Профил",
|
"label.profile": "Профил",
|
||||||
"label.properties": "Свойства",
|
"label.properties": "Свойства",
|
||||||
"label.property": "Свойство",
|
"label.property": "Свойство",
|
||||||
|
|
@ -211,8 +211,8 @@
|
||||||
"label.reset-website": "Нулирай уебсайт",
|
"label.reset-website": "Нулирай уебсайт",
|
||||||
"label.retention": "Привързване",
|
"label.retention": "Привързване",
|
||||||
"label.retention-description": "Измерете привързаността към вашия уебсайт, като проследявате колко често потребителите се връщат.",
|
"label.retention-description": "Измерете привързаността към вашия уебсайт, като проследявате колко често потребителите се връщат.",
|
||||||
"label.revenue": "Revenue",
|
"label.revenue": "Приходи",
|
||||||
"label.revenue-description": "Look into your revenue across time.",
|
"label.revenue-description": "Прегледайте приходите си във времето.",
|
||||||
"label.role": "Роля",
|
"label.role": "Роля",
|
||||||
"label.run-query": "Изпълни запитване",
|
"label.run-query": "Изпълни запитване",
|
||||||
"label.save": "Запази",
|
"label.save": "Запази",
|
||||||
|
|
@ -260,14 +260,14 @@
|
||||||
"label.total": "Общо",
|
"label.total": "Общо",
|
||||||
"label.total-records": "Общо записи",
|
"label.total-records": "Общо записи",
|
||||||
"label.tracking-code": "Код за проследяване",
|
"label.tracking-code": "Код за проследяване",
|
||||||
"label.transactions": "Transactions",
|
"label.transactions": "Транзакции",
|
||||||
"label.transfer": "Прехвърли",
|
"label.transfer": "Прехвърли",
|
||||||
"label.transfer-website": "Прехвърляне на уебсайт",
|
"label.transfer-website": "Прехвърляне на уебсайт",
|
||||||
"label.true": "Вярно",
|
"label.true": "Вярно",
|
||||||
"label.type": "Вид",
|
"label.type": "Вид",
|
||||||
"label.unique": "Уникален",
|
"label.unique": "Уникален",
|
||||||
"label.unique-visitors": "Уникални посетители",
|
"label.unique-visitors": "Уникални посетители",
|
||||||
"label.uniqueCustomers": "Unique Customers",
|
"label.uniqueCustomers": "Уникални клиенти",
|
||||||
"label.unknown": "Неизвестен",
|
"label.unknown": "Неизвестен",
|
||||||
"label.untitled": "Без заглавие",
|
"label.untitled": "Без заглавие",
|
||||||
"label.update": "Актуализирай",
|
"label.update": "Актуализирай",
|
||||||
|
|
@ -282,7 +282,7 @@
|
||||||
"label.view-only": "Само за преглед",
|
"label.view-only": "Само за преглед",
|
||||||
"label.views": "Прегледи",
|
"label.views": "Прегледи",
|
||||||
"label.views-per-visit": "Прегледи на посещение",
|
"label.views-per-visit": "Прегледи на посещение",
|
||||||
"label.visit-duration": "Visit duration",
|
"label.visit-duration": "Продължителност на посещение",
|
||||||
"label.visitors": "Посетители",
|
"label.visitors": "Посетители",
|
||||||
"label.visits": "Посещения",
|
"label.visits": "Посещения",
|
||||||
"label.website": "Уебсайт",
|
"label.website": "Уебсайт",
|
||||||
|
|
@ -292,8 +292,8 @@
|
||||||
"label.yesterday": "Вчера",
|
"label.yesterday": "Вчера",
|
||||||
"message.action-confirmation": "Въведете {confirmation} в полето по-долу, за да потвърдите.",
|
"message.action-confirmation": "Въведете {confirmation} в полето по-долу, за да потвърдите.",
|
||||||
"message.active-users": "{x} {x, plural, one {активен един} other {активни други}}",
|
"message.active-users": "{x} {x, plural, one {активен един} other {активни други}}",
|
||||||
"message.bad-request": "Bad request",
|
"message.bad-request": "Невалидна заявка",
|
||||||
"message.collected-data": "Collected data",
|
"message.collected-data": "Събрани данни",
|
||||||
"message.confirm-delete": "Сигурни ли сте, че искате да изтриете {target}?",
|
"message.confirm-delete": "Сигурни ли сте, че искате да изтриете {target}?",
|
||||||
"message.confirm-leave": "Сигурни ли сте, че искате да напуснете {target}?",
|
"message.confirm-leave": "Сигурни ли сте, че искате да напуснете {target}?",
|
||||||
"message.confirm-remove": "Сигурни ли сте, че искате да премахнете {target}?",
|
"message.confirm-remove": "Сигурни ли сте, че искате да премахнете {target}?",
|
||||||
|
|
@ -302,7 +302,7 @@
|
||||||
"message.delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.",
|
"message.delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.",
|
||||||
"message.error": "Възникна грешка.",
|
"message.error": "Възникна грешка.",
|
||||||
"message.event-log": "{event} на {url}",
|
"message.event-log": "{event} на {url}",
|
||||||
"message.forbidden": "Forbidden",
|
"message.forbidden": "Забранено",
|
||||||
"message.go-to-settings": "Отидете в настройките",
|
"message.go-to-settings": "Отидете в настройките",
|
||||||
"message.incorrect-username-password": "Неправилно потребителско име и/или парола.",
|
"message.incorrect-username-password": "Неправилно потребителско име и/или парола.",
|
||||||
"message.invalid-domain": "Невалиден домейн. Не включвайте http/https.",
|
"message.invalid-domain": "Невалиден домейн. Не включвайте http/https.",
|
||||||
|
|
@ -316,13 +316,13 @@
|
||||||
"message.no-teams": "Няма създадени екипи.",
|
"message.no-teams": "Няма създадени екипи.",
|
||||||
"message.no-users": "Няма потребители.",
|
"message.no-users": "Няма потребители.",
|
||||||
"message.no-websites-configured": "Нямате конфигурирани уебсайтове.",
|
"message.no-websites-configured": "Нямате конфигурирани уебсайтове.",
|
||||||
"message.not-found": "Not found",
|
"message.not-found": "Не е намерено",
|
||||||
"message.nothing-selected": "Nothing selected.",
|
"message.nothing-selected": "Няма избрано.",
|
||||||
"message.page-not-found": "Страницата не е намерена",
|
"message.page-not-found": "Страницата не е намерена",
|
||||||
"message.reset-website": "За да нулирате този уебсайт, въведете {confirmation} в полето по-долу, за да потвърдите.",
|
"message.reset-website": "За да нулирате този уебсайт, въведете {confirmation} в полето по-долу, за да потвърдите.",
|
||||||
"message.reset-website-warning": "Всички статистически данни за този уебсайт ще бъдат изтрити, но вашите настройки ще останат непроменени.",
|
"message.reset-website-warning": "Всички статистически данни за този уебсайт ще бъдат изтрити, но вашите настройки ще останат непроменени.",
|
||||||
"message.saved": "Запазено.",
|
"message.saved": "Запазено.",
|
||||||
"message.sever-error": "Server error",
|
"message.sever-error": "Сървърна грешка",
|
||||||
"message.share-url": "Статистиката за вашия уебсайт е публично достъпна на следния URL адрес:",
|
"message.share-url": "Статистиката за вашия уебсайт е публично достъпна на следния URL адрес:",
|
||||||
"message.team-already-member": "Вече сте член на екипа.",
|
"message.team-already-member": "Вече сте член на екипа.",
|
||||||
"message.team-not-found": "Екипът не е намерен.",
|
"message.team-not-found": "Екипът не е намерен.",
|
||||||
|
|
@ -332,7 +332,7 @@
|
||||||
"message.transfer-user-website-to-team": "Изберете екипът на който да бъде прехвърлен уебсайта.",
|
"message.transfer-user-website-to-team": "Изберете екипът на който да бъде прехвърлен уебсайта.",
|
||||||
"message.transfer-website": "Прехвърли собствеността на уебсайта към вашия акаунт или към друг екип.",
|
"message.transfer-website": "Прехвърли собствеността на уебсайта към вашия акаунт или към друг екип.",
|
||||||
"message.triggered-event": "Активирано събитие",
|
"message.triggered-event": "Активирано събитие",
|
||||||
"message.unauthorized": "Unauthorized",
|
"message.unauthorized": "Неоторизиран достъп",
|
||||||
"message.user-deleted": "Потребителят е изтрит.",
|
"message.user-deleted": "Потребителят е изтрит.",
|
||||||
"message.viewed-page": "Страницата е видяна",
|
"message.viewed-page": "Страницата е видяна",
|
||||||
"message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}"
|
"message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}"
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"label.behavior": "行動",
|
"label.behavior": "行動",
|
||||||
"label.boards": "ボード",
|
"label.boards": "ボード",
|
||||||
"label.bounce-rate": "直帰率",
|
"label.bounce-rate": "直帰率",
|
||||||
"label.breakdown": "故障",
|
"label.breakdown": "内訳",
|
||||||
"label.browser": "ブラウザ",
|
"label.browser": "ブラウザ",
|
||||||
"label.browsers": "ブラウザ",
|
"label.browsers": "ブラウザ",
|
||||||
"label.campaigns": "キャンペーン",
|
"label.campaigns": "キャンペーン",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
{
|
{
|
||||||
"label.access-code": "访问代码",
|
"label.access-code": "访问代码",
|
||||||
|
"label.account": "账户",
|
||||||
|
"label.action": "行为",
|
||||||
"label.actions": "用户行为",
|
"label.actions": "用户行为",
|
||||||
"label.activity": "活动日志",
|
"label.activity": "活动日志",
|
||||||
"label.add": "添加",
|
"label.add": "添加",
|
||||||
"label.add-board": "添加看板",
|
"label.add-board": "添加看板",
|
||||||
"label.add-description": "添加描述",
|
"label.add-description": "添加描述",
|
||||||
|
"label.add-link": "添加链接",
|
||||||
"label.add-member": "添加成员",
|
"label.add-member": "添加成员",
|
||||||
|
"label.add-pixel": "添加像素",
|
||||||
"label.add-step": "添加步骤",
|
"label.add-step": "添加步骤",
|
||||||
"label.add-website": "添加网站",
|
"label.add-website": "添加网站",
|
||||||
"label.admin": "管理员",
|
"label.admin": "管理员",
|
||||||
|
|
@ -13,10 +17,13 @@
|
||||||
"label.after": "之后",
|
"label.after": "之后",
|
||||||
"label.all": "所有",
|
"label.all": "所有",
|
||||||
"label.all-time": "所有时间段",
|
"label.all-time": "所有时间段",
|
||||||
|
"label.analysis": "分析",
|
||||||
"label.analytics": "分析",
|
"label.analytics": "分析",
|
||||||
|
"label.application": "应用",
|
||||||
"label.apply": "应用",
|
"label.apply": "应用",
|
||||||
"label.attribution": "归因",
|
"label.attribution": "归因",
|
||||||
"label.attribution-description": "查看用户如何与您的营销互动,以及是什么促成了转化。",
|
"label.attribution-description": "查看用户如何与您的营销互动,以及是什么促成了转化。",
|
||||||
|
"label.audience": "受众",
|
||||||
"label.average": "平均",
|
"label.average": "平均",
|
||||||
"label.back": "返回",
|
"label.back": "返回",
|
||||||
"label.before": "之前",
|
"label.before": "之前",
|
||||||
|
|
@ -29,11 +36,14 @@
|
||||||
"label.campaigns": "活动",
|
"label.campaigns": "活动",
|
||||||
"label.cancel": "取消",
|
"label.cancel": "取消",
|
||||||
"label.change-password": "修改密码",
|
"label.change-password": "修改密码",
|
||||||
|
"label.channel": "渠道",
|
||||||
"label.channels": "渠道",
|
"label.channels": "渠道",
|
||||||
|
"label.chart": "图表",
|
||||||
"label.cities": "市/县",
|
"label.cities": "市/县",
|
||||||
"label.city": "市/县",
|
"label.city": "市/县",
|
||||||
"label.clear-all": "清除全部",
|
"label.clear-all": "清除全部",
|
||||||
"label.cohort": "队列",
|
"label.cohort": "队列",
|
||||||
|
"label.cohorts": "队列",
|
||||||
"label.compare": "比较",
|
"label.compare": "比较",
|
||||||
"label.compare-dates": "比较日期",
|
"label.compare-dates": "比较日期",
|
||||||
"label.confirm": "确认",
|
"label.confirm": "确认",
|
||||||
|
|
@ -53,6 +63,7 @@
|
||||||
"label.create-user": "创建用户",
|
"label.create-user": "创建用户",
|
||||||
"label.created": "已创建",
|
"label.created": "已创建",
|
||||||
"label.created-by": "创建者",
|
"label.created-by": "创建者",
|
||||||
|
"label.criteria": "条件",
|
||||||
"label.currency": "货币",
|
"label.currency": "货币",
|
||||||
"label.current": "当前",
|
"label.current": "当前",
|
||||||
"label.current-password": "当前密码",
|
"label.current-password": "当前密码",
|
||||||
|
|
@ -70,24 +81,28 @@
|
||||||
"label.delete-website": "删除网站",
|
"label.delete-website": "删除网站",
|
||||||
"label.description": "描述",
|
"label.description": "描述",
|
||||||
"label.desktop": "台式机",
|
"label.desktop": "台式机",
|
||||||
|
"label.destination-url": "目标URL",
|
||||||
"label.details": "详细信息",
|
"label.details": "详细信息",
|
||||||
"label.device": "设备",
|
"label.device": "设备",
|
||||||
"label.devices": "设备",
|
"label.devices": "设备",
|
||||||
"label.direct": "直接",
|
"label.direct": "直接",
|
||||||
"label.dismiss": "关闭",
|
"label.dismiss": "关闭",
|
||||||
"label.distinct-id": "唯一ID",
|
"label.distinct-id": "唯一ID",
|
||||||
|
"label.documentation": "文档",
|
||||||
"label.does-not-contain": "不包含",
|
"label.does-not-contain": "不包含",
|
||||||
"label.does-not-include": "不包括",
|
"label.does-not-include": "不包括",
|
||||||
"label.doest-not-exist": "不存在",
|
"label.doest-not-exist": "不存在",
|
||||||
"label.domain": "域名",
|
"label.domain": "域名",
|
||||||
|
"label.download": "下载",
|
||||||
"label.dropoff": "丢弃",
|
"label.dropoff": "丢弃",
|
||||||
"label.edit": "编辑",
|
"label.edit": "编辑",
|
||||||
"label.edit-dashboard": "编辑仪表盘",
|
"label.edit-dashboard": "编辑仪表盘",
|
||||||
"label.edit-member": "编辑成员",
|
"label.edit-member": "编辑成员",
|
||||||
"label.email": "Email",
|
"label.email": "邮箱",
|
||||||
"label.enable-share-url": "启用共享链接",
|
"label.enable-share-url": "启用共享链接",
|
||||||
"label.end-step": "结束步骤",
|
"label.end-step": "结束步骤",
|
||||||
"label.entry": "入口 URL",
|
"label.entry": "入口 URL",
|
||||||
|
"label.environment": "环境",
|
||||||
"label.event": "事件",
|
"label.event": "事件",
|
||||||
"label.event-data": "事件数据",
|
"label.event-data": "事件数据",
|
||||||
"label.event-name": "事件名称",
|
"label.event-name": "事件名称",
|
||||||
|
|
@ -112,11 +127,13 @@
|
||||||
"label.greater-than": "大于",
|
"label.greater-than": "大于",
|
||||||
"label.greater-than-equals": "大于或等于",
|
"label.greater-than-equals": "大于或等于",
|
||||||
"label.grouped": "分组",
|
"label.grouped": "分组",
|
||||||
|
"label.growth": "增长",
|
||||||
"label.hostname": "主机名",
|
"label.hostname": "主机名",
|
||||||
"label.includes": "包括",
|
"label.includes": "包括",
|
||||||
"label.insight": "洞察",
|
"label.insight": "洞察",
|
||||||
"label.insights": "见解",
|
"label.insights": "见解",
|
||||||
"label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。",
|
"label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。",
|
||||||
|
"label.invalid-url": "无效URL",
|
||||||
"label.is": "等于",
|
"label.is": "等于",
|
||||||
"label.is-false": "否",
|
"label.is-false": "否",
|
||||||
"label.is-not": "不等于",
|
"label.is-not": "不等于",
|
||||||
|
|
@ -140,7 +157,9 @@
|
||||||
"label.leave-team": "离开团队",
|
"label.leave-team": "离开团队",
|
||||||
"label.less-than": "少于",
|
"label.less-than": "少于",
|
||||||
"label.less-than-equals": "少于等于",
|
"label.less-than-equals": "少于等于",
|
||||||
|
"label.link": "链接",
|
||||||
"label.links": "链接",
|
"label.links": "链接",
|
||||||
|
"label.location": "位置",
|
||||||
"label.login": "登录",
|
"label.login": "登录",
|
||||||
"label.logout": "退出",
|
"label.logout": "退出",
|
||||||
"label.manage": "管理",
|
"label.manage": "管理",
|
||||||
|
|
@ -161,7 +180,7 @@
|
||||||
"label.none": "无",
|
"label.none": "无",
|
||||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||||
"label.ok": "好的",
|
"label.ok": "好的",
|
||||||
"label.online": "Online",
|
"label.online": "在线",
|
||||||
"label.organic-search": "自然搜索",
|
"label.organic-search": "自然搜索",
|
||||||
"label.organic-shopping": "自然购物",
|
"label.organic-shopping": "自然购物",
|
||||||
"label.organic-social": "自然社交",
|
"label.organic-social": "自然社交",
|
||||||
|
|
@ -183,19 +202,22 @@
|
||||||
"label.password": "密码",
|
"label.password": "密码",
|
||||||
"label.path": "路径",
|
"label.path": "路径",
|
||||||
"label.paths": "路径",
|
"label.paths": "路径",
|
||||||
|
"label.pixel": "像素",
|
||||||
"label.pixels": "像素",
|
"label.pixels": "像素",
|
||||||
"label.powered-by": "由 {name} 提供支持",
|
"label.powered-by": "由 {name} 提供支持",
|
||||||
|
"label.preferences": "偏好",
|
||||||
"label.previous": "先前",
|
"label.previous": "先前",
|
||||||
"label.previous-period": "上一时期",
|
"label.previous-period": "上一时期",
|
||||||
"label.previous-year": "上一年",
|
"label.previous-year": "上一年",
|
||||||
"label.profile": "个人资料",
|
"label.profile": "个人资料",
|
||||||
|
"label.profiles": "个人资料",
|
||||||
"label.properties": "属性",
|
"label.properties": "属性",
|
||||||
"label.property": "属性",
|
"label.property": "属性",
|
||||||
"label.queries": "查询",
|
"label.queries": "查询",
|
||||||
"label.query": "查询",
|
"label.query": "查询",
|
||||||
"label.query-parameters": "查询参数",
|
"label.query-parameters": "查询参数",
|
||||||
"label.realtime": "实时",
|
"label.realtime": "实时",
|
||||||
"label.referral": "Referral",
|
"label.referral": "来源",
|
||||||
"label.referrer": "来源",
|
"label.referrer": "来源",
|
||||||
"label.referrers": "来源域名",
|
"label.referrers": "来源域名",
|
||||||
"label.refresh": "刷新",
|
"label.refresh": "刷新",
|
||||||
|
|
@ -216,8 +238,13 @@
|
||||||
"label.role": "角色",
|
"label.role": "角色",
|
||||||
"label.run-query": "查询",
|
"label.run-query": "查询",
|
||||||
"label.save": "保存",
|
"label.save": "保存",
|
||||||
|
"label.save-cohort": "保存为群组",
|
||||||
|
"label.save-segment": "保存为细分",
|
||||||
|
"label.screen": "屏幕",
|
||||||
"label.screens": "屏幕尺寸",
|
"label.screens": "屏幕尺寸",
|
||||||
"label.search": "搜索",
|
"label.search": "搜索",
|
||||||
|
"label.segment": "细分",
|
||||||
|
"label.segments": "细分",
|
||||||
"label.select": "选择",
|
"label.select": "选择",
|
||||||
"label.select-date": "选择日期",
|
"label.select-date": "选择日期",
|
||||||
"label.select-filter": "选择筛选器",
|
"label.select-filter": "选择筛选器",
|
||||||
|
|
@ -235,6 +262,9 @@
|
||||||
"label.start-step": "开始步骤",
|
"label.start-step": "开始步骤",
|
||||||
"label.steps": "步骤",
|
"label.steps": "步骤",
|
||||||
"label.sum": "总和",
|
"label.sum": "总和",
|
||||||
|
"label.support": "支持",
|
||||||
|
"label.switch-account": "切换账户",
|
||||||
|
"label.table": "表格",
|
||||||
"label.tablet": "平板",
|
"label.tablet": "平板",
|
||||||
"label.tag": "标签",
|
"label.tag": "标签",
|
||||||
"label.tags": "标签",
|
"label.tags": "标签",
|
||||||
|
|
@ -260,6 +290,7 @@
|
||||||
"label.total": "总数",
|
"label.total": "总数",
|
||||||
"label.total-records": "总记录数",
|
"label.total-records": "总记录数",
|
||||||
"label.tracking-code": "跟踪代码",
|
"label.tracking-code": "跟踪代码",
|
||||||
|
"label.traffic": "流量",
|
||||||
"label.transactions": "交易",
|
"label.transactions": "交易",
|
||||||
"label.transfer": "转移",
|
"label.transfer": "转移",
|
||||||
"label.transfer-website": "转移网站",
|
"label.transfer-website": "转移网站",
|
||||||
|
|
@ -292,7 +323,7 @@
|
||||||
"label.yesterday": "昨天",
|
"label.yesterday": "昨天",
|
||||||
"message.action-confirmation": "请在下方输入框中输入 {confirmation} 以确认操作。",
|
"message.action-confirmation": "请在下方输入框中输入 {confirmation} 以确认操作。",
|
||||||
"message.active-users": "当前在线 {x} 位访客",
|
"message.active-users": "当前在线 {x} 位访客",
|
||||||
"message.bad-request": "Bad request",
|
"message.bad-request": "请求错误",
|
||||||
"message.collected-data": "已收集的数据",
|
"message.collected-data": "已收集的数据",
|
||||||
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
||||||
"message.confirm-leave": "你确定要离开 {target} 吗?",
|
"message.confirm-leave": "你确定要离开 {target} 吗?",
|
||||||
|
|
@ -302,7 +333,7 @@
|
||||||
"message.delete-website-warning": "所有相关数据将会被删除。",
|
"message.delete-website-warning": "所有相关数据将会被删除。",
|
||||||
"message.error": "发生错误。",
|
"message.error": "发生错误。",
|
||||||
"message.event-log": "{url} 上的 {event}",
|
"message.event-log": "{url} 上的 {event}",
|
||||||
"message.forbidden": "Forbidden",
|
"message.forbidden": "禁止访问",
|
||||||
"message.go-to-settings": "去设置",
|
"message.go-to-settings": "去设置",
|
||||||
"message.incorrect-username-password": "用户名或密码不正确。",
|
"message.incorrect-username-password": "用户名或密码不正确。",
|
||||||
"message.invalid-domain": "无效域名",
|
"message.invalid-domain": "无效域名",
|
||||||
|
|
@ -316,13 +347,13 @@
|
||||||
"message.no-teams": "您尚未创建任何团队。",
|
"message.no-teams": "您尚未创建任何团队。",
|
||||||
"message.no-users": "暂无用户。",
|
"message.no-users": "暂无用户。",
|
||||||
"message.no-websites-configured": "你还没有设置任何网站。",
|
"message.no-websites-configured": "你还没有设置任何网站。",
|
||||||
"message.not-found": "Not found",
|
"message.not-found": "未找到",
|
||||||
"message.nothing-selected": "Nothing selected.",
|
"message.nothing-selected": "未选择",
|
||||||
"message.page-not-found": "页面未找到。",
|
"message.page-not-found": "页面未找到。",
|
||||||
"message.reset-website": "如确定要重置该网站,请在下面输入 {confirmation} 以确认。",
|
"message.reset-website": "如确定要重置该网站,请在下面输入 {confirmation} 以确认。",
|
||||||
"message.reset-website-warning": "此网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
|
"message.reset-website-warning": "此网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
|
||||||
"message.saved": "保存成功。",
|
"message.saved": "保存成功。",
|
||||||
"message.sever-error": "Server error",
|
"message.sever-error": "服务器错误",
|
||||||
"message.share-url": "这是 {target} 的共享链接。",
|
"message.share-url": "这是 {target} 的共享链接。",
|
||||||
"message.team-already-member": "你已是该团队的成员。",
|
"message.team-already-member": "你已是该团队的成员。",
|
||||||
"message.team-not-found": "未找到团队。",
|
"message.team-not-found": "未找到团队。",
|
||||||
|
|
@ -332,7 +363,7 @@
|
||||||
"message.transfer-user-website-to-team": "选择要转移此网站的团队。",
|
"message.transfer-user-website-to-team": "选择要转移此网站的团队。",
|
||||||
"message.transfer-website": "将网站所有权转移到您的账户或其他团队。",
|
"message.transfer-website": "将网站所有权转移到您的账户或其他团队。",
|
||||||
"message.triggered-event": "触发事件",
|
"message.triggered-event": "触发事件",
|
||||||
"message.unauthorized": "Unauthorized",
|
"message.unauthorized": "未授权",
|
||||||
"message.user-deleted": "用户已删除。",
|
"message.user-deleted": "用户已删除。",
|
||||||
"message.viewed-page": "已浏览页面",
|
"message.viewed-page": "已浏览页面",
|
||||||
"message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。"
|
"message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。"
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ function getDateStringSQL(data: any, unit: string = 'utc', timezone?: string) {
|
||||||
|
|
||||||
function getDateSQL(field: string, unit: string, timezone?: string) {
|
function getDateSQL(field: string, unit: string, timezone?: string) {
|
||||||
if (timezone) {
|
if (timezone) {
|
||||||
return `toDateTime(date_trunc('${unit}', ${field}, '${timezone}'))`;
|
return `toDateTime(date_trunc('${unit}', ${field}, '${timezone}'), '${timezone}')`;
|
||||||
}
|
}
|
||||||
return `toDateTime(date_trunc('${unit}', ${field}))`;
|
return `toDateTime(date_trunc('${unit}', ${field}))`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export const LOCALE_CONFIG = 'umami.locale';
|
||||||
export const TIMEZONE_CONFIG = 'umami.timezone';
|
export const TIMEZONE_CONFIG = 'umami.timezone';
|
||||||
export const DATE_RANGE_CONFIG = 'umami.date-range';
|
export const DATE_RANGE_CONFIG = 'umami.date-range';
|
||||||
export const THEME_CONFIG = 'umami.theme';
|
export const THEME_CONFIG = 'umami.theme';
|
||||||
|
export const CURRENCY_CONFIG = 'umami.currency';
|
||||||
export const DASHBOARD_CONFIG = 'umami.dashboard';
|
export const DASHBOARD_CONFIG = 'umami.dashboard';
|
||||||
export const LAST_TEAM_CONFIG = 'umami.last-team';
|
export const LAST_TEAM_CONFIG = 'umami.last-team';
|
||||||
export const VERSION_CHECK = 'umami.version-check';
|
export const VERSION_CHECK = 'umami.version-check';
|
||||||
|
|
@ -25,6 +26,7 @@ export const DEFAULT_WEBSITE_LIMIT = 10;
|
||||||
export const DEFAULT_RESET_DATE = '2000-01-01';
|
export const DEFAULT_RESET_DATE = '2000-01-01';
|
||||||
export const DEFAULT_PAGE_SIZE = 20;
|
export const DEFAULT_PAGE_SIZE = 20;
|
||||||
export const DEFAULT_DATE_COMPARE = 'prev';
|
export const DEFAULT_DATE_COMPARE = 'prev';
|
||||||
|
export const DEFAULT_CURRENCY = 'USD';
|
||||||
|
|
||||||
export const REALTIME_RANGE = 30;
|
export const REALTIME_RANGE = 30;
|
||||||
export const REALTIME_INTERVAL = 10000;
|
export const REALTIME_INTERVAL = 10000;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ const PROVIDER_HEADERS = [
|
||||||
regionHeader: 'cloudfront-viewer-country-region',
|
regionHeader: 'cloudfront-viewer-country-region',
|
||||||
cityHeader: 'cloudfront-viewer-city',
|
cityHeader: 'cloudfront-viewer-city',
|
||||||
},
|
},
|
||||||
|
// EdgeOne headers (requires custom request headers in Rule Priorities, see: https://edgeone.ai/document/46151)
|
||||||
|
{
|
||||||
|
countryHeader: 'eo-ipcountry',
|
||||||
|
regionHeader: 'eo-region-code',
|
||||||
|
cityHeader: 'eo-ipcity',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getDevice(userAgent: string, screen: string = '') {
|
export function getDevice(userAgent: string, screen: string = '') {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { DEFAULT_CURRENCY } from './constants';
|
||||||
|
|
||||||
export function parseTime(val: number) {
|
export function parseTime(val: number) {
|
||||||
const days = ~~(val / 86400);
|
const days = ~~(val / 86400);
|
||||||
const hours = ~~(val / 3600) - days * 24;
|
const hours = ~~(val / 3600) - days * 24;
|
||||||
|
|
@ -94,7 +96,7 @@ export function formatCurrency(value: number, currency: string, locale = 'en-US'
|
||||||
// Fallback to default currency format if an error occurs
|
// Fallback to default currency format if an error occurs
|
||||||
formattedValue = new Intl.NumberFormat(locale, {
|
formattedValue = new Intl.NumberFormat(locale, {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: 'USD',
|
currency: DEFAULT_CURRENCY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
|
|
||||||
export const IP_ADDRESS_HEADERS = [
|
export const IP_ADDRESS_HEADERS = [
|
||||||
'true-client-ip', // CDN
|
'true-client-ip', // CDN
|
||||||
'cf-connecting-ip', // Cloudflare
|
'cf-connecting-ip', // Cloudflare
|
||||||
|
|
@ -13,35 +15,87 @@ export const IP_ADDRESS_HEADERS = [
|
||||||
'x-forwarded',
|
'x-forwarded',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize IP strings to a canonical form:
|
||||||
|
* - strips IPv4-mapped IPv6 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
|
||||||
|
* - keeps valid IPv4/IPv6 as-is (canonically formatted by ipaddr.js)
|
||||||
|
*/
|
||||||
|
function normalizeIp(ip?: string | null) {
|
||||||
|
if (!ip) return ip;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = ipaddr.parse(ip);
|
||||||
|
|
||||||
|
if (parsed.kind() === 'ipv6' && (parsed as ipaddr.IPv6).isIPv4MappedAddress()) {
|
||||||
|
return (parsed as ipaddr.IPv6).toIPv4Address().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
// Fallback: return original if parsing fails
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveIp(ip?: string | null) {
|
||||||
|
if (!ip) return ip;
|
||||||
|
|
||||||
|
// First, try as-is
|
||||||
|
const normalized = normalizeIp(ip);
|
||||||
|
try {
|
||||||
|
ipaddr.parse(normalized);
|
||||||
|
return normalized;
|
||||||
|
} catch {
|
||||||
|
// try stripping port (handles IPv4:port; leaves IPv6 intact)
|
||||||
|
const stripped = stripPort(ip);
|
||||||
|
if (stripped !== ip) {
|
||||||
|
const normalizedStripped = normalizeIp(stripped);
|
||||||
|
try {
|
||||||
|
ipaddr.parse(normalizedStripped);
|
||||||
|
return normalizedStripped;
|
||||||
|
} catch {
|
||||||
|
return normalizedStripped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getIpAddress(headers: Headers) {
|
export function getIpAddress(headers: Headers) {
|
||||||
const customHeader = process.env.CLIENT_IP_HEADER;
|
const customHeader = process.env.CLIENT_IP_HEADER;
|
||||||
|
|
||||||
if (customHeader && headers.get(customHeader)) {
|
if (customHeader && headers.get(customHeader)) {
|
||||||
return headers.get(customHeader);
|
return resolveIp(headers.get(customHeader));
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = IP_ADDRESS_HEADERS.find(name => {
|
const header = IP_ADDRESS_HEADERS.find(name => headers.get(name));
|
||||||
return headers.get(name);
|
if (!header) {
|
||||||
});
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const ip = headers.get(header);
|
const ip = headers.get(header);
|
||||||
|
|
||||||
if (header === 'x-forwarded-for') {
|
if (header === 'x-forwarded-for') {
|
||||||
return ip?.split(',')?.[0]?.trim();
|
return resolveIp(ip?.split(',')?.[0]?.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header === 'forwarded') {
|
if (header === 'forwarded') {
|
||||||
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
|
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
return match[1];
|
return resolveIp(match[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ip;
|
return resolveIp(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stripPort(ip: string) {
|
export function stripPort(ip?: string | null) {
|
||||||
|
if (!ip) {
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
if (ip.startsWith('[')) {
|
if (ip.startsWith('[')) {
|
||||||
const endBracket = ip.indexOf(']');
|
const endBracket = ip.indexOf(']');
|
||||||
if (endBracket !== -1) {
|
if (endBracket !== -1) {
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,9 @@ export async function fetchSession(websiteId: string, sessionId: string): Promis
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAccount(userId: string) {
|
||||||
|
const account = await redis.client.get(`account:${userId}`);
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,15 +74,21 @@ function getSearchSQL(column: string, param: string = 'search'): string {
|
||||||
function mapFilter(column: string, operator: string, name: string, type: string = '') {
|
function mapFilter(column: string, operator: string, name: string, type: string = '') {
|
||||||
const value = `{{${name}${type ? `::${type}` : ''}}}`;
|
const value = `{{${name}${type ? `::${type}` : ''}}}`;
|
||||||
|
|
||||||
|
if (name.startsWith('cohort_')) {
|
||||||
|
name = name.slice('cohort_'.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = SESSION_COLUMNS.includes(name) ? 'session' : 'website_event';
|
||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case OPERATORS.equals:
|
case OPERATORS.equals:
|
||||||
return `${column} = ${value}`;
|
return `${table}.${column} = ${value}`;
|
||||||
case OPERATORS.notEquals:
|
case OPERATORS.notEquals:
|
||||||
return `${column} != ${value}`;
|
return `${table}.${column} != ${value}`;
|
||||||
case OPERATORS.contains:
|
case OPERATORS.contains:
|
||||||
return `${column} ilike ${value}`;
|
return `${table}.${column} ilike ${value}`;
|
||||||
case OPERATORS.doesNotContain:
|
case OPERATORS.doesNotContain:
|
||||||
return `${column} not ilike ${value}`;
|
return `${table}.${column} not ilike ${value}`;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
|
import { startOfMonth, subMonths } from 'date-fns';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { checkAuth } from '@/lib/auth';
|
import { checkAuth } from '@/lib/auth';
|
||||||
import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS } from '@/lib/constants';
|
import { DEFAULT_PAGE_SIZE, FILTER_COLUMNS } from '@/lib/constants';
|
||||||
import { getAllowedUnits, getMinimumUnit, maxDate, parseDateRange } from '@/lib/date';
|
import { getAllowedUnits, getMinimumUnit, maxDate, parseDateRange } from '@/lib/date';
|
||||||
import { fetchWebsite } from '@/lib/load';
|
import { fetchAccount, fetchWebsite } from '@/lib/load';
|
||||||
import { filtersArrayToObject } from '@/lib/params';
|
import { filtersArrayToObject } from '@/lib/params';
|
||||||
import { badRequest, unauthorized } from '@/lib/response';
|
import { badRequest, unauthorized } from '@/lib/response';
|
||||||
import type { QueryFilters } from '@/lib/types';
|
import type { QueryFilters } from '@/lib/types';
|
||||||
|
|
@ -16,7 +17,7 @@ export async function parseRequest(
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
let query = Object.fromEntries(url.searchParams);
|
let query = Object.fromEntries(url.searchParams);
|
||||||
let body = await getJsonBody(request);
|
let body = await getJsonBody(request);
|
||||||
let error: () => undefined | undefined;
|
let error: () => undefined | undefined | Response;
|
||||||
let auth = null;
|
let auth = null;
|
||||||
|
|
||||||
if (schema) {
|
if (schema) {
|
||||||
|
|
@ -80,8 +81,17 @@ export function getRequestFilters(query: Record<string, any>) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setWebsiteDate(websiteId: string, data: Record<string, any>) {
|
export async function setWebsiteDate(websiteId: string, userId: string, data: Record<string, any>) {
|
||||||
const website = await fetchWebsite(websiteId);
|
const website = await fetchWebsite(websiteId);
|
||||||
|
const cloudMode = !!process.env.CLOUD_MODE;
|
||||||
|
|
||||||
|
if (cloudMode && website && !website.teamId) {
|
||||||
|
const account = await fetchAccount(userId);
|
||||||
|
|
||||||
|
if (!account?.hasSubscription) {
|
||||||
|
data.startDate = maxDate(data.startDate, startOfMonth(subMonths(new Date(), 6)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (website?.resetAt) {
|
if (website?.resetAt) {
|
||||||
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
|
data.startDate = maxDate(data.startDate, new Date(website?.resetAt));
|
||||||
|
|
@ -93,12 +103,13 @@ export async function setWebsiteDate(websiteId: string, data: Record<string, any
|
||||||
export async function getQueryFilters(
|
export async function getQueryFilters(
|
||||||
params: Record<string, any>,
|
params: Record<string, any>,
|
||||||
websiteId?: string,
|
websiteId?: string,
|
||||||
|
userId?: string,
|
||||||
): Promise<QueryFilters> {
|
): Promise<QueryFilters> {
|
||||||
const dateRange = getRequestDateRange(params);
|
const dateRange = getRequestDateRange(params);
|
||||||
const filters = getRequestFilters(params);
|
const filters = getRequestFilters(params);
|
||||||
|
|
||||||
if (websiteId) {
|
if (websiteId) {
|
||||||
await setWebsiteDate(websiteId, dateRange);
|
await setWebsiteDate(websiteId, userId, dateRange);
|
||||||
|
|
||||||
if (params.segment) {
|
if (params.segment) {
|
||||||
const segmentParams = (await getWebsiteSegment(websiteId, params.segment))
|
const segmentParams = (await getWebsiteSegment(websiteId, params.segment))
|
||||||
|
|
|
||||||
|
|
@ -132,42 +132,46 @@ export async function updateWebsite(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetWebsite(websiteId: string) {
|
export async function resetWebsite(websiteId: string) {
|
||||||
const { client, transaction } = prisma;
|
const { transaction } = prisma;
|
||||||
const cloudMode = !!process.env.CLOUD_MODE;
|
const cloudMode = !!process.env.CLOUD_MODE;
|
||||||
|
|
||||||
return transaction(
|
return transaction(
|
||||||
[
|
async tx => {
|
||||||
client.revenue.deleteMany({
|
await tx.revenue.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.eventData.deleteMany({
|
|
||||||
|
await tx.eventData.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.sessionData.deleteMany({
|
|
||||||
|
await tx.sessionData.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.websiteEvent.deleteMany({
|
|
||||||
|
await tx.websiteEvent.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.session.deleteMany({
|
|
||||||
|
await tx.session.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.website.update({
|
|
||||||
|
const website = await tx.website.update({
|
||||||
where: { id: websiteId },
|
where: { id: websiteId },
|
||||||
data: {
|
data: {
|
||||||
resetAt: new Date(),
|
resetAt: new Date(),
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
],
|
|
||||||
|
return website;
|
||||||
|
},
|
||||||
{
|
{
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
).then(async data => {
|
).then(async data => {
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
await redis.client.set(
|
await redis.client.set(`website:${websiteId}`, data);
|
||||||
`website:${websiteId}`,
|
|
||||||
data.find(website => website.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -175,43 +179,52 @@ export async function resetWebsite(websiteId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteWebsite(websiteId: string) {
|
export async function deleteWebsite(websiteId: string) {
|
||||||
const { client, transaction } = prisma;
|
const { transaction } = prisma;
|
||||||
const cloudMode = !!process.env.CLOUD_MODE;
|
const cloudMode = !!process.env.CLOUD_MODE;
|
||||||
|
|
||||||
return transaction(
|
return transaction(
|
||||||
[
|
async tx => {
|
||||||
client.revenue.deleteMany({
|
await tx.revenue.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.eventData.deleteMany({
|
|
||||||
|
await tx.eventData.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.sessionData.deleteMany({
|
|
||||||
|
await tx.sessionData.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.websiteEvent.deleteMany({
|
|
||||||
|
await tx.websiteEvent.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.session.deleteMany({
|
|
||||||
|
await tx.session.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.report.deleteMany({
|
|
||||||
|
await tx.report.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
client.segment.deleteMany({
|
|
||||||
|
await tx.segment.deleteMany({
|
||||||
where: { websiteId },
|
where: { websiteId },
|
||||||
}),
|
});
|
||||||
cloudMode
|
|
||||||
? client.website.update({
|
const website = cloudMode
|
||||||
|
? await tx.website.update({
|
||||||
data: {
|
data: {
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
},
|
},
|
||||||
where: { id: websiteId },
|
where: { id: websiteId },
|
||||||
})
|
})
|
||||||
: client.website.delete({
|
: await tx.website.delete({
|
||||||
where: { id: websiteId },
|
where: { id: websiteId },
|
||||||
}),
|
});
|
||||||
],
|
|
||||||
|
return website;
|
||||||
|
},
|
||||||
{
|
{
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ async function relationalQuery(
|
||||||
select distinct
|
select distinct
|
||||||
website_event.visit_id,
|
website_event.visit_id,
|
||||||
website_event.referrer_path,
|
website_event.referrer_path,
|
||||||
coalesce(nullIf(website_event.event_name, ''), website_event.url_path) event,
|
coalesce(nullIf(website_event.event_name, ''), website_event.url_path) "event",
|
||||||
row_number() OVER (PARTITION BY visit_id ORDER BY website_event.created_at) AS event_number
|
row_number() OVER (PARTITION BY visit_id ORDER BY website_event.created_at) AS event_number
|
||||||
from website_event
|
from website_event
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,15 @@ async function relationalQuery(
|
||||||
currency,
|
currency,
|
||||||
});
|
});
|
||||||
|
|
||||||
const joinQuery = filterQuery
|
const joinQuery =
|
||||||
? `join website_event
|
filterQuery || cohortQuery
|
||||||
|
? `join website_event
|
||||||
on website_event.website_id = revenue.website_id
|
on website_event.website_id = revenue.website_id
|
||||||
and website_event.session_id = revenue.session_id
|
and website_event.session_id = revenue.session_id
|
||||||
and website_event.event_id = revenue.event_id
|
and website_event.event_id = revenue.event_id
|
||||||
and website_event.website_id = {{websiteId::uuid}}
|
and website_event.website_id = {{websiteId::uuid}}
|
||||||
and website_event.created_at between {{startDate}} and {{endDate}}`
|
and website_event.created_at between {{startDate}} and {{endDate}}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const chart = await rawQuery(
|
const chart = await rawQuery(
|
||||||
`
|
`
|
||||||
|
|
@ -62,7 +63,7 @@ async function relationalQuery(
|
||||||
${joinSessionQuery}
|
${joinSessionQuery}
|
||||||
where revenue.website_id = {{websiteId::uuid}}
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
and revenue.currency = upper({{currency}})
|
and upper(revenue.currency) = {{currency}}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by x, t
|
group by x, t
|
||||||
order by t
|
order by t
|
||||||
|
|
@ -83,7 +84,7 @@ async function relationalQuery(
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
where revenue.website_id = {{websiteId::uuid}}
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
and revenue.currency = upper({{currency}})
|
and upper(revenue.currency) = {{currency}}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by session.country
|
group by session.country
|
||||||
`,
|
`,
|
||||||
|
|
@ -102,7 +103,7 @@ async function relationalQuery(
|
||||||
${joinSessionQuery}
|
${joinSessionQuery}
|
||||||
where revenue.website_id = {{websiteId::uuid}}
|
where revenue.website_id = {{websiteId::uuid}}
|
||||||
and revenue.created_at between {{startDate}} and {{endDate}}
|
and revenue.created_at between {{startDate}} and {{endDate}}
|
||||||
and revenue.currency = upper({{currency}})
|
and upper(revenue.currency) = {{currency}}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
`,
|
`,
|
||||||
queryParams,
|
queryParams,
|
||||||
|
|
@ -154,7 +155,7 @@ async function clickhouseQuery(
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
where website_revenue.website_id = {websiteId:UUID}
|
where website_revenue.website_id = {websiteId:UUID}
|
||||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
and website_revenue.currency = upper({currency:String})
|
and upper(website_revenue.currency) = {currency:String}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by x, t
|
group by x, t
|
||||||
order by t
|
order by t
|
||||||
|
|
@ -182,7 +183,7 @@ async function clickhouseQuery(
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
where website_revenue.website_id = {websiteId:UUID}
|
where website_revenue.website_id = {websiteId:UUID}
|
||||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
and website_revenue.currency = upper({currency:String})
|
and upper(website_revenue.currency) = {currency:String}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by website_event.country
|
group by website_event.country
|
||||||
order by value desc
|
order by value desc
|
||||||
|
|
@ -205,7 +206,7 @@ async function clickhouseQuery(
|
||||||
${cohortQuery}
|
${cohortQuery}
|
||||||
where website_revenue.website_id = {websiteId:UUID}
|
where website_revenue.website_id = {websiteId:UUID}
|
||||||
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and website_revenue.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
and website_revenue.currency = upper({currency:String})
|
and upper(website_revenue.currency) = {currency:String}
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
`,
|
`,
|
||||||
queryParams,
|
queryParams,
|
||||||
|
|
|
||||||
|
|
@ -46,31 +46,23 @@ export async function relationalQuery({
|
||||||
createdAt,
|
createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const existing = await client.sessionData.findMany({
|
|
||||||
where: {
|
|
||||||
sessionId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
sessionId: true,
|
|
||||||
dataKey: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const data of flattenedData) {
|
for (const data of flattenedData) {
|
||||||
const { sessionId, dataKey, ...props } = data;
|
const { sessionId, dataKey, ...props } = data;
|
||||||
const record = existing.find(e => e.sessionId === sessionId && e.dataKey === dataKey);
|
|
||||||
|
|
||||||
if (record) {
|
// Try to update existing record using compound where clause
|
||||||
await client.sessionData.update({
|
// This is safer than using id from a previous query due to race conditions
|
||||||
where: {
|
const updateResult = await client.sessionData.updateMany({
|
||||||
id: record.id,
|
where: {
|
||||||
},
|
sessionId,
|
||||||
data: {
|
dataKey,
|
||||||
...props,
|
},
|
||||||
},
|
data: {
|
||||||
});
|
...props,
|
||||||
} else {
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no record was updated, create a new one
|
||||||
|
if (updateResult.count === 0) {
|
||||||
await client.sessionData.create({
|
await client.sessionData.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@
|
||||||
if (!currentScript) return;
|
if (!currentScript) return;
|
||||||
|
|
||||||
const { hostname, href, origin } = location;
|
const { hostname, href, origin } = location;
|
||||||
const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
|
|
||||||
|
let localStorage;
|
||||||
|
try {
|
||||||
|
localStorage = href.startsWith('data:') ? undefined : window.localStorage;
|
||||||
|
} catch {
|
||||||
|
/* (DOMException) SecurityError: Access is denied for this document. */
|
||||||
|
}
|
||||||
|
|
||||||
const _data = 'data-';
|
const _data = 'data-';
|
||||||
const _false = 'false';
|
const _false = 'false';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue