Compare commits

..

No commits in common. "5213e04f444297b47eb3e5c7d8f5c8d1d68417f9" and "42d0594118b67d916e2c9a1d7e8e50dc7b72a5d9" have entirely different histories.

16 changed files with 160 additions and 452 deletions

91
.gitignore vendored
View file

@ -1,46 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
.pnpm-store
package-lock.json
# testing
/coverage
# next.js
/.next
/out
# production
/build
/public/script.js
/geo
/dist
/generated
/src/generated
pm2.yml
# misc
.DS_Store
.idea
.yarn
*.iml
*.log
.vscode
.tool-versions
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.*
*.env.*
*.dev.yml
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
.pnpm-store
package-lock.json
# testing
/coverage
# next.js
/.next
/out
# production
/build
/public/script.js
/geo
/dist
/generated
/src/generated
# misc
.DS_Store
.idea
.yarn
*.iml
*.log
.vscode
.tool-versions
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.*
*.env.*
*.dev.yml

View file

@ -8,7 +8,6 @@ const cloudMode = process.env.CLOUD_MODE || '';
const cloudUrl = process.env.CLOUD_URL || '';
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
const corsMaxAge = process.env.CORS_MAX_AGE || '';
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
const defaultLocale = process.env.DEFAULT_LOCALE || '';
const forceSSL = process.env.FORCE_SSL || '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
@ -171,7 +170,6 @@ export default {
cloudMode,
cloudUrl,
currentVersion: pkg.version,
defaultCurrency,
defaultLocale,
},
basePath,

View file

@ -5,18 +5,6 @@
"value": "访问代码"
}
],
"label.account": [
{
"type": 0,
"value": "账户"
}
],
"label.action": [
{
"type": 0,
"value": "行为"
}
],
"label.actions": [
{
"type": 0,
@ -47,24 +35,12 @@
"value": "添加描述"
}
],
"label.add-link": [
{
"type": 0,
"value": "添加链接"
}
],
"label.add-member": [
{
"type": 0,
"value": "添加成员"
}
],
"label.add-pixel": [
{
"type": 0,
"value": "添加像素"
}
],
"label.add-step": [
{
"type": 0,
@ -107,24 +83,12 @@
"value": "所有时间段"
}
],
"label.analysis": [
{
"type": 0,
"value": "分析"
}
],
"label.analytics": [
{
"type": 0,
"value": "分析"
}
],
"label.application": [
{
"type": 0,
"value": "应用"
}
],
"label.apply": [
{
"type": 0,
@ -143,12 +107,6 @@
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
}
],
"label.audience": [
{
"type": 0,
"value": "受众"
}
],
"label.average": [
{
"type": 0,
@ -167,12 +125,6 @@
"value": "之前"
}
],
"label.behavior": [
{
"type": 0,
"value": "行为"
}
],
"label.boards": [
{
"type": 0,
@ -221,24 +173,12 @@
"value": "修改密码"
}
],
"label.channel": [
{
"type": 0,
"value": "渠道"
}
],
"label.channels": [
{
"type": 0,
"value": "渠道"
}
],
"label.chart": [
{
"type": 0,
"value": "图表"
}
],
"label.cities": [
{
"type": 0,
@ -263,12 +203,6 @@
"value": "队列"
}
],
"label.cohorts": [
{
"type": 0,
"value": "队列"
}
],
"label.compare": [
{
"type": 0,
@ -383,12 +317,6 @@
"value": "创建者"
}
],
"label.criteria": [
{
"type": 0,
"value": "条件"
}
],
"label.currency": [
{
"type": 0,
@ -491,12 +419,6 @@
"value": "台式机"
}
],
"label.destination-url": [
{
"type": 0,
"value": "目标URL"
}
],
"label.details": [
{
"type": 0,
@ -533,12 +455,6 @@
"value": "唯一ID"
}
],
"label.documentation": [
{
"type": 0,
"value": "文档"
}
],
"label.does-not-contain": [
{
"type": 0,
@ -563,12 +479,6 @@
"value": "域名"
}
],
"label.download": [
{
"type": 0,
"value": "下载"
}
],
"label.dropoff": [
{
"type": 0,
@ -596,7 +506,7 @@
"label.email": [
{
"type": 0,
"value": "邮箱"
"value": "Email"
}
],
"label.enable-share-url": [
@ -617,12 +527,6 @@
"value": "入口 URL"
}
],
"label.environment": [
{
"type": 0,
"value": "环境"
}
],
"label.event": [
{
"type": 0,
@ -767,12 +671,6 @@
"value": "分组"
}
],
"label.growth": [
{
"type": 0,
"value": "增长"
}
],
"label.hostname": [
{
"type": 0,
@ -803,12 +701,6 @@
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
}
],
"label.invalid-url": [
{
"type": 0,
"value": "无效URL"
}
],
"label.is": [
{
"type": 0,
@ -971,24 +863,12 @@
"value": "少于等于"
}
],
"label.link": [
{
"type": 0,
"value": "链接"
}
],
"label.links": [
{
"type": 0,
"value": "链接"
}
],
"label.location": [
{
"type": 0,
"value": "位置"
}
],
"label.login": [
{
"type": 0,
@ -1140,7 +1020,7 @@
"label.online": [
{
"type": 0,
"value": "在线"
"value": "Online"
}
],
"label.organic-search": [
@ -1285,12 +1165,6 @@
"value": "路径"
}
],
"label.pixel": [
{
"type": 0,
"value": "像素"
}
],
"label.pixels": [
{
"type": 0,
@ -1311,12 +1185,6 @@
"value": " 提供支持"
}
],
"label.preferences": [
{
"type": 0,
"value": "偏好"
}
],
"label.previous": [
{
"type": 0,
@ -1341,12 +1209,6 @@
"value": "个人资料"
}
],
"label.profiles": [
{
"type": 0,
"value": "个人资料"
}
],
"label.properties": [
{
"type": 0,
@ -1386,7 +1248,7 @@
"label.referral": [
{
"type": 0,
"value": "来源"
"value": "Referral"
}
],
"label.referrer": [
@ -1509,24 +1371,6 @@
"value": "保存"
}
],
"label.save-cohort": [
{
"type": 0,
"value": "保存为群组"
}
],
"label.save-segment": [
{
"type": 0,
"value": "保存为细分"
}
],
"label.screen": [
{
"type": 0,
"value": "屏幕"
}
],
"label.screens": [
{
"type": 0,
@ -1539,18 +1383,6 @@
"value": "搜索"
}
],
"label.segment": [
{
"type": 0,
"value": "细分"
}
],
"label.segments": [
{
"type": 0,
"value": "细分"
}
],
"label.select": [
{
"type": 0,
@ -1653,24 +1485,6 @@
"value": "总和"
}
],
"label.support": [
{
"type": 0,
"value": "支持"
}
],
"label.switch-account": [
{
"type": 0,
"value": "切换账户"
}
],
"label.table": [
{
"type": 0,
"value": "表格"
}
],
"label.tablet": [
{
"type": 0,
@ -1821,12 +1635,6 @@
"value": "跟踪代码"
}
],
"label.traffic": [
{
"type": 0,
"value": "流量"
}
],
"label.transactions": [
{
"type": 0,
@ -2038,7 +1846,7 @@
"message.bad-request": [
{
"type": 0,
"value": "请求错误"
"value": "Bad request"
}
],
"message.collected-data": [
@ -2138,7 +1946,7 @@
"message.forbidden": [
{
"type": 0,
"value": "禁止访问"
"value": "Forbidden"
}
],
"message.go-to-settings": [
@ -2238,13 +2046,13 @@
"message.not-found": [
{
"type": 0,
"value": "未找到"
"value": "Not found"
}
],
"message.nothing-selected": [
{
"type": 0,
"value": "未选择"
"value": "Nothing selected."
}
],
"message.page-not-found": [
@ -2282,7 +2090,7 @@
"message.sever-error": [
{
"type": 0,
"value": "服务器错误"
"value": "Server error"
}
],
"message.share-url": [
@ -2350,7 +2158,7 @@
"message.unauthorized": [
{
"type": 0,
"value": "未授权"
"value": "Unauthorized"
}
],
"message.user-deleted": [

View file

@ -6,13 +6,13 @@
"AD-06": "Sant Julia de Loria",
"AD-07": "Andorra la Vella",
"AD-08": "Escaldes-Engordany",
"AE-AJ": "Ajman",
"AE-AZ": "Abu Dhabi",
"AE-DU": "Dubai",
"AE-FU": "Al Fujairah",
"AE-RK": "Ras al Khaimah",
"AE-SH": "Sharjah",
"AE-UQ": "Umm al Quwain",
"AE-AJ": "'Ajman",
"AE-AZ": "Abu Zaby",
"AE-DU": "Dubayy",
"AE-FU": "Al Fujayrah",
"AE-RK": "Ra's al Khaymah",
"AE-SH": "Ash Shariqah",
"AE-UQ": "Umm al Qaywayn",
"AF-BAL": "Balkh",
"AF-BAM": "Bamyan",
"AF-BDG": "Badghis",

View file

@ -4,14 +4,13 @@ import {
Form,
FormField,
FormSubmitButton,
Grid,
Icon,
Label,
Loading,
Row,
TextField,
} from '@umami/react-zen';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
import { RefreshCw } from '@/components/icons';
@ -43,7 +42,7 @@ export function LinkEditForm({
const { linksUrl } = useConfig();
const hostUrl = linksUrl || LINKS_URL;
const { data, isLoading } = useLinkQuery(linkId);
const [defaultSlug] = useState(generateId());
const [slug, setSlug] = useState(generateId());
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
@ -56,6 +55,14 @@ export function LinkEditForm({
});
};
const handleSlug = () => {
const slug = generateId();
setSlug(slug);
return slug;
};
const checkUrl = (url: string) => {
if (!isValidUrl(url)) {
return formatMessage(labels.invalidUrl);
@ -63,19 +70,19 @@ export function LinkEditForm({
return true;
};
useEffect(() => {
if (data) {
setSlug(data.slug);
}
}, [data]);
if (linkId && isLoading) {
return <Loading placement="absolute" />;
}
return (
<Form
onSubmit={handleSubmit}
error={getErrorMessage(error)}
defaultValues={{ slug: defaultSlug, ...data }}
>
{({ setValue, watch }) => {
const slug = watch('slug');
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}>
{({ setValue }) => {
return (
<>
<FormField
@ -94,25 +101,15 @@ export function LinkEditForm({
<TextField placeholder="https://example.com" autoComplete="off" />
</FormField>
<Grid columns="1fr auto" alignItems="end" gap>
<FormField
name="slug"
label={formatMessage({ id: 'label.slug', defaultMessage: 'Slug' })}
rules={{
required: formatMessage(labels.required),
}}
>
<TextField autoComplete="off" />
</FormField>
<Button
variant="quiet"
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
>
<Icon>
<RefreshCw />
</Icon>
</Button>
</Grid>
<FormField
name="slug"
rules={{
required: formatMessage(labels.required),
}}
style={{ display: 'none' }}
>
<input type="hidden" />
</FormField>
<Column>
<Label>{formatMessage(labels.link)}</Label>
@ -124,6 +121,14 @@ export function LinkEditForm({
allowCopy
style={{ width: '100%' }}
/>
<Button
variant="quiet"
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
>
<Icon>
<RefreshCw />
</Icon>
</Button>
</Row>
</Column>

View file

@ -12,10 +12,9 @@ import { ListTable } from '@/components/metrics/ListTable';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { renderDateLabels } from '@/lib/charts';
import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
import { CHART_COLORS } from '@/lib/constants';
import { generateTimeSeries } from '@/lib/date';
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
import { getItem, setItem } from '@/lib/storage';
export interface RevenueProps {
websiteId: string;
@ -25,15 +24,7 @@ export interface RevenueProps {
}
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
const [currency, setCurrency] = useState(
getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY,
);
const handleCurrencyChange = (value: string) => {
setCurrency(value);
setItem(CURRENCY_CONFIG, value);
};
const [currency, setCurrency] = useState('USD');
const { formatMessage, labels } = useMessages();
const { locale, dateLocale } = useLocale();
const { countryNames } = useCountryNames(locale);
@ -116,7 +107,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
return (
<Column gap>
<Grid columns="280px" gap>
<CurrencySelect value={currency} onChange={handleCurrencyChange} />
<CurrencySelect value={currency} onChange={setCurrency} />
</Grid>
<LoadingPanel data={data} isLoading={isLoading} error={error}>
{data && (

View file

@ -16,7 +16,7 @@ export function useEventDataValuesQuery(
return useQuery<any>({
queryKey: [
'websites:event-data:values',
{ websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName },
{ websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
],
queryFn: () =>
get(`/websites/${websiteId}/event-data/values`, {

View file

@ -65,7 +65,7 @@ export function WebsiteSelect({
renderValue={renderValue}
listProps={{
renderEmptyState: () => <Empty message={formatMessage(messages.noResultsFound)} />,
style: { maxHeight: 'calc(42vh - 65px)' },
style: { maxHeight: '400px' },
}}
>
{({ id, name }: any) => <ListItem key={id}>{name}</ListItem>}

View file

@ -93,7 +93,7 @@
"label.event-name": "Име на събитие",
"label.events": "Събития",
"label.exists": "Съществува",
"label.exit": "URL за изход",
"label.exit": "Exit URL",
"label.false": "Грешно",
"label.field": "Поле",
"label.fields": "Полета",
@ -135,7 +135,7 @@
"label.last-days": "Последните {x} дни",
"label.last-hours": "Последните {x} часа",
"label.last-months": "Последните {x} месеца",
"label.last-seen": "Последно видяно",
"label.last-seen": "Last seen",
"label.leave": "Напусни",
"label.leave-team": "Напусни екип",
"label.less-than": "По-малко от",
@ -161,7 +161,7 @@
"label.none": "Няма",
"label.number-of-records": "{x} {x, plural, one {един} other {други}}",
"label.ok": "Добре",
"label.online": "Онлайн",
"label.online": "Online",
"label.organic-search": "Органично търсене",
"label.organic-shopping": "Органично пазаруване",
"label.organic-social": "Органични социални мрежи",
@ -185,9 +185,9 @@
"label.paths": "Пътища",
"label.pixels": "Пиксели",
"label.powered-by": "Поддържано от {name}",
"label.previous": "Предишен",
"label.previous-period": "Предишен период",
"label.previous-year": "Предишна година",
"label.previous": "Previous",
"label.previous-period": "Previous period",
"label.previous-year": "Previous year",
"label.profile": "Профил",
"label.properties": "Свойства",
"label.property": "Свойство",
@ -211,8 +211,8 @@
"label.reset-website": "Нулирай уебсайт",
"label.retention": "Привързване",
"label.retention-description": "Измерете привързаността към вашия уебсайт, като проследявате колко често потребителите се връщат.",
"label.revenue": "Приходи",
"label.revenue-description": "Прегледайте приходите си във времето.",
"label.revenue": "Revenue",
"label.revenue-description": "Look into your revenue across time.",
"label.role": "Роля",
"label.run-query": "Изпълни запитване",
"label.save": "Запази",
@ -260,14 +260,14 @@
"label.total": "Общо",
"label.total-records": "Общо записи",
"label.tracking-code": "Код за проследяване",
"label.transactions": "Транзакции",
"label.transactions": "Transactions",
"label.transfer": "Прехвърли",
"label.transfer-website": "Прехвърляне на уебсайт",
"label.true": "Вярно",
"label.type": "Вид",
"label.unique": "Уникален",
"label.unique-visitors": "Уникални посетители",
"label.uniqueCustomers": "Уникални клиенти",
"label.uniqueCustomers": "Unique Customers",
"label.unknown": "Неизвестен",
"label.untitled": "Без заглавие",
"label.update": "Актуализирай",
@ -282,7 +282,7 @@
"label.view-only": "Само за преглед",
"label.views": "Прегледи",
"label.views-per-visit": "Прегледи на посещение",
"label.visit-duration": "Продължителност на посещение",
"label.visit-duration": "Visit duration",
"label.visitors": "Посетители",
"label.visits": "Посещения",
"label.website": "Уебсайт",
@ -292,8 +292,8 @@
"label.yesterday": "Вчера",
"message.action-confirmation": "Въведете {confirmation} в полето по-долу, за да потвърдите.",
"message.active-users": "{x} {x, plural, one {активен един} other {активни други}}",
"message.bad-request": "Невалидна заявка",
"message.collected-data": "Събрани данни",
"message.bad-request": "Bad request",
"message.collected-data": "Collected data",
"message.confirm-delete": "Сигурни ли сте, че искате да изтриете {target}?",
"message.confirm-leave": "Сигурни ли сте, че искате да напуснете {target}?",
"message.confirm-remove": "Сигурни ли сте, че искате да премахнете {target}?",
@ -302,7 +302,7 @@
"message.delete-website-warning": "Всички данни за уебсайта ще бъдат изтрити.",
"message.error": "Възникна грешка.",
"message.event-log": "{event} на {url}",
"message.forbidden": "Забранено",
"message.forbidden": "Forbidden",
"message.go-to-settings": "Отидете в настройките",
"message.incorrect-username-password": "Неправилно потребителско име и/или парола.",
"message.invalid-domain": "Невалиден домейн. Не включвайте http/https.",
@ -316,13 +316,13 @@
"message.no-teams": "Няма създадени екипи.",
"message.no-users": "Няма потребители.",
"message.no-websites-configured": "Нямате конфигурирани уебсайтове.",
"message.not-found": "Не е намерено",
"message.nothing-selected": "Няма избрано.",
"message.not-found": "Not found",
"message.nothing-selected": "Nothing selected.",
"message.page-not-found": "Страницата не е намерена",
"message.reset-website": "За да нулирате този уебсайт, въведете {confirmation} в полето по-долу, за да потвърдите.",
"message.reset-website-warning": "Всички статистически данни за този уебсайт ще бъдат изтрити, но вашите настройки ще останат непроменени.",
"message.saved": "Запазено.",
"message.sever-error": "Сървърна грешка",
"message.sever-error": "Server error",
"message.share-url": "Статистиката за вашия уебсайт е публично достъпна на следния URL адрес:",
"message.team-already-member": "Вече сте член на екипа.",
"message.team-not-found": "Екипът не е намерен.",
@ -332,7 +332,7 @@
"message.transfer-user-website-to-team": "Изберете екипът на който да бъде прехвърлен уебсайта.",
"message.transfer-website": "Прехвърли собствеността на уебсайта към вашия акаунт или към друг екип.",
"message.triggered-event": "Активирано събитие",
"message.unauthorized": "Неоторизиран достъп",
"message.unauthorized": "Unauthorized",
"message.user-deleted": "Потребителят е изтрит.",
"message.viewed-page": "Страницата е видяна",
"message.visitor-log": "Посетител от {country}, използващ {browser} на {os} {device}"

View file

@ -1,15 +1,11 @@
{
"label.access-code": "访问代码",
"label.account": "账户",
"label.action": "行为",
"label.actions": "用户行为",
"label.activity": "活动日志",
"label.add": "添加",
"label.add-board": "添加看板",
"label.add-description": "添加描述",
"label.add-link": "添加链接",
"label.add-member": "添加成员",
"label.add-pixel": "添加像素",
"label.add-step": "添加步骤",
"label.add-website": "添加网站",
"label.admin": "管理员",
@ -17,13 +13,10 @@
"label.after": "之后",
"label.all": "所有",
"label.all-time": "所有时间段",
"label.analysis": "分析",
"label.analytics": "分析",
"label.application": "应用",
"label.apply": "应用",
"label.attribution": "归因",
"label.attribution-description": "查看用户如何与您的营销互动,以及是什么促成了转化。",
"label.audience": "受众",
"label.average": "平均",
"label.back": "返回",
"label.before": "之前",
@ -36,14 +29,11 @@
"label.campaigns": "活动",
"label.cancel": "取消",
"label.change-password": "修改密码",
"label.channel": "渠道",
"label.channels": "渠道",
"label.chart": "图表",
"label.cities": "市/县",
"label.city": "市/县",
"label.clear-all": "清除全部",
"label.cohort": "队列",
"label.cohorts": "队列",
"label.compare": "比较",
"label.compare-dates": "比较日期",
"label.confirm": "确认",
@ -63,7 +53,6 @@
"label.create-user": "创建用户",
"label.created": "已创建",
"label.created-by": "创建者",
"label.criteria": "条件",
"label.currency": "货币",
"label.current": "当前",
"label.current-password": "当前密码",
@ -81,28 +70,24 @@
"label.delete-website": "删除网站",
"label.description": "描述",
"label.desktop": "台式机",
"label.destination-url": "目标URL",
"label.details": "详细信息",
"label.device": "设备",
"label.devices": "设备",
"label.direct": "直接",
"label.dismiss": "关闭",
"label.distinct-id": "唯一ID",
"label.documentation": "文档",
"label.does-not-contain": "不包含",
"label.does-not-include": "不包括",
"label.doest-not-exist": "不存在",
"label.domain": "域名",
"label.download": "下载",
"label.dropoff": "丢弃",
"label.edit": "编辑",
"label.edit-dashboard": "编辑仪表盘",
"label.edit-member": "编辑成员",
"label.email": "邮箱",
"label.email": "Email",
"label.enable-share-url": "启用共享链接",
"label.end-step": "结束步骤",
"label.entry": "入口 URL",
"label.environment": "环境",
"label.event": "事件",
"label.event-data": "事件数据",
"label.event-name": "事件名称",
@ -127,13 +112,11 @@
"label.greater-than": "大于",
"label.greater-than-equals": "大于或等于",
"label.grouped": "分组",
"label.growth": "增长",
"label.hostname": "主机名",
"label.includes": "包括",
"label.insight": "洞察",
"label.insights": "见解",
"label.insights-description": "通过使用筛选器和划分时间段来更深入地研究数据。",
"label.invalid-url": "无效URL",
"label.is": "等于",
"label.is-false": "否",
"label.is-not": "不等于",
@ -157,9 +140,7 @@
"label.leave-team": "离开团队",
"label.less-than": "少于",
"label.less-than-equals": "少于等于",
"label.link": "链接",
"label.links": "链接",
"label.location": "位置",
"label.login": "登录",
"label.logout": "退出",
"label.manage": "管理",
@ -180,7 +161,7 @@
"label.none": "无",
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
"label.ok": "好的",
"label.online": "在线",
"label.online": "Online",
"label.organic-search": "自然搜索",
"label.organic-shopping": "自然购物",
"label.organic-social": "自然社交",
@ -202,22 +183,19 @@
"label.password": "密码",
"label.path": "路径",
"label.paths": "路径",
"label.pixel": "像素",
"label.pixels": "像素",
"label.powered-by": "由 {name} 提供支持",
"label.preferences": "偏好",
"label.previous": "先前",
"label.previous-period": "上一时期",
"label.previous-year": "上一年",
"label.profile": "个人资料",
"label.profiles": "个人资料",
"label.properties": "属性",
"label.property": "属性",
"label.queries": "查询",
"label.query": "查询",
"label.query-parameters": "查询参数",
"label.realtime": "实时",
"label.referral": "来源",
"label.referral": "Referral",
"label.referrer": "来源",
"label.referrers": "来源域名",
"label.refresh": "刷新",
@ -238,13 +216,8 @@
"label.role": "角色",
"label.run-query": "查询",
"label.save": "保存",
"label.save-cohort": "保存为群组",
"label.save-segment": "保存为细分",
"label.screen": "屏幕",
"label.screens": "屏幕尺寸",
"label.search": "搜索",
"label.segment": "细分",
"label.segments": "细分",
"label.select": "选择",
"label.select-date": "选择日期",
"label.select-filter": "选择筛选器",
@ -262,9 +235,6 @@
"label.start-step": "开始步骤",
"label.steps": "步骤",
"label.sum": "总和",
"label.support": "支持",
"label.switch-account": "切换账户",
"label.table": "表格",
"label.tablet": "平板",
"label.tag": "标签",
"label.tags": "标签",
@ -290,7 +260,6 @@
"label.total": "总数",
"label.total-records": "总记录数",
"label.tracking-code": "跟踪代码",
"label.traffic": "流量",
"label.transactions": "交易",
"label.transfer": "转移",
"label.transfer-website": "转移网站",
@ -323,7 +292,7 @@
"label.yesterday": "昨天",
"message.action-confirmation": "请在下方输入框中输入 {confirmation} 以确认操作。",
"message.active-users": "当前在线 {x} 位访客",
"message.bad-request": "请求错误",
"message.bad-request": "Bad request",
"message.collected-data": "已收集的数据",
"message.confirm-delete": "你确定要删除 {target} 吗?",
"message.confirm-leave": "你确定要离开 {target} 吗?",
@ -333,7 +302,7 @@
"message.delete-website-warning": "所有相关数据将会被删除。",
"message.error": "发生错误。",
"message.event-log": "{url} 上的 {event}",
"message.forbidden": "禁止访问",
"message.forbidden": "Forbidden",
"message.go-to-settings": "去设置",
"message.incorrect-username-password": "用户名或密码不正确。",
"message.invalid-domain": "无效域名",
@ -347,13 +316,13 @@
"message.no-teams": "您尚未创建任何团队。",
"message.no-users": "暂无用户。",
"message.no-websites-configured": "你还没有设置任何网站。",
"message.not-found": "未找到",
"message.nothing-selected": "未选择",
"message.not-found": "Not found",
"message.nothing-selected": "Nothing selected.",
"message.page-not-found": "页面未找到。",
"message.reset-website": "如确定要重置该网站,请在下面输入 {confirmation} 以确认。",
"message.reset-website-warning": "此网站的所有统计数据将被删除,但您的跟踪代码将保持不变。",
"message.saved": "保存成功。",
"message.sever-error": "服务器错误",
"message.sever-error": "Server error",
"message.share-url": "这是 {target} 的共享链接。",
"message.team-already-member": "你已是该团队的成员。",
"message.team-not-found": "未找到团队。",
@ -363,7 +332,7 @@
"message.transfer-user-website-to-team": "选择要转移此网站的团队。",
"message.transfer-website": "将网站所有权转移到您的账户或其他团队。",
"message.triggered-event": "触发事件",
"message.unauthorized": "未授权",
"message.unauthorized": "Unauthorized",
"message.user-deleted": "用户已删除。",
"message.viewed-page": "已浏览页面",
"message.visitor-log": "来自 {country} 的访客在搭载 {os} 的 {device} 上使用 {browser} 浏览器进行访问。"

View file

@ -4,7 +4,6 @@ export const LOCALE_CONFIG = 'umami.locale';
export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme';
export const CURRENCY_CONFIG = 'umami.currency';
export const DASHBOARD_CONFIG = 'umami.dashboard';
export const LAST_TEAM_CONFIG = 'umami.last-team';
export const VERSION_CHECK = 'umami.version-check';
@ -26,7 +25,6 @@ export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 20;
export const DEFAULT_DATE_COMPARE = 'prev';
export const DEFAULT_CURRENCY = 'USD';
export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 10000;

View file

@ -28,12 +28,6 @@ const PROVIDER_HEADERS = [
regionHeader: 'cloudfront-viewer-country-region',
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 = '') {

View file

@ -1,5 +1,3 @@
import { DEFAULT_CURRENCY } from './constants';
export function parseTime(val: number) {
const days = ~~(val / 86400);
const hours = ~~(val / 3600) - days * 24;
@ -96,7 +94,7 @@ export function formatCurrency(value: number, currency: string, locale = 'en-US'
// Fallback to default currency format if an error occurs
formattedValue = new Intl.NumberFormat(locale, {
style: 'currency',
currency: DEFAULT_CURRENCY,
currency: 'USD',
});
}

View file

@ -1,5 +1,3 @@
import ipaddr from 'ipaddr.js';
export const IP_ADDRESS_HEADERS = [
'true-client-ip', // CDN
'cf-connecting-ip', // Cloudflare
@ -15,87 +13,35 @@ export const IP_ADDRESS_HEADERS = [
'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) {
const customHeader = process.env.CLIENT_IP_HEADER;
if (customHeader && headers.get(customHeader)) {
return resolveIp(headers.get(customHeader));
return headers.get(customHeader);
}
const header = IP_ADDRESS_HEADERS.find(name => headers.get(name));
if (!header) {
return undefined;
}
const header = IP_ADDRESS_HEADERS.find(name => {
return headers.get(name);
});
const ip = headers.get(header);
if (header === 'x-forwarded-for') {
return resolveIp(ip?.split(',')?.[0]?.trim());
return ip?.split(',')?.[0]?.trim();
}
if (header === 'forwarded') {
const match = ip.match(/for=(\[?[0-9a-fA-F:.]+\]?)/);
if (match) {
return resolveIp(match[1]);
return match[1];
}
}
return resolveIp(ip);
return ip;
}
export function stripPort(ip?: string | null) {
if (!ip) {
return ip;
}
export function stripPort(ip: string) {
if (ip.startsWith('[')) {
const endBracket = ip.indexOf(']');
if (endBracket !== -1) {

View file

@ -46,23 +46,31 @@ export async function relationalQuery({
createdAt,
}));
const existing = await client.sessionData.findMany({
where: {
sessionId,
},
select: {
id: true,
sessionId: true,
dataKey: true,
},
});
for (const data of flattenedData) {
const { sessionId, dataKey, ...props } = data;
const record = existing.find(e => e.sessionId === sessionId && e.dataKey === dataKey);
// Try to update existing record using compound where clause
// This is safer than using id from a previous query due to race conditions
const updateResult = await client.sessionData.updateMany({
where: {
sessionId,
dataKey,
},
data: {
...props,
},
});
// If no record was updated, create a new one
if (updateResult.count === 0) {
if (record) {
await client.sessionData.update({
where: {
id: record.id,
},
data: {
...props,
},
});
} else {
await client.sessionData.create({
data,
});

View file

@ -12,13 +12,7 @@
if (!currentScript) return;
const { hostname, href, origin } = location;
let localStorage;
try {
localStorage = href.startsWith('data:') ? undefined : window.localStorage;
} catch {
/* (DOMException) SecurityError: Access is denied for this document. */
}
const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
const _data = 'data-';
const _false = 'false';