Compare commits

...

56 commits

Author SHA1 Message Date
Mike Cao
aefc36b476 Merge branch 'dev' into boards
Some checks failed
Node.js CI / build (push) Has been cancelled
# Conflicts:
#	pnpm-lock.yaml
2026-01-07 01:01:39 -08:00
Mike Cao
5213e04f44
Merge pull request #3866 from RaenonX/master
Some checks are pending
Node.js CI / build (push) Waiting to run
Added custom slug for links
2026-01-06 18:23:28 -08:00
Mike Cao
17d24bf8e2
Merge pull request #3872 from GochoMugo/fix/denied-storage-access
fix: handle denied storage access
2026-01-06 18:03:16 -08:00
Mike Cao
767fda21cd
Merge pull request #3874 from maphubs/fix-session-error
fix prisma session race condition error
2026-01-06 18:01:09 -08:00
Mike Cao
c122fb718b
Merge pull request #3875 from FEgor04/master
fix(events): use correct key for event values
2026-01-06 17:50:24 -08:00
Mike Cao
f105a52fc2
Merge pull request #3884 from XahidEx/patch-2
Correct UAE emirate names in iso-3166-2.json
2026-01-06 17:48:29 -08:00
Mike Cao
5506314e54
Merge pull request #3891 from journry789/master
This resolves the issue of being unable to obtain the client's IP add…
2026-01-06 17:46:04 -08:00
Mike Cao
e90b2201ca
Merge pull request #3917 from Mintimate/master
feat: add EdgeOne CDN geolocation headers support
2026-01-06 17:32:48 -08:00
Mike Cao
d8dcf05a20
Merge pull request #3920 from PaiJi/chore/update-zh-CN-translation
chore(i18n): update zh-CN translation
2026-01-06 17:29:43 -08:00
Mike Cao
2e62a06aa4
Merge pull request #3926 from Yashh56/feat/default-currency
feat: Add default currency support and update currency handling in Revenue component
2026-01-06 17:27:47 -08:00
Mike Cao
75ae7528fb
Merge pull request #3929 from dyanakiev/master
chore(i18n): update bg-BG translation
2026-01-06 17:25:35 -08:00
Mike Cao
58880b6a5f
Merge pull request #3939 from AymanAlSuleihi/master
Fix double scrollbar in dropdown
2026-01-06 17:24:38 -08:00
AymanAlSuleihi
491716f4dd Fix double scrollbar in dropdown 2026-01-03 00:48:09 +00:00
Mike Cao
42d0594118
Merge pull request #3936 from sbozh/fix/compatible-event-name
Some checks failed
Node.js CI / build (push) Has been cancelled
Fixed PostgreSQL 12/13 syntax error in Journeys feature
2026-01-02 13:03:34 -08:00
Mike Cao
34c31ca63c
Merge pull request #3937 from AymanAlSuleihi/master
Add country flag image for Tor traffic (T1)
2026-01-02 13:02:35 -08:00
AymanAlSuleihi
dacf13475a Add country image t1.png for Tor 2026-01-01 15:01:04 +00:00
SBOZH
8286af1453 Fixed PostgreSQL 12/13 syntax error in Journeys feature 2026-01-01 15:53:20 +01:00
Francis Cao
34677bca8f
Merge pull request #3925 from Yashh56/feat/version
Some checks failed
Node.js CI / build (push) Has been cancelled
feat : Add version settings to display application version
2025-12-30 13:51:09 -08:00
Yash
b0aa6fd6ef feat: Add current version to API response 2025-12-26 22:13:23 +05:30
Dimitar Yanakiev
97c26bc075 Translated various labels and messages in Bulgarian. 2025-12-26 16:34:38 +02:00
Yash
4eddac21c7 feat: Add default currency support and update currency handling in Revenue component 2025-12-25 20:41:14 +05:30
Yash
5e3e6b3edd refactor: Simplify version display by removing API endpoint and using constant 2025-12-25 09:48:09 +05:30
Yash
612b00179b feat : Add version settings and API endpoint to display application version 2025-12-25 00:21:10 +05:30
JiPai
783098fadc chore(i18n): update zh-CN translation 2025-12-23 23:04:41 +08:00
JiPai
6859f00bf6 chore(i18n): update zh-CN translation 2025-12-23 22:39:33 +08:00
Francis Cao
b0ed4bddb6 change reset/delete website to interactive transaction with timeout
Some checks failed
Node.js CI / build (push) Has been cancelled
Closes #3905
2025-12-22 10:59:33 -08:00
Mintihuang
ad264f941d feat: add EdgeOne headers for geolocation detection​ 2025-12-22 11:35:28 +00:00
Mike Cao
4c0c9e6aa0
Merge pull request #3910 from kkhys/fix/ja-jp-breakdown
Some checks failed
Node.js CI / build (push) Has been cancelled
Fix Japanese translation for label 'breakdown'
2025-12-20 21:39:29 -08:00
kkhys
687318bd09
fix Japanese translation for label 'breakdown' 2025-12-20 11:58:25 +09:00
Francis Cao
912d2d544d Fix deleted website visibility bug
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
Closes #3865
2025-12-19 14:21:42 -08:00
Francis Cao
3072f02f1b Add table alias to filterQuery. Closed  #3869
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-12-19 13:32:20 -08:00
Francis Cao
86d2672c47 fix shareId error
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-12-18 11:33:29 -08:00
Francis Cao
741c6039e6 refactor 6 month retention. use auth instead of cache:website 2025-12-18 10:35:52 -08:00
Francis Cao
37b6194c5f set 6 month retention for hobby users
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-12-17 13:28:34 -08:00
Francis Cao
b75c15dc43 Merge branch 'analytics' of https://github.com/umami-software/umami into dev
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-12-17 09:35:51 -08:00
Francis Cao
d04fff65fe fix monthly truncation timezone issue
Some checks are pending
Create docker images (cloud) / Build, push, and deploy (push) Waiting to run
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
2025-12-17 09:34:59 -08:00
journry789
437c168e6f This resolves the issue of being unable to obtain the client's IP address due to the IPv6 format. 2025-12-15 13:56:39 +08:00
Jahidul Islam
886544f297
Correct UAE emirate names in iso-3166-2.json
Updated names of UAE emirates for accuracy.
2025-12-13 16:14:27 +04:00
RaenonX
53dfc5e76a
Added pm2.yml in .gitignore 2025-12-12 23:04:45 +08:00
RaenonX
5fbef149d0
Added custom slug for links 2025-12-12 23:04:44 +08:00
Mike Cao
860e6390f1 Updated Docker build.
Some checks failed
Node.js CI / build (push) Has been cancelled
2025-12-11 20:16:56 -08:00
Mike Cao
9b0d1b092e Updated worfkflow. 2025-12-11 19:31:36 -08:00
Mike Cao
2a71cc721b Fixed CI build. 2025-12-11 18:25:23 -08:00
Mike Cao
7bea47d9e8 Bumped version v3.0.3. Updated workflow. 2025-12-11 18:04:10 -08:00
Mike Cao
e9cdabab5a Upgraded react and next. 2025-12-11 17:58:20 -08:00
Egor Fedorov
5b97fb908a
fix(events): use correct key for event values 2025-12-11 23:24:01 +03:00
Kristofor Carle
b088a2ee6e fix prisma session race condition error 2025-12-11 14:53:08 -05:00
GochoMugo
8cc571f548
fix: handle denied storage access 2025-12-11 08:53:13 +03:00
Francis Cao
59a16e719b update bug template
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-12-09 10:35:10 -08:00
Francis Cao
220c2af344 fix revenue type
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-12-05 09:18:53 -08:00
Mike Cao
7c804b10ea
Merge pull request #3834 from umami-software/dependabot/npm_and_yarn/jws-3.2.3
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
Bump jws from 3.2.2 to 3.2.3
2025-12-04 17:50:55 -08:00
Francis Cao
83b03d682c fix getRevenue case-insensitivity bug
Some checks failed
Create docker images (cloud) / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-12-04 12:27:44 -08:00
dependabot[bot]
aaa8b5b6c9
Bump jws from 3.2.2 to 3.2.3
Bumps [jws](https://github.com/brianloveswords/node-jws) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/brianloveswords/node-jws/releases)
- [Changelog](https://github.com/auth0/node-jws/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianloveswords/node-jws/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: jws
  dependency-version: 3.2.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 17:37:01 +00:00
Mike Cao
81e27fc18c
Fix formatting for Docker compose instructions
Some checks failed
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled
2025-12-04 00:37:13 -08:00
Mike Cao
2b771ff2b4
Merge pull request #3832 from umami-software/dev
v3.0.2
2025-12-03 23:54:31 -08:00
Mike Cao
7e42b5b35e
Merge pull request #3827 from umami-software/dependabot/npm_and_yarn/next-15.5.7
Some checks are pending
Node.js CI / build (postgresql, 18.18, 10) (push) Waiting to run
Bump next from 15.5.3 to 15.5.7
2025-12-03 21:27:43 -08:00
64 changed files with 699 additions and 319 deletions

View file

@ -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'

View file

@ -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

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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": [

View file

@ -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",

View file

@ -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>

View file

@ -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} />;
} }

View file

@ -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} />;
} }

View file

@ -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>
); );
} }

View 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>;
}

View file

@ -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 && (

View file

@ -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>;
} }

View file

@ -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,
}); });
} }

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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: [],

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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),

View file

@ -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}`;

View file

@ -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}`;

View file

@ -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),

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);
} }

View file

@ -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);

View file

@ -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`, {

View file

@ -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>}

View file

@ -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' },

View file

@ -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}"

View file

@ -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": "キャンペーン",

View file

@ -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} 浏览器进行访问。"

View file

@ -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}))`;
} }

View file

@ -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;

View file

@ -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 = '') {

View file

@ -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,
}); });
} }

View file

@ -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) {

View file

@ -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;
}

View file

@ -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 '';
} }

View file

@ -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))

View file

@ -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,
}, },

View file

@ -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}

View file

@ -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,

View file

@ -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,
}); });

View file

@ -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';