mirror of
https://github.com/umami-software/umami.git
synced 2025-12-08 05:12:36 +01:00
Merge branch 'dev' into add-cors-header-for-tracker-script
This commit is contained in:
commit
fec0701a48
101 changed files with 14567 additions and 1009 deletions
19
.github/workflows/cd-manual.yml
vendored
19
.github/workflows/cd-manual.yml
vendored
|
|
@ -20,11 +20,26 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Extract version parts from input
|
||||
id: extract_version
|
||||
run: |
|
||||
echo "version=$(echo ${{ github.event.inputs.version }})" >> $GITHUB_ENV
|
||||
echo "major=$(echo ${{ github.event.inputs.version }} | cut -d. -f1)" >> $GITHUB_ENV
|
||||
echo "minor=$(echo ${{ github.event.inputs.version }} | cut -d. -f2)" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate tags
|
||||
id: generate_tags
|
||||
run: |
|
||||
echo "tag_major=$(echo ${{ matrix.db-type }}-${{ env.major }})" >> $GITHUB_ENV
|
||||
echo "tag_minor=$(echo ${{ matrix.db-type }}-${{ env.major }}.${{ env.minor }})" >> $GITHUB_ENV
|
||||
echo "tag_patch=$(echo ${{ matrix.db-type }}-${{ env.version }})" >> $GITHUB_ENV
|
||||
echo "tag_latest=$(echo ${{ matrix.db-type }}-latest)" >> $GITHUB_ENV
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umami
|
||||
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
|
||||
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
|
|
@ -36,7 +51,7 @@ jobs:
|
|||
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umamisoftware/umami
|
||||
tags: ${{ matrix.db-type }}-${{ inputs.version }}, ${{ matrix.db-type }}-latest
|
||||
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
|
|
|||
14
.github/workflows/cd.yml
vendored
14
.github/workflows/cd.yml
vendored
|
|
@ -17,14 +17,21 @@ jobs:
|
|||
|
||||
- name: Set env
|
||||
run: |
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
echo "NOW=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate tags
|
||||
id: generate_tags
|
||||
run: |
|
||||
echo "tag_patch=$(echo ${{ matrix.db-type }})-${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
echo "tag_minor=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1,2)" >> $GITHUB_ENV
|
||||
echo "tag_major=$(echo ${{ matrix.db-type }})-$(echo ${GITHUB_REF#refs/tags/} | cut -d. -f1)" >> $GITHUB_ENV
|
||||
echo "tag_latest=$(echo ${{ matrix.db-type }})-latest" >> $GITHUB_ENV
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to ghcr.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umami
|
||||
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
||||
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: ghcr.io
|
||||
multiPlatform: true
|
||||
|
|
@ -32,12 +39,11 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- uses: mr-smithers-excellent/docker-build-push@v6
|
||||
name: Build & push Docker image to docker.io for ${{ matrix.db-type }}
|
||||
with:
|
||||
image: umamisoftware/umami
|
||||
tags: ${{ matrix.db-type }}-${{ env.RELEASE_VERSION }}, ${{ matrix.db-type }}-latest
|
||||
tags: ${{ env.tag_major }}, ${{ env.tag_minor }}, ${{ env.tag_patch }}, ${{ env.tag_latest }}
|
||||
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<img src="https://umami.is/images/umami-logo.png" alt="Umami Logo" width="100">
|
||||
<img src="https://content.umami.is/website/images/umami-logo.png" alt="Umami Logo" width="100">
|
||||
</p>
|
||||
|
||||
<h1 align="center">Umami</h1>
|
||||
|
|
@ -36,7 +36,7 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.i
|
|||
### Requirements
|
||||
|
||||
- A server with Node.js version 16.13 or newer
|
||||
- A database. Umami supports [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
|
||||
- A database. Umami supports [MariaDB](https://www.mariadb.org/) (minimum v10.5), [MySQL](https://www.mysql.com/) (minimum v8.0) and [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
|
||||
|
||||
### Install Yarn
|
||||
|
||||
|
|
|
|||
77
db/clickhouse/migrations/04_add_tag.sql
Normal file
77
db/clickhouse/migrations/04_add_tag.sql
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
-- add tag column
|
||||
ALTER TABLE umami.website_event ADD COLUMN "tag" String AFTER "event_name";
|
||||
ALTER TABLE umami.website_event_stats_hourly ADD COLUMN "tag" SimpleAggregateFunction(groupArrayArray, Array(String)) AFTER "max_time";
|
||||
|
||||
-- update materialized view
|
||||
DROP TABLE umami.website_event_stats_hourly_mv;
|
||||
|
||||
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
|
||||
TO umami.website_event_stats_hourly
|
||||
AS
|
||||
SELECT
|
||||
website_id,
|
||||
session_id,
|
||||
visit_id,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
city,
|
||||
entry_url,
|
||||
exit_url,
|
||||
url_paths as url_path,
|
||||
url_query,
|
||||
referrer_domain,
|
||||
page_title,
|
||||
event_type,
|
||||
event_name,
|
||||
views,
|
||||
min_time,
|
||||
max_time,
|
||||
tag,
|
||||
timestamp as created_at
|
||||
FROM (SELECT
|
||||
website_id,
|
||||
session_id,
|
||||
visit_id,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
city,
|
||||
argMinState(url_path, created_at) entry_url,
|
||||
argMaxState(url_path, created_at) exit_url,
|
||||
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
||||
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
||||
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
|
||||
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
||||
event_type,
|
||||
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||
sumIf(1, event_type = 1) views,
|
||||
min(created_at) min_time,
|
||||
max(created_at) max_time,
|
||||
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||
toStartOfHour(created_at) timestamp
|
||||
FROM umami.website_event
|
||||
GROUP BY website_id,
|
||||
session_id,
|
||||
visit_id,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
city,
|
||||
event_type,
|
||||
timestamp);
|
||||
|
|
@ -26,6 +26,7 @@ CREATE TABLE umami.website_event
|
|||
--events
|
||||
event_type UInt32,
|
||||
event_name String,
|
||||
tag String,
|
||||
created_at DateTime('UTC'),
|
||||
job_id Nullable(UUID)
|
||||
)
|
||||
|
|
@ -96,6 +97,7 @@ CREATE TABLE umami.website_event_stats_hourly
|
|||
views SimpleAggregateFunction(sum, UInt64),
|
||||
min_time SimpleAggregateFunction(min, DateTime('UTC')),
|
||||
max_time SimpleAggregateFunction(max, DateTime('UTC')),
|
||||
tag SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||
created_at Datetime('UTC')
|
||||
)
|
||||
ENGINE = AggregatingMergeTree
|
||||
|
|
@ -136,6 +138,7 @@ SELECT
|
|||
views,
|
||||
min_time,
|
||||
max_time,
|
||||
tag,
|
||||
timestamp as created_at
|
||||
FROM (SELECT
|
||||
website_id,
|
||||
|
|
@ -161,6 +164,7 @@ FROM (SELECT
|
|||
sumIf(1, event_type = 1) views,
|
||||
min(created_at) min_time,
|
||||
max(created_at) max_time,
|
||||
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||
toStartOfHour(created_at) timestamp
|
||||
FROM umami.website_event
|
||||
GROUP BY website_id,
|
||||
|
|
|
|||
5
db/mysql/migrations/07_add_tag/migration.sql
Normal file
5
db/mysql/migrations/07_add_tag/migration.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE `website_event` ADD COLUMN `tag` VARCHAR(50) NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX `website_event_website_id_created_at_tag_idx` ON `website_event`(`website_id`, `created_at`, `tag`);
|
||||
|
|
@ -102,6 +102,7 @@ model WebsiteEvent {
|
|||
pageTitle String? @map("page_title") @db.VarChar(500)
|
||||
eventType Int @default(1) @map("event_type") @db.UnsignedInt
|
||||
eventName String? @map("event_name") @db.VarChar(50)
|
||||
tag String? @db.VarChar(50)
|
||||
|
||||
eventData EventData[]
|
||||
session Session @relation(fields: [sessionId], references: [id])
|
||||
|
|
@ -116,6 +117,7 @@ model WebsiteEvent {
|
|||
@@index([websiteId, createdAt, referrerDomain])
|
||||
@@index([websiteId, createdAt, pageTitle])
|
||||
@@index([websiteId, createdAt, eventName])
|
||||
@@index([websiteId, createdAt, tag])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@index([websiteId, visitId, createdAt])
|
||||
@@map("website_event")
|
||||
|
|
|
|||
5
db/postgresql/migrations/07_add_tag/migration.sql
Normal file
5
db/postgresql/migrations/07_add_tag/migration.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "website_event" ADD COLUMN "tag" VARCHAR(50);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_event_website_id_created_at_tag_idx" ON "website_event"("website_id", "created_at", "tag");
|
||||
|
|
@ -102,6 +102,7 @@ model WebsiteEvent {
|
|||
pageTitle String? @map("page_title") @db.VarChar(500)
|
||||
eventType Int @default(1) @map("event_type") @db.Integer
|
||||
eventName String? @map("event_name") @db.VarChar(50)
|
||||
tag String? @db.VarChar(50)
|
||||
|
||||
eventData EventData[]
|
||||
session Session @relation(fields: [sessionId], references: [id])
|
||||
|
|
@ -111,11 +112,13 @@ model WebsiteEvent {
|
|||
@@index([visitId])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, createdAt])
|
||||
|
||||
@@index([websiteId, createdAt, urlPath])
|
||||
@@index([websiteId, createdAt, urlQuery])
|
||||
@@index([websiteId, createdAt, referrerDomain])
|
||||
@@index([websiteId, createdAt, pageTitle])
|
||||
@@index([websiteId, createdAt, eventName])
|
||||
@@index([websiteId, createdAt, tag])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@index([websiteId, visitId, createdAt])
|
||||
@@map("website_event")
|
||||
|
|
|
|||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
|
|
@ -3,4 +3,4 @@
|
|||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -184,6 +184,10 @@ const config = {
|
|||
async rewrites() {
|
||||
return [
|
||||
...rewrites,
|
||||
{
|
||||
source: '/script.js',
|
||||
destination: 'https://tracker-script.umami.dev/',
|
||||
},
|
||||
{
|
||||
source: '/telemetry.js',
|
||||
destination: '/api/scripts/telemetry',
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
"maxmind": "^4.3.6",
|
||||
"md5": "^2.3.0",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "14.2.5",
|
||||
"next": "14.2.10",
|
||||
"next-basics": "^0.39.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1012,7 +1012,7 @@
|
|||
"label.retention-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하십시오."
|
||||
"value": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하세요."
|
||||
}
|
||||
],
|
||||
"label.revenue": [
|
||||
|
|
@ -1372,7 +1372,7 @@
|
|||
"label.utm-description": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "UTM 매개변수를 통해 캠페인을 추적합니다."
|
||||
"value": "UTM 매개변수를 통해 캠페인을 추적하세요."
|
||||
}
|
||||
],
|
||||
"label.value": [
|
||||
|
|
@ -1414,7 +1414,7 @@
|
|||
"label.visit-duration": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "평균 방문 시간"
|
||||
"value": "방문 시간"
|
||||
}
|
||||
],
|
||||
"label.visitors": [
|
||||
|
|
@ -1548,7 +1548,7 @@
|
|||
"message.error": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "오류가 발생했습니다."
|
||||
"value": "문제가 발생했습니다."
|
||||
}
|
||||
],
|
||||
"message.event-log": [
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { terser } from 'rollup-plugin-terser';
|
|||
export default {
|
||||
input: 'src/tracker/index.js',
|
||||
output: {
|
||||
file: 'public/script.js',
|
||||
file: 'public/tracker.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
|
|
|
|||
|
|
@ -48,6 +48,46 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
});
|
||||
}
|
||||
|
||||
function handleRunRevenue() {
|
||||
window['umami'].track(props => ({
|
||||
...props,
|
||||
url: '/checkout-cart',
|
||||
referrer: 'https://www.google.com',
|
||||
}));
|
||||
window['umami'].track('checkout-cart', {
|
||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||
currency: 'USD',
|
||||
});
|
||||
window['umami'].track('affiliate-link', {
|
||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||
currency: 'USD',
|
||||
});
|
||||
window['umami'].track('promotion-link', {
|
||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||
currency: 'USD',
|
||||
});
|
||||
window['umami'].track('checkout-cart', {
|
||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||
currency: 'EUR',
|
||||
});
|
||||
window['umami'].track('promotion-link', {
|
||||
revenue: parseFloat((Math.random() * 1000).toFixed(2)),
|
||||
currency: 'EUR',
|
||||
});
|
||||
window['umami'].track('affiliate-link', {
|
||||
item1: {
|
||||
productIdentity: 'ABC424',
|
||||
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
|
||||
currency: 'JPY',
|
||||
},
|
||||
item2: {
|
||||
productIdentity: 'ZYW684',
|
||||
revenue: parseFloat((Math.random() * 10000).toFixed(2)),
|
||||
currency: 'JPY',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleRunIdentify() {
|
||||
window['umami'].identify({
|
||||
userId: 123,
|
||||
|
|
@ -127,10 +167,19 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
>
|
||||
Send event with data
|
||||
</Button>
|
||||
<Button
|
||||
id="generate-revenue-button"
|
||||
data-umami-event="checkout-cart"
|
||||
data-umami-event-revenue={(Math.random() * 10000).toFixed(2).toString()}
|
||||
data-umami-event-currency="USD"
|
||||
variant="primary"
|
||||
>
|
||||
Generate revenue data
|
||||
</Button>
|
||||
<Button
|
||||
id="button-with-div-button"
|
||||
data-umami-event="button-click"
|
||||
data-umami-event-name="bob"
|
||||
data-umami-event-name={'bob'}
|
||||
data-umami-event-id="123"
|
||||
variant="primary"
|
||||
>
|
||||
|
|
@ -155,6 +204,9 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
|
|||
<Button id="manual-button" variant="primary" onClick={handleRunIdentify}>
|
||||
Run identify
|
||||
</Button>
|
||||
<Button id="manual-button" variant="primary" onClick={handleRunRevenue}>
|
||||
Revenue script
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<WebsiteChart websiteId={website.id} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Funnel from 'assets/funnel.svg';
|
||||
import Money from 'assets/money.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import Magnet from 'assets/magnet.svg';
|
||||
import Path from 'assets/path.svg';
|
||||
|
|
@ -51,12 +52,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
|||
url: renderTeamUrl('/reports/journey'),
|
||||
icon: <Path />,
|
||||
},
|
||||
// {
|
||||
// title: formatMessage(labels.revenue),
|
||||
// description: formatMessage(labels.revenueDescription),
|
||||
// url: renderTeamUrl('/reports/revenue'),
|
||||
// icon: <Money />,
|
||||
// },
|
||||
{
|
||||
title: formatMessage(labels.revenue),
|
||||
description: formatMessage(labels.revenueDescription),
|
||||
url: renderTeamUrl('/reports/revenue'),
|
||||
icon: <Money />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
import BarChart, { BarChartProps } from 'components/charts/BarChart';
|
||||
import { useLocale, useMessages } from 'components/hooks';
|
||||
import MetricCard from 'components/metrics/MetricCard';
|
||||
import MetricsBar from 'components/metrics/MetricsBar';
|
||||
import { renderDateLabels } from 'lib/charts';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export interface PageviewsChartProps extends BarChartProps {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function RevenueChart({ isLoading, ...props }: PageviewsChartProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { report } = useContext(ReportContext);
|
||||
const { data, parameters } = report || {};
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
label: formatMessage(labels.average),
|
||||
data: data?.chart.map(a => ({ x: a.time, y: a.avg })),
|
||||
borderWidth: 2,
|
||||
backgroundColor: '#8601B0',
|
||||
borderColor: '#8601B0',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.total),
|
||||
data: data?.chart.map(a => ({ x: a.time, y: a.sum })),
|
||||
borderWidth: 2,
|
||||
backgroundColor: '#f15bb5',
|
||||
borderColor: '#f15bb5',
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [data, locale]);
|
||||
|
||||
const metricData = useMemo(() => {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { sum, avg, count, uniqueCount } = data.total;
|
||||
|
||||
return [
|
||||
{
|
||||
value: sum,
|
||||
label: formatMessage(labels.total),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: avg,
|
||||
label: formatMessage(labels.average),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: count,
|
||||
label: formatMessage(labels.transactions),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: uniqueCount,
|
||||
label: formatMessage(labels.uniqueCustomers),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
] as any;
|
||||
}, [data, locale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricsBar isFetched={data}>
|
||||
{metricData?.map(({ label, value, formatValue }) => {
|
||||
return <MetricCard key={label} value={value} label={label} formatValue={formatValue} />;
|
||||
})}
|
||||
</MetricsBar>
|
||||
{data && (
|
||||
<BarChart
|
||||
{...props}
|
||||
data={chartData}
|
||||
unit={parameters?.dateRange.unit}
|
||||
isLoading={isLoading}
|
||||
renderXLabel={renderDateLabels(parameters?.dateRange.unit, locale)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RevenueChart;
|
||||
|
|
@ -1,46 +1,41 @@
|
|||
import { useMessages } from 'components/hooks';
|
||||
import useRevenueValues from 'components/hooks/queries/useRevenueValues';
|
||||
import { useContext } from 'react';
|
||||
import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics';
|
||||
import { Dropdown, Form, FormButtons, FormInput, FormRow, Item, SubmitButton } from 'react-basics';
|
||||
import BaseParameters from '../[reportId]/BaseParameters';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export function RevenueParameters() {
|
||||
const { report, runReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange;
|
||||
const queryEnabled = websiteId && dateRange;
|
||||
const { data: values = [] } = useRevenueValues(
|
||||
websiteId,
|
||||
dateRange?.startDate,
|
||||
dateRange?.endDate,
|
||||
);
|
||||
|
||||
const handleSubmit = (data: any, e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!queryDisabled) {
|
||||
runReport(data);
|
||||
}
|
||||
runReport(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||
<FormRow label={formatMessage(labels.event)}>
|
||||
<FormInput name="eventName" rules={{ required: formatMessage(labels.required) }}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.revenueProperty)}>
|
||||
<FormInput name="revenueProperty" rules={{ required: formatMessage(labels.required) }}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormRow label={formatMessage(labels.userProperty)}>
|
||||
<FormInput name="userProperty">
|
||||
<TextField autoComplete="off" />
|
||||
<FormRow label={formatMessage(labels.currency)}>
|
||||
<FormInput name="currency" rules={{ required: formatMessage(labels.required) }}>
|
||||
<Dropdown items={values.map(item => item.currency)}>
|
||||
{item => <Item key={item}>{item}</Item>}
|
||||
</Dropdown>
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
|
||||
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: var(--border-radius);
|
||||
line-height: 32px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import RevenueChart from './RevenueChart';
|
||||
import RevenueParameters from './RevenueParameters';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import Money from 'assets/money.svg';
|
||||
import { REPORT_TYPES } from 'lib/constants';
|
||||
import Report from '../[reportId]/Report';
|
||||
import ReportBody from '../[reportId]/ReportBody';
|
||||
import ReportHeader from '../[reportId]/ReportHeader';
|
||||
import ReportMenu from '../[reportId]/ReportMenu';
|
||||
import RevenueParameters from './RevenueParameters';
|
||||
import RevenueView from './RevenueView';
|
||||
|
||||
const defaultParameters = {
|
||||
type: REPORT_TYPES.revenue,
|
||||
parameters: { Revenue: [] },
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export default function RevenueReport({ reportId }: { reportId?: string }) {
|
||||
|
|
@ -20,7 +20,7 @@ export default function RevenueReport({ reportId }: { reportId?: string }) {
|
|||
<RevenueParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<RevenueChart unit="day" />
|
||||
<RevenueView />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
|
|
|
|||
38
src/app/(main)/reports/revenue/RevenueTable.tsx
Normal file
38
src/app/(main)/reports/revenue/RevenueTable.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { useContext } from 'react';
|
||||
import { GridColumn, GridTable } from 'react-basics';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import { formatLongCurrency } from 'lib/format';
|
||||
|
||||
export function RevenueTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data } = report || {};
|
||||
|
||||
if (!data) {
|
||||
return <EmptyPlaceholder />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GridTable data={data.table || []}>
|
||||
<GridColumn name="currency" label={formatMessage(labels.currency)} alignment="end">
|
||||
{row => row.currency}
|
||||
</GridColumn>
|
||||
<GridColumn name="currency" label={formatMessage(labels.total)} width="300px" alignment="end">
|
||||
{row => formatLongCurrency(row.sum, row.currency)}
|
||||
</GridColumn>
|
||||
<GridColumn name="currency" label={formatMessage(labels.average)} alignment="end">
|
||||
{row => formatLongCurrency(row.count ? row.sum / row.count : 0, row.currency)}
|
||||
</GridColumn>
|
||||
<GridColumn name="currency" label={formatMessage(labels.transactions)} alignment="end">
|
||||
{row => row.count}
|
||||
</GridColumn>
|
||||
<GridColumn name="currency" label={formatMessage(labels.uniqueCustomers)} alignment="end">
|
||||
{row => row.unique_count}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default RevenueTable;
|
||||
11
src/app/(main)/reports/revenue/RevenueView.module.css
Normal file
11
src/app/(main)/reports/revenue/RevenueView.module.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.container {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
156
src/app/(main)/reports/revenue/RevenueView.tsx
Normal file
156
src/app/(main)/reports/revenue/RevenueView.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import classNames from 'classnames';
|
||||
import { colord } from 'colord';
|
||||
import BarChart from 'components/charts/BarChart';
|
||||
import PieChart from 'components/charts/PieChart';
|
||||
import TypeIcon from 'components/common/TypeIcon';
|
||||
import { useCountryNames, useLocale, useMessages } from 'components/hooks';
|
||||
import { GridRow } from 'components/layout/Grid';
|
||||
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 } from 'lib/constants';
|
||||
import { formatLongCurrency, formatLongNumber } from 'lib/format';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import RevenueTable from './RevenueTable';
|
||||
import styles from './RevenueView.module.css';
|
||||
|
||||
export interface RevenueViewProps {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function RevenueView({ isLoading }: RevenueViewProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { locale } = useLocale();
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
const { report } = useContext(ReportContext);
|
||||
const {
|
||||
data,
|
||||
parameters: { dateRange, currency },
|
||||
} = report || {};
|
||||
const showTable = data?.table.length > 1;
|
||||
|
||||
const renderCountryName = useCallback(
|
||||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||
{countryNames[code]}
|
||||
</span>
|
||||
),
|
||||
[countryNames, locale],
|
||||
);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
const map = (data.chart as any[]).reduce((obj, { x, t, y }) => {
|
||||
if (!obj[x]) {
|
||||
obj[x] = [];
|
||||
}
|
||||
|
||||
obj[x].push({ x: t, y });
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
datasets: Object.keys(map).map((key, index) => {
|
||||
const color = colord(CHART_COLORS[index % CHART_COLORS.length]);
|
||||
return {
|
||||
label: key,
|
||||
data: map[key],
|
||||
lineTension: 0,
|
||||
backgroundColor: color.alpha(0.6).toRgbString(),
|
||||
borderColor: color.alpha(0.7).toRgbString(),
|
||||
borderWidth: 1,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
const countryData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
const labels = data.country.map(({ name }) => name);
|
||||
const datasets = [
|
||||
{
|
||||
data: data.country.map(({ value }) => value),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
];
|
||||
|
||||
return { labels, datasets };
|
||||
}, [data]);
|
||||
|
||||
const metricData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
const { sum, count, unique_count } = data.total;
|
||||
|
||||
return [
|
||||
{
|
||||
value: sum,
|
||||
label: formatMessage(labels.total),
|
||||
formatValue: n => formatLongCurrency(n, currency),
|
||||
},
|
||||
{
|
||||
value: count ? sum / count : 0,
|
||||
label: formatMessage(labels.average),
|
||||
formatValue: n => formatLongCurrency(n, currency),
|
||||
},
|
||||
{
|
||||
value: count,
|
||||
label: formatMessage(labels.transactions),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: unique_count,
|
||||
label: formatMessage(labels.uniqueCustomers),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
] as any;
|
||||
}, [data, locale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<MetricsBar isFetched={data}>
|
||||
{metricData?.map(({ label, value, formatValue }) => {
|
||||
return <MetricCard key={label} value={value} label={label} formatValue={formatValue} />;
|
||||
})}
|
||||
</MetricsBar>
|
||||
{data && (
|
||||
<>
|
||||
<BarChart
|
||||
minDate={dateRange?.startDate}
|
||||
maxDate={dateRange?.endDate}
|
||||
data={chartData}
|
||||
unit={dateRange?.unit}
|
||||
stacked={true}
|
||||
currency={currency}
|
||||
renderXLabel={renderDateLabels(dateRange?.unit, locale)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<GridRow columns="two">
|
||||
<ListTable
|
||||
metric={formatMessage(labels.country)}
|
||||
data={data?.country.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
z: (value / data?.total.sum) * 100,
|
||||
}))}
|
||||
renderLabel={renderCountryName}
|
||||
/>
|
||||
<PieChart type="doughnut" data={countryData} />
|
||||
</GridRow>
|
||||
</>
|
||||
)}
|
||||
{showTable && <RevenueTable />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RevenueView;
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
import { Icons, Icon, Text, Dropdown, Item } from 'react-basics';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import { useLocale, useMessages, useNavigation } from 'components/hooks';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import CitiesTable from 'components/metrics/CitiesTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import HostsTable from 'components/metrics/HostsTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import HostsTable from 'components/metrics/HostsTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import ScreenTable from 'components/metrics/ScreenTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import { useNavigation, useMessages, useLocale } from 'components/hooks';
|
||||
import LinkButton from 'components/common/LinkButton';
|
||||
import TagsTable from 'components/metrics/TagsTable';
|
||||
import { Dropdown, Icon, Icons, Item, Text } from 'react-basics';
|
||||
import styles from './WebsiteExpandedView.module.css';
|
||||
|
||||
const views = {
|
||||
|
|
@ -34,6 +35,7 @@ const views = {
|
|||
language: LanguagesTable,
|
||||
event: EventsTable,
|
||||
query: QueryParametersTable,
|
||||
tag: TagsTable,
|
||||
};
|
||||
|
||||
export default function WebsiteExpandedView({
|
||||
|
|
@ -117,6 +119,11 @@ export default function WebsiteExpandedView({
|
|||
label: formatMessage(labels.hosts),
|
||||
url: renderUrl({ view: 'host' }),
|
||||
},
|
||||
{
|
||||
key: 'tag',
|
||||
label: formatMessage(labels.tags),
|
||||
url: renderUrl({ view: 'tag' }),
|
||||
},
|
||||
];
|
||||
|
||||
const DetailsComponent = views[view] || (() => null);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
import { useState } from 'react';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import { useDateRange, useMessages, useNavigation } from 'components/hooks';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import ScreenTable from 'components/metrics/ScreenTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import CitiesTable from 'components/metrics/CitiesTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import { Grid, GridRow } from 'components/layout/Grid';
|
||||
import SideNav from 'components/layout/SideNav';
|
||||
import BrowsersTable from 'components/metrics/BrowsersTable';
|
||||
import ChangeLabel from 'components/metrics/ChangeLabel';
|
||||
import CitiesTable from 'components/metrics/CitiesTable';
|
||||
import CountriesTable from 'components/metrics/CountriesTable';
|
||||
import DevicesTable from 'components/metrics/DevicesTable';
|
||||
import EventsTable from 'components/metrics/EventsTable';
|
||||
import LanguagesTable from 'components/metrics/LanguagesTable';
|
||||
import MetricsTable from 'components/metrics/MetricsTable';
|
||||
import useStore from 'store/websites';
|
||||
import OSTable from 'components/metrics/OSTable';
|
||||
import PagesTable from 'components/metrics/PagesTable';
|
||||
import QueryParametersTable from 'components/metrics/QueryParametersTable';
|
||||
import ReferrersTable from 'components/metrics/ReferrersTable';
|
||||
import RegionsTable from 'components/metrics/RegionsTable';
|
||||
import ScreenTable from 'components/metrics/ScreenTable';
|
||||
import TagsTable from 'components/metrics/TagsTable';
|
||||
import { getCompareDate } from 'lib/date';
|
||||
import { formatNumber } from 'lib/format';
|
||||
import ChangeLabel from 'components/metrics/ChangeLabel';
|
||||
import { useState } from 'react';
|
||||
import useStore from 'store/websites';
|
||||
import styles from './WebsiteCompareTables.module.css';
|
||||
|
||||
const views = {
|
||||
|
|
@ -35,6 +36,7 @@ const views = {
|
|||
language: LanguagesTable,
|
||||
event: EventsTable,
|
||||
query: QueryParametersTable,
|
||||
tag: TagsTable,
|
||||
};
|
||||
|
||||
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
||||
|
|
@ -109,6 +111,16 @@ export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
|
|||
label: formatMessage(labels.queryParameters),
|
||||
url: renderUrl({ view: 'query' }),
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
label: formatMessage(labels.hosts),
|
||||
url: renderUrl({ view: 'host' }),
|
||||
},
|
||||
{
|
||||
key: 'tag',
|
||||
label: formatMessage(labels.tags),
|
||||
url: renderUrl({ view: 'tag' }),
|
||||
},
|
||||
];
|
||||
|
||||
const renderChange = ({ x, y }) => {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
|
|||
{propertyName && (
|
||||
<div className={styles.chart}>
|
||||
<div className={styles.title}>{propertyName}</div>
|
||||
<PieChart key={propertyName} type="doughnut" data={chartData} />
|
||||
<PieChart key={propertyName + eventName} type="doughnut" data={chartData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function RealtimeCountries({ data }) {
|
|||
|
||||
const renderCountryName = useCallback(
|
||||
({ x: code }) => (
|
||||
<span className={classNames(locale, styles.row)}>
|
||||
<span className={classNames(styles.row)}>
|
||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||
{countryNames[code]}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import '@fontsource/inter/400.css';
|
|||
import '@fontsource/inter/700.css';
|
||||
import '@fontsource/inter/800.css';
|
||||
import 'react-basics/dist/styles.css';
|
||||
import 'styles/locale.css';
|
||||
import 'styles/index.css';
|
||||
import 'styles/variables.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useMemo, useState } from 'react';
|
|||
export interface BarChartProps extends ChartProps {
|
||||
unit: string;
|
||||
stacked?: boolean;
|
||||
currency?: string;
|
||||
renderXLabel?: (label: string, index: number, values: any[]) => string;
|
||||
renderYLabel?: (label: string, index: number, values: any[]) => string;
|
||||
XAxisType?: string;
|
||||
|
|
@ -27,6 +28,7 @@ export function BarChart(props: BarChartProps) {
|
|||
stacked = false,
|
||||
minDate,
|
||||
maxDate,
|
||||
currency,
|
||||
} = props;
|
||||
|
||||
const options: any = useMemo(() => {
|
||||
|
|
@ -76,7 +78,9 @@ export function BarChart(props: BarChartProps) {
|
|||
const handleTooltip = ({ tooltip }: { tooltip: any }) => {
|
||||
const { opacity } = tooltip;
|
||||
|
||||
setTooltip(opacity ? <BarChartTooltip tooltip={tooltip} unit={unit} /> : null);
|
||||
setTooltip(
|
||||
opacity ? <BarChartTooltip tooltip={tooltip} unit={unit} currency={currency} /> : null,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { formatDate } from 'lib/date';
|
||||
import { Flexbox, StatusLight } from 'react-basics';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import { formatDate } from 'lib/date';
|
||||
import { formatLongCurrency, formatLongNumber } from 'lib/format';
|
||||
import { Flexbox, StatusLight } from 'react-basics';
|
||||
|
||||
const formats = {
|
||||
millisecond: 'T',
|
||||
|
|
@ -15,7 +15,7 @@ const formats = {
|
|||
year: 'yyyy',
|
||||
};
|
||||
|
||||
export default function BarChartTooltip({ tooltip, unit }) {
|
||||
export default function BarChartTooltip({ tooltip, unit, currency }) {
|
||||
const { locale } = useLocale();
|
||||
const { labelColors, dataPoints } = tooltip;
|
||||
|
||||
|
|
@ -26,7 +26,10 @@ export default function BarChartTooltip({ tooltip, unit }) {
|
|||
</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
|
||||
{currency
|
||||
? formatLongCurrency(dataPoints[0].raw.y, currency)
|
||||
: formatLongNumber(dataPoints[0].raw.y)}{' '}
|
||||
{dataPoints[0].dataset.label}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</Flexbox>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
export * from './queries/useApi';
|
||||
export * from './queries/useConfig';
|
||||
export * from './queries/useEventDataEvents';
|
||||
export * from './queries/useEventDataProperties';
|
||||
export * from './queries/useEventDataValues';
|
||||
export * from './queries/usePagedQuery';
|
||||
export * from './queries/useLogin';
|
||||
export * from './queries/useRealtime';
|
||||
export * from './queries/useReport';
|
||||
|
|
@ -28,6 +26,7 @@ export * from './queries/useWebsiteEvents';
|
|||
export * from './queries/useWebsiteEventsSeries';
|
||||
export * from './queries/useWebsiteMetrics';
|
||||
export * from './queries/useWebsiteValues';
|
||||
export * from './useApi';
|
||||
export * from './useCountryNames';
|
||||
export * from './useDateRange';
|
||||
export * from './useDocumentClick';
|
||||
|
|
@ -41,6 +40,7 @@ export * from './useLocale';
|
|||
export * from './useMessages';
|
||||
export * from './useModified';
|
||||
export * from './useNavigation';
|
||||
export * from './usePagedQuery';
|
||||
export * from './useRegionNames';
|
||||
export * from './useSticky';
|
||||
export * from './useTeamUrl';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import useStore, { setConfig } from 'store/app';
|
||||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
let loading = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useEventDataProperties(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useEventDataValues(
|
||||
|
|
@ -12,7 +12,7 @@ export function useEventDataValues(
|
|||
const params = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<any>({
|
||||
queryKey: ['websites:event-data:values', { websiteId, propertyName, ...params }],
|
||||
queryKey: ['websites:event-data:values', { websiteId, eventName, propertyName, ...params }],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/event-data/values`, { ...params, eventName, propertyName }),
|
||||
enabled: !!(websiteId && propertyName),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import useStore, { setUser } from 'store/app';
|
||||
import useApi from './useApi';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
import useStore, { setUser } from 'store/app';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
const selector = (state: { user: any }) => state.user;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useTimezone } from 'components/hooks';
|
||||
import { REALTIME_INTERVAL } from 'lib/constants';
|
||||
import { RealtimeData } from 'lib/types';
|
||||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useRealtime(websiteId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { produce } from 'immer';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { useTimezone } from '../useTimezone';
|
||||
import { useMessages } from '../useMessages';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import useApi from '../useApi';
|
||||
import usePagedQuery from '../usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useReports({ websiteId, teamId }: { websiteId?: string; teamId?: string }) {
|
||||
|
|
|
|||
18
src/components/hooks/queries/useRevenueValues.ts
Normal file
18
src/components/hooks/queries/useRevenueValues.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useApi } from '../useApi';
|
||||
|
||||
export function useRevenueValues(websiteId: string, startDate: Date, endDate: Date) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['revenue:values', { websiteId, startDate, endDate }],
|
||||
queryFn: () =>
|
||||
get(`/reports/revenue`, {
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
}),
|
||||
enabled: !!(websiteId && startDate && endDate),
|
||||
});
|
||||
}
|
||||
|
||||
export default useRevenueValues;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useSessionActivity(
|
||||
websiteId: string,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useSessionData(websiteId: string, sessionId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useStore, { setShareToken } from 'store/app';
|
||||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
const selector = (state: { shareToken: string }) => state.shareToken;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useTeam(teamId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import { useApi } from '../useApi';
|
||||
import usePagedQuery from '../usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeamMembers(teamId: string) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeamWebsites(teamId: string) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useTeams(userId: string) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useUser(userId: string, options?: { [key: string]: any }) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import useApi from './useApi';
|
||||
import usePagedQuery from './usePagedQuery';
|
||||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
|
||||
export function useUsers() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useWebsite(websiteId: string, options?: { [key: string]: any }) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
import { usePagedQuery } from './usePagedQuery';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
|
||||
export function useWebsiteEvents(
|
||||
websiteId: string,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import useApi from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteMetrics(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '..//useFilterParams';
|
||||
|
||||
export function useWebsitePageviews(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useWebsiteSession(websiteId: string, sessionId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteSessionStats(websiteId: string, options?: { [key: string]: string }) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useApi } from './useApi';
|
||||
import { usePagedQuery } from './usePagedQuery';
|
||||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import useModified from '../useModified';
|
||||
import { useFilterParams } from 'components/hooks/useFilterParams';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import useModified from '../useModified';
|
||||
import { useFilterParams } from 'components/hooks/useFilterParams';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteStats(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export function useWebsiteValues({
|
||||
websiteId,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useApi } from './useApi';
|
||||
import { usePagedQuery } from './usePagedQuery';
|
||||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import { useLogin } from './useLogin';
|
||||
import useModified from '../useModified';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/
|
|||
import appStore, { setDateRange } from 'store/app';
|
||||
import { DateRange } from 'lib/types';
|
||||
import { useLocale } from './useLocale';
|
||||
import { useApi } from './queries/useApi';
|
||||
import { useApi } from './useApi';
|
||||
|
||||
export function useDateRange(websiteId?: string): {
|
||||
dateRange: DateRange;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export function useFields() {
|
|||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||
{ name: 'host', type: 'string', label: formatMessage(labels.host) },
|
||||
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
||||
];
|
||||
|
||||
return { fields };
|
||||
|
|
|
|||
|
|
@ -7,7 +7,21 @@ export function useFilterParams(websiteId: string) {
|
|||
const { startDate, endDate, unit } = dateRange;
|
||||
const { timezone, toUtc } = useTimezone();
|
||||
const {
|
||||
query: { url, referrer, title, query, host, os, browser, device, country, region, city, event },
|
||||
query: {
|
||||
url,
|
||||
referrer,
|
||||
title,
|
||||
query,
|
||||
host,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
region,
|
||||
city,
|
||||
event,
|
||||
tag,
|
||||
},
|
||||
} = useNavigation();
|
||||
|
||||
return {
|
||||
|
|
@ -27,5 +41,6 @@ export function useFilterParams(websiteId: string) {
|
|||
region,
|
||||
city,
|
||||
event,
|
||||
tag,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useApi } from './useApi';
|
||||
import { PageResult, PageParams, PagedQueryResult } from 'lib/types';
|
||||
import { useNavigation } from '../useNavigation';
|
||||
import { useApi } from './useApi';
|
||||
import { useNavigation } from './useNavigation';
|
||||
|
||||
export function usePagedQuery<T = any>({
|
||||
queryKey,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Icon, Button, PopupTrigger, Popup, Text } from 'react-basics';
|
||||
import { Icon, Button, PopupTrigger, Popup } from 'react-basics';
|
||||
import classNames from 'classnames';
|
||||
import { languages } from 'lib/lang';
|
||||
import { useLocale } from 'components/hooks';
|
||||
|
|
@ -33,7 +33,7 @@ export function LanguageButton() {
|
|||
className={classNames(styles.item, { [styles.selected]: value === locale })}
|
||||
onClick={(e: any) => handleSelect(value, close, e)}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
<span lang={value}>{label}</span>
|
||||
{value === locale && (
|
||||
<Icon className={styles.icon}>
|
||||
<Icons.Check />
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export const labels = defineMessages({
|
|||
devices: { id: 'label.devices', defaultMessage: 'Devices' },
|
||||
countries: { id: 'label.countries', defaultMessage: 'Countries' },
|
||||
languages: { id: 'label.languages', defaultMessage: 'Languages' },
|
||||
tags: { id: 'label.tags', defaultMessage: 'Tags' },
|
||||
count: { id: 'label.count', defaultMessage: 'Count' },
|
||||
average: { id: 'label.average', defaultMessage: 'Average' },
|
||||
sum: { id: 'label.sum', defaultMessage: 'Sum' },
|
||||
|
|
@ -114,8 +115,6 @@ export const labels = defineMessages({
|
|||
none: { id: 'label.none', defaultMessage: 'None' },
|
||||
clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' },
|
||||
property: { id: 'label.property', defaultMessage: 'Property' },
|
||||
revenueProperty: { id: 'label.revenue-property', defaultMessage: 'Revenue Property' },
|
||||
userProperty: { id: 'label.user-property', defaultMessage: 'User Property' },
|
||||
today: { id: 'label.today', defaultMessage: 'Today' },
|
||||
lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' },
|
||||
yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' },
|
||||
|
|
@ -162,8 +161,9 @@ export const labels = defineMessages({
|
|||
revenue: { id: 'label.revenue', defaultMessage: 'Revenue' },
|
||||
revenueDescription: {
|
||||
id: 'label.revenue-description',
|
||||
defaultMessage: 'Look into your revenue across time.',
|
||||
defaultMessage: 'Look into your revenue data and how users are spending.',
|
||||
},
|
||||
currency: { id: 'label.currency', defaultMessage: 'Currency' },
|
||||
url: { id: 'label.url', defaultMessage: 'URL' },
|
||||
urls: { id: 'label.urls', defaultMessage: 'URLs' },
|
||||
path: { id: 'label.path', defaultMessage: 'Path' },
|
||||
|
|
@ -221,6 +221,7 @@ export const labels = defineMessages({
|
|||
browser: { id: 'label.browser', defaultMessage: 'Browser' },
|
||||
device: { id: 'label.device', defaultMessage: 'Device' },
|
||||
pageTitle: { id: 'label.pageTitle', defaultMessage: 'Page title' },
|
||||
tag: { id: 'label.tag', defaultMessage: 'Tag' },
|
||||
day: { id: 'label.day', defaultMessage: 'Day' },
|
||||
date: { id: 'label.date', defaultMessage: 'Date' },
|
||||
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
|
||||
|
|
|
|||
|
|
@ -12,12 +12,7 @@ export function CountriesTable({ ...props }: MetricsTableProps) {
|
|||
|
||||
const renderLink = ({ x: code }) => {
|
||||
return (
|
||||
<FilterLink
|
||||
id="country"
|
||||
className={locale}
|
||||
value={countryNames[code] && code}
|
||||
label={formatCountry(code)}
|
||||
>
|
||||
<FilterLink id="country" value={countryNames[code] && code} label={formatCountry(code)}>
|
||||
<TypeIcon type="country" value={code?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function HostsTable(props: MetricsTableProps) {
|
|||
{...props}
|
||||
title={formatMessage(labels.hosts)}
|
||||
type="host"
|
||||
metric={formatMessage(labels.views)}
|
||||
metric={formatMessage(labels.visitors)}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function LanguagesTable({
|
|||
const languageNames = useLanguageNames(locale);
|
||||
|
||||
const renderLabel = ({ x }) => {
|
||||
return <div className={locale}>{languageNames[x?.split('-')[0]] ?? x}</div>;
|
||||
return languageNames[x?.split('-')[0]] ?? x;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { safeDecodeURIComponent } from 'next-basics';
|
|||
import { colord } from 'colord';
|
||||
import classNames from 'classnames';
|
||||
import { LegendItem } from 'chart.js/auto';
|
||||
import { useLocale } from 'components/hooks';
|
||||
import styles from './Legend.module.css';
|
||||
|
||||
export function Legend({
|
||||
|
|
@ -13,8 +12,6 @@ export function Legend({
|
|||
items: any[];
|
||||
onClick: (index: LegendItem) => void;
|
||||
}) {
|
||||
const { locale } = useLocale();
|
||||
|
||||
if (!items.find(({ text }) => text)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -32,7 +29,7 @@ export function Legend({
|
|||
onClick={() => onClick(item)}
|
||||
>
|
||||
<StatusLight color={color.alpha(color.alpha() + 0.2).toHex()}>
|
||||
<span className={locale}>{safeDecodeURIComponent(text)}</span>
|
||||
{safeDecodeURIComponent(text)}
|
||||
</StatusLight>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function RegionsTable(props: MetricsTableProps) {
|
|||
|
||||
const renderLink = ({ x: code, country }) => {
|
||||
return (
|
||||
<FilterLink id="region" className={locale} value={code} label={getRegionName(code, country)}>
|
||||
<FilterLink id="region" value={code} label={getRegionName(code, country)}>
|
||||
<TypeIcon type="country" value={country?.toLowerCase()} />
|
||||
</FilterLink>
|
||||
);
|
||||
|
|
|
|||
30
src/components/metrics/TagsTable.tsx
Normal file
30
src/components/metrics/TagsTable.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import MetricsTable, { MetricsTableProps } from './MetricsTable';
|
||||
import FilterLink from 'components/common/FilterLink';
|
||||
import { useMessages } from 'components/hooks';
|
||||
import { Flexbox } from 'react-basics';
|
||||
|
||||
export function TagsTable(props: MetricsTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const renderLink = ({ x: tag }) => {
|
||||
return (
|
||||
<Flexbox alignItems="center">
|
||||
<FilterLink id="tag" value={tag} label={!tag && formatMessage(labels.none)} />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={formatMessage(labels.tags)}
|
||||
type="tag"
|
||||
metric={formatMessage(labels.views)}
|
||||
renderLabel={renderLink}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TagsTable;
|
||||
|
|
@ -1,88 +1,88 @@
|
|||
{
|
||||
"label.access-code": "Access code",
|
||||
"label.access-code": "Přístupový kód",
|
||||
"label.actions": "Akce",
|
||||
"label.activity": "Activity log",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.activity": "Log aktivity",
|
||||
"label.add": "Přidat",
|
||||
"label.add-description": "Přidat popis",
|
||||
"label.add-member": "Přidat člena",
|
||||
"label.add-step": "Přidat krok",
|
||||
"label.add-website": "Přidat web",
|
||||
"label.admin": "Administrátor",
|
||||
"label.after": "After",
|
||||
"label.after": "Po",
|
||||
"label.all": "Vše",
|
||||
"label.all-time": "All time",
|
||||
"label.all-time": "Celá doba",
|
||||
"label.analytics": "Analytics",
|
||||
"label.average": "Average",
|
||||
"label.average": "Průměr",
|
||||
"label.back": "Zpět",
|
||||
"label.before": "Before",
|
||||
"label.before": "Před",
|
||||
"label.bounce-rate": "Okamžité opuštění",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
"label.browsers": "Prohlížeč",
|
||||
"label.browser": "Prohlížeč",
|
||||
"label.browsers": "Prohlížeče",
|
||||
"label.cancel": "Zrušit",
|
||||
"label.change-password": "Změnit heslo",
|
||||
"label.cities": "Cities",
|
||||
"label.city": "City",
|
||||
"label.clear-all": "Clear all",
|
||||
"label.compare": "Compare",
|
||||
"label.confirm": "Confirm",
|
||||
"label.cities": "Města",
|
||||
"label.city": "Město",
|
||||
"label.clear-all": "Vyčistit vše",
|
||||
"label.compare": "Porovnat",
|
||||
"label.confirm": "Potvrdit",
|
||||
"label.confirm-password": "Potvrdit heslo",
|
||||
"label.contains": "Contains",
|
||||
"label.continue": "Continue",
|
||||
"label.count": "Count",
|
||||
"label.countries": "Země",
|
||||
"label.country": "Country",
|
||||
"label.create": "Create",
|
||||
"label.create-report": "Create report",
|
||||
"label.create-team": "Create team",
|
||||
"label.create-user": "Create user",
|
||||
"label.created": "Created",
|
||||
"label.contains": "Obsahuje",
|
||||
"label.continue": "Pokračovat",
|
||||
"label.count": "Počet",
|
||||
"label.countries": "Státy",
|
||||
"label.country": "Stát",
|
||||
"label.create": "Vytvořit",
|
||||
"label.create-report": "Vytvořit hlášení",
|
||||
"label.create-team": "Vytvořit tým",
|
||||
"label.create-user": "Vytvořit uživatele",
|
||||
"label.created": "Vytvořeno",
|
||||
"label.created-by": "Created By",
|
||||
"label.current": "Current",
|
||||
"label.current": "Aktuální",
|
||||
"label.current-password": "Aktuální heslo",
|
||||
"label.custom-range": "Vlastní rozsah",
|
||||
"label.dashboard": "Přehled",
|
||||
"label.data": "Data",
|
||||
"label.date": "Date",
|
||||
"label.date": "Datum",
|
||||
"label.date-range": "Období",
|
||||
"label.day": "Day",
|
||||
"label.day": "Den",
|
||||
"label.default-date-range": "Výchozí období",
|
||||
"label.delete": "Smazat",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-team": "Delete team",
|
||||
"label.delete-user": "Delete user",
|
||||
"label.delete-report": "Smazat hlášení",
|
||||
"label.delete-team": "Smazat tým",
|
||||
"label.delete-user": "Smazat uživatele",
|
||||
"label.delete-website": "Smazat web",
|
||||
"label.description": "Description",
|
||||
"label.description": "Popis",
|
||||
"label.desktop": "Stolní počítač",
|
||||
"label.details": "Details",
|
||||
"label.device": "Device",
|
||||
"label.details": "Detaily",
|
||||
"label.device": "Zařízení",
|
||||
"label.devices": "Zařízení",
|
||||
"label.dismiss": "Odejít",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.does-not-contain": "Neobsahuje",
|
||||
"label.domain": "Doména",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.edit": "Upravit",
|
||||
"label.edit-dashboard": "Edit dashboard",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-dashboard": "Upravit dashboard",
|
||||
"label.edit-member": "Upravit člena",
|
||||
"label.enable-share-url": "Povolit sdílení URL",
|
||||
"label.end-step": "End Step",
|
||||
"label.entry": "Entry URL",
|
||||
"label.event": "Event",
|
||||
"label.entry": "Vstupní URL",
|
||||
"label.event": "Událost",
|
||||
"label.event-data": "Event data",
|
||||
"label.events": "Události",
|
||||
"label.exit": "Exit URL",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.field": "Pole",
|
||||
"label.fields": "Fields",
|
||||
"label.filter": "Filter",
|
||||
"label.filter": "Filtr",
|
||||
"label.filter-combined": "Kombinace",
|
||||
"label.filter-raw": "Nezpracované",
|
||||
"label.filters": "Filters",
|
||||
"label.filters": "Filtry",
|
||||
"label.first-seen": "First seen",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.goal": "Goal",
|
||||
"label.goals": "Goals",
|
||||
"label.goal": "Cíl",
|
||||
"label.goals": "Cíle",
|
||||
"label.goals-description": "Track your goals for pageviews and events.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
|
|
@ -98,44 +98,44 @@
|
|||
"label.join-team": "Join team",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.language": "Language",
|
||||
"label.languages": "Languages",
|
||||
"label.language": "Jazyk",
|
||||
"label.languages": "Jazyky",
|
||||
"label.laptop": "Přenosný počítač",
|
||||
"label.last-days": "Posledních {x} dnů",
|
||||
"label.last-hours": "Posledních {x} hodin",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.last-months": "Posledních {x} měsíců",
|
||||
"label.last-seen": "Last seen",
|
||||
"label.leave": "Leave",
|
||||
"label.leave-team": "Leave team",
|
||||
"label.leave": "Opustit",
|
||||
"label.leave-team": "Opustit tým",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.login": "Přihlásit",
|
||||
"label.logout": "Odhlásit",
|
||||
"label.manage": "Manage",
|
||||
"label.manager": "Manager",
|
||||
"label.manage": "Spravovat",
|
||||
"label.manager": "Správce",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.members": "Members",
|
||||
"label.member": "Člen",
|
||||
"label.members": "Členové",
|
||||
"label.min": "Min",
|
||||
"label.mobile": "Mobilní telefon",
|
||||
"label.more": "Více",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "Můj účet",
|
||||
"label.my-websites": "Mé weby",
|
||||
"label.name": "Jméno",
|
||||
"label.new-password": "Nové heslo",
|
||||
"label.none": "None",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.owner": "Owner",
|
||||
"label.overview": "Přehled",
|
||||
"label.owner": "Vlastník",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.page-views": "Zobrazení stránek",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.pageTitle": "Název stránky",
|
||||
"label.pages": "Stránky",
|
||||
"label.password": "Heslo",
|
||||
"label.path": "Path",
|
||||
"label.paths": "Paths",
|
||||
"label.path": "Cesta",
|
||||
"label.paths": "Cesty",
|
||||
"label.powered-by": "Běží na {name}",
|
||||
"label.previous": "Previous",
|
||||
"label.previous-period": "Previous period",
|
||||
|
|
@ -228,13 +228,13 @@
|
|||
"label.views": "Zobrazení",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.visit-duration": "Průměrný čas návštěvy",
|
||||
"label.visitors": "Návštěvy",
|
||||
"label.visits": "Visits",
|
||||
"label.visitors": "Návštěvníci",
|
||||
"label.visits": "Návštěvy",
|
||||
"label.website": "Website",
|
||||
"label.website-id": "Website ID",
|
||||
"label.websites": "Weby",
|
||||
"label.window": "Window",
|
||||
"label.yesterday": "Yesterday",
|
||||
"label.window": "Okno",
|
||||
"label.yesterday": "Včera",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.active-users": "{x} aktuálně {x, plural, one {návštěvník} other {návštěvníci}}",
|
||||
"message.collected-data": "Collected data",
|
||||
|
|
|
|||
|
|
@ -2,278 +2,278 @@
|
|||
"label.access-code": "Zuegangscode",
|
||||
"label.actions": "Aktione",
|
||||
"label.activity": "Aktivitätsverlauf",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.add": "hinzuefüege",
|
||||
"label.add-description": "Beschriibig hinzuefüege",
|
||||
"label.add-member": "Mitglied hinzuefüege",
|
||||
"label.add-step": "Schritt hinzuefüege",
|
||||
"label.add-website": "Websiite hinzuefüege",
|
||||
"label.admin": "Administrator",
|
||||
"label.after": "After",
|
||||
"label.after": "Nach",
|
||||
"label.all": "Alli",
|
||||
"label.all-time": "Gesamte Zitruum",
|
||||
"label.all-time": "Gsamte Zitruum",
|
||||
"label.analytics": "Analytics",
|
||||
"label.average": "Average",
|
||||
"label.average": "Durchschnitt",
|
||||
"label.back": "Zrugg",
|
||||
"label.before": "Before",
|
||||
"label.before": "Vor",
|
||||
"label.bounce-rate": "Absprungsrate",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.breakdown": "Uufschlüsselig",
|
||||
"label.browser": "Browser",
|
||||
"label.browsers": "Browser",
|
||||
"label.cancel": "Abbreche",
|
||||
"label.change-password": "Passwort ändere",
|
||||
"label.cities": "Städt",
|
||||
"label.city": "City",
|
||||
"label.city": "Stadt",
|
||||
"label.clear-all": "Alles lösche",
|
||||
"label.compare": "Compare",
|
||||
"label.compare": "Vergliiche",
|
||||
"label.confirm": "Bestätige",
|
||||
"label.confirm-password": "Passwort widerhole",
|
||||
"label.contains": "Contains",
|
||||
"label.contains": "Enthaltet",
|
||||
"label.continue": "Wiiter",
|
||||
"label.count": "Count",
|
||||
"label.count": "Azahl",
|
||||
"label.countries": "Länder",
|
||||
"label.country": "Country",
|
||||
"label.create": "Create",
|
||||
"label.create-report": "Create report",
|
||||
"label.country": "Land",
|
||||
"label.create": "Erstelle",
|
||||
"label.create-report": "Bricht erstelle",
|
||||
"label.create-team": "Team erstelle",
|
||||
"label.create-user": "Benutzer erstelle",
|
||||
"label.created": "Erstellt",
|
||||
"label.created-by": "Created By",
|
||||
"label.current": "Current",
|
||||
"label.current-password": "Jetzigs Passwort",
|
||||
"label.current": "Aktuell",
|
||||
"label.current-password": "Aktuells Passwort",
|
||||
"label.custom-range": "Benutzerdefinierte Bereich",
|
||||
"label.dashboard": "Übersicht",
|
||||
"label.data": "Datä",
|
||||
"label.date": "Date",
|
||||
"label.date": "Datum",
|
||||
"label.date-range": "Datumsbereich",
|
||||
"label.day": "Day",
|
||||
"label.default-date-range": "Vorigstellte Datumsbereich",
|
||||
"label.day": "Tag",
|
||||
"label.default-date-range": "Voriigstellte Datumsbereich",
|
||||
"label.delete": "Lösche",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-report": "Bricht lösche",
|
||||
"label.delete-team": "Team lösche",
|
||||
"label.delete-user": "Benutzer lösche",
|
||||
"label.delete-website": "Websiite lösche",
|
||||
"label.description": "Description",
|
||||
"label.description": "Beschriibig",
|
||||
"label.desktop": "Desktop",
|
||||
"label.details": "Details",
|
||||
"label.device": "Device",
|
||||
"label.device": "Grät",
|
||||
"label.devices": "Grät",
|
||||
"label.dismiss": "Verwerfe",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.dismiss": "Verwärfe",
|
||||
"label.does-not-contain": "Enthaltet nid",
|
||||
"label.domain": "Domain",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.dropoff": "Absprung",
|
||||
"label.edit": "Bearbeite",
|
||||
"label.edit-dashboard": "Dashboard bearbeite",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-member": "Mitglied bearbeite",
|
||||
"label.enable-share-url": "Freigab-URL aktiviere",
|
||||
"label.end-step": "End Step",
|
||||
"label.entry": "Entry URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.end-step": "Schlussschritt",
|
||||
"label.entry": "Iigangs URL",
|
||||
"label.event": "Ereigniss",
|
||||
"label.event-data": "Ereigniss Date",
|
||||
"label.events": "Ereigniss",
|
||||
"label.exit": "Exit URL",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.exit": "Uusgangs URL",
|
||||
"label.false": "Falsch",
|
||||
"label.field": "Fäld",
|
||||
"label.fields": "Fälder",
|
||||
"label.filter": "Filter",
|
||||
"label.filter-combined": "Kombiniert",
|
||||
"label.filter-raw": "Rohdate",
|
||||
"label.filters": "Filters",
|
||||
"label.first-seen": "First seen",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.goal": "Goal",
|
||||
"label.goals": "Goals",
|
||||
"label.goals-description": "Track your goals for pageviews and events.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
"label.first-seen": "Erstmal gse",
|
||||
"label.funnel": "Tunnel",
|
||||
"label.funnel-description": "Verstönd Sie d Konversions- und Abspruungsrate vo Nutzer.",
|
||||
"label.goal": "Ziel",
|
||||
"label.goals": "Ziele",
|
||||
"label.goals-description": "verfolged Sie Ihri Ziel für Siitenufrüef und Ereigniss.",
|
||||
"label.greater-than": "Grösser als",
|
||||
"label.greater-than-equals": "Grösser oder gliich",
|
||||
"label.host": "Host",
|
||||
"label.hosts": "Hosts",
|
||||
"label.insights": "Insights",
|
||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
||||
"label.is": "Is",
|
||||
"label.is-not": "Is not",
|
||||
"label.is-not-set": "Is not set",
|
||||
"label.is-set": "Is set",
|
||||
"label.insights": "Iiblick",
|
||||
"label.insights-description": "Vertüfed Sie sich i Ihri Date, mit Hilf vo Segment und Filter.",
|
||||
"label.is": "Isch",
|
||||
"label.is-not": "Isch nid",
|
||||
"label.is-not-set": "Isch ned gsetzt",
|
||||
"label.is-set": "Isch gsetzt",
|
||||
"label.join": "Biträte",
|
||||
"label.join-team": "Team biträte",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.journey": "Reis",
|
||||
"label.journey-description": "Verstönd Sie, wie Nutzer dur Ihri Website navigiered.",
|
||||
"label.language": "Sprach",
|
||||
"label.languages": "Sprache",
|
||||
"label.laptop": "Laptop",
|
||||
"label.last-days": "Letzti {x} Täg",
|
||||
"label.last-hours": "Letzti {x} Stunde",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.last-seen": "Last seen",
|
||||
"label.last-months": "Letzti {x} Mönet",
|
||||
"label.last-seen": "Zletzt gse",
|
||||
"label.leave": "Verlah",
|
||||
"label.leave-team": "Team verlah",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.login": "Aamelde",
|
||||
"label.logout": "Abmelde",
|
||||
"label.manage": "Manage",
|
||||
"label.less-than": "Kliiner als",
|
||||
"label.less-than-equals": "Kliiner oder gliich",
|
||||
"label.login": "Aamälde",
|
||||
"label.logout": "Abmälde",
|
||||
"label.manage": "Verwalte",
|
||||
"label.manager": "Manager",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.member": "Mitglied",
|
||||
"label.members": "Mitglieder",
|
||||
"label.min": "Min",
|
||||
"label.mobile": "Handy",
|
||||
"label.mobile": "Händy",
|
||||
"label.more": "Meh",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "Min Account",
|
||||
"label.my-websites": "Mini Websiite",
|
||||
"label.name": "Name",
|
||||
"label.new-password": "Neus Passwort",
|
||||
"label.none": "Keis",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.overview": "Übersicht",
|
||||
"label.owner": "Bsitzer",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.page-of": "Siite {current} vo {total}",
|
||||
"label.page-views": "Siitenufrüef",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.pageTitle": "Siitetitel",
|
||||
"label.pages": "Siite",
|
||||
"label.password": "Passwort",
|
||||
"label.path": "Path",
|
||||
"label.paths": "Paths",
|
||||
"label.powered-by": "Betribe dur {name}",
|
||||
"label.previous": "Previous",
|
||||
"label.previous-period": "Previous period",
|
||||
"label.previous-year": "Previous year",
|
||||
"label.path": "Pfad",
|
||||
"label.paths": "Pfade",
|
||||
"label.powered-by": "Betriibe dur {name}",
|
||||
"label.previous": "Vorherig",
|
||||
"label.previous-period": "Vorherigi Periode",
|
||||
"label.previous-year": "Vorherigs Jahr",
|
||||
"label.profile": "Profil",
|
||||
"label.properties": "Properties",
|
||||
"label.property": "Property",
|
||||
"label.queries": "Abfrage",
|
||||
"label.query": "Query",
|
||||
"label.query": "Abfrag",
|
||||
"label.query-parameters": "Abfragparameter",
|
||||
"label.realtime": "Echtzit",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrers": "Referrer",
|
||||
"label.referrer": "Verwiiser",
|
||||
"label.referrers": "Verwiisendi",
|
||||
"label.refresh": "Aktualisiere",
|
||||
"label.regenerate": "Erneuere",
|
||||
"label.region": "Region",
|
||||
"label.regions": "Regionä",
|
||||
"label.remove": "Entferne",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.reports": "Reports",
|
||||
"label.remove-member": "Mitglied entferne",
|
||||
"label.reports": "Brichte",
|
||||
"label.required": "Erforderlich",
|
||||
"label.reset": "Zruggsetze",
|
||||
"label.reset-website": "Statistik zruggsetze",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
||||
"label.revenue": "Revenue",
|
||||
"label.revenue-description": "Look into your revenue across time.",
|
||||
"label.revenue-property": "Revenue Property",
|
||||
"label.retention-description": "Mässed Sie d Verwiilduur vo Ihrere Website, indem Sie verfolged wie oft ihri Nutzer zruggkehred.",
|
||||
"label.revenue": "Umsatz",
|
||||
"label.revenue-description": "Lueged Sie sich Ihre Umsatz im Lauf vor Ziit a.",
|
||||
"label.revenue-property": "Umsatzeigenschafte",
|
||||
"label.role": "Rollä",
|
||||
"label.run-query": "Run query",
|
||||
"label.run-query": "Abfrag starte",
|
||||
"label.save": "Speichere",
|
||||
"label.screens": "Bildschirmuflösige",
|
||||
"label.search": "Search",
|
||||
"label.select": "Select",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-role": "Select role",
|
||||
"label.search": "Sueche",
|
||||
"label.select": "Auswähle",
|
||||
"label.select-date": "Datä uuswähle",
|
||||
"label.select-role": "Rollä uuswähle",
|
||||
"label.select-website": "Websiite uuswähle",
|
||||
"label.session": "Session",
|
||||
"label.sessions": "Sessions",
|
||||
"label.session": "Sitzig",
|
||||
"label.sessions": "Sitzige",
|
||||
"label.settings": "Istellige",
|
||||
"label.share-url": "Freigab-URL",
|
||||
"label.single-day": "Ein Tag",
|
||||
"label.start-step": "Start Step",
|
||||
"label.steps": "Steps",
|
||||
"label.sum": "Sum",
|
||||
"label.start-step": "Startschritt",
|
||||
"label.steps": "Schritt",
|
||||
"label.sum": "Summe",
|
||||
"label.tablet": "Tablet",
|
||||
"label.team": "Team",
|
||||
"label.team-id": "Team ID",
|
||||
"label.team-manager": "Team manager",
|
||||
"label.team-manager": "Team Manager",
|
||||
"label.team-member": "Team Mitglied",
|
||||
"label.team-name": "Team name",
|
||||
"label.team-name": "Team Name",
|
||||
"label.team-owner": "Team Bsitzer",
|
||||
"label.team-view-only": "Team view only",
|
||||
"label.team-websites": "Team websites",
|
||||
"label.team-view-only": "Nur für Teammitglieder sichtbar",
|
||||
"label.team-websites": "Team Websiite",
|
||||
"label.teams": "Teams",
|
||||
"label.theme": "Thema",
|
||||
"label.this-month": "De Monet",
|
||||
"label.this-week": "Die Wuche",
|
||||
"label.this-year": "Das Jahr",
|
||||
"label.this-month": "Dä Monet",
|
||||
"label.this-week": "Diä Wuuche",
|
||||
"label.this-year": "Das Johr",
|
||||
"label.timezone": "Ziitzone",
|
||||
"label.title": "Titel",
|
||||
"label.today": "Hüt",
|
||||
"label.toggle-charts": "Schaubilder umschalte",
|
||||
"label.toggle-charts": "Charts umschalte",
|
||||
"label.total": "Total",
|
||||
"label.total-records": "Total records",
|
||||
"label.total-records": "Gsamti Datesätz",
|
||||
"label.tracking-code": "Tracking Code",
|
||||
"label.transactions": "Transactions",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.true": "True",
|
||||
"label.type": "Type",
|
||||
"label.unique": "Unique",
|
||||
"label.unique-visitors": "Eidütigi Bsuecher",
|
||||
"label.uniqueCustomers": "Unique Customers",
|
||||
"label.transactions": "Transaktione",
|
||||
"label.transfer": "Transferiere",
|
||||
"label.transfer-website": "Websiite transferiere",
|
||||
"label.true": "Wahr",
|
||||
"label.type": "Typ",
|
||||
"label.unique": "Einzigartigi",
|
||||
"label.unique-visitors": "Einzigartigi Bsuecher",
|
||||
"label.uniqueCustomers": "Einzigartigi Kunde",
|
||||
"label.unknown": "Unbekannt",
|
||||
"label.untitled": "Untitled",
|
||||
"label.untitled": "Unbennant",
|
||||
"label.update": "Update",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "Benutzer",
|
||||
"label.user-property": "User Property",
|
||||
"label.user-property": "Benutzereigeschafte",
|
||||
"label.username": "Benutzername",
|
||||
"label.users": "Benutzer",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.value": "Value",
|
||||
"label.utm-description": "Tracked Sie Ihri Kampagnen mit UTM Parameters.",
|
||||
"label.value": "Wärt",
|
||||
"label.view": "Azeige",
|
||||
"label.view-details": "Details azeige",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "Nume aluege",
|
||||
"label.views": "Ufrüef",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.views-per-visit": "Ufrüef pro Bsuech",
|
||||
"label.visit-duration": "Durchschn. Bsuechsziit",
|
||||
"label.visitors": "Bsuecher",
|
||||
"label.visits": "Visits",
|
||||
"label.visits": "Bsüech",
|
||||
"label.website": "Website",
|
||||
"label.website-id": "Websiite ID",
|
||||
"label.websites": "Websiite",
|
||||
"label.window": "Window",
|
||||
"label.window": "Fenster",
|
||||
"label.yesterday": "Gester",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.action-confirmation": "Typed Sie {confirmation} is Feld underhalb um z bestätige.",
|
||||
"message.active-users": "{x} {x, plural, one {aktive Bsuecher} other {aktivi Bsuecher}}",
|
||||
"message.collected-data": "Collected data",
|
||||
"message.collected-data": "Gsammleti Date",
|
||||
"message.confirm-delete": "Sind Sie sich sicher, {target} zlösche?",
|
||||
"message.confirm-leave": "Sind Sie sich sicher, {target} zverlah?",
|
||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
||||
"message.confirm-reset": "Sind Sie sicher, dass Sie dStatistike vo {target} zruggsetze wend?",
|
||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
||||
"message.delete-website-warning": "Alli dezueghörige Date werdet ebefalls glöscht.",
|
||||
"message.error": "Es isch en Fehler uftrete.",
|
||||
"message.confirm-remove": "Sind Sie sich sicher, dass Sie {target} wänd entferne?",
|
||||
"message.confirm-reset": "Sind Sie sicher, dass Sie d Statistike vo {target} zruggsetze wänd?",
|
||||
"message.delete-team-warning": "Es Team lösche dued ebefalls alli team Websiite lösche.",
|
||||
"message.delete-website-warning": "Alli dezueghörige Date werded ebefalls glöscht.",
|
||||
"message.error": "Es isch en Fehler ufträte.",
|
||||
"message.event-log": "{event} uf {url}",
|
||||
"message.go-to-settings": "Zu de Istellige",
|
||||
"message.incorrect-username-password": "Falschs Passwort oder Benutzername.",
|
||||
"message.incorrect-username-password": "Falsches Passwort oder Benutzername.",
|
||||
"message.invalid-domain": "Ungültigi Domain",
|
||||
"message.min-password-length": "Miminamli längi vo {n} Zeiche",
|
||||
"message.new-version-available": "A new version of Umami {version} is available!",
|
||||
"message.new-version-available": "Es isch en neue Version vo Umami {version} verfügbar!",
|
||||
"message.no-data-available": "Kei Date vorhande.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "Es sind kei Event Date verfügbar.",
|
||||
"message.no-match-password": "Passwörter stimmed ned überi",
|
||||
"message.no-results-found": "No results were found.",
|
||||
"message.no-results-found": "Kei Ergäbnis gfunde.",
|
||||
"message.no-team-websites": "Dem Team sind kei Websiite zuegordnet.",
|
||||
"message.no-teams": "Bisher sind no kei Teams erstellt worde.",
|
||||
"message.no-users": "Da gits kei Benutzer",
|
||||
"message.no-websites-configured": "Es isch kei Websiite vorhande.",
|
||||
"message.page-not-found": "Siite ned gfunde.",
|
||||
"message.reset-website": "To reset this website, type {confirmation} in the box below to confirm.",
|
||||
"message.reset-website": "Um die Websiite zruggzsetze, typed Sie {confirmation} is Feld unde dran.",
|
||||
"message.reset-website-warning": "Alli Date für die Websiite werdet glöscht, nur de Tracking Code blibt bestah.",
|
||||
"message.saved": "Erfolgrich gspeichert.",
|
||||
"message.share-url": "Ihri Websiitestatistik isch under de folgende URL öffentlich zuegänglich:",
|
||||
"message.team-already-member": "Sie sind bereits es Mitglied vo dem Team.",
|
||||
"message.team-already-member": "Sie sind bereits es Mitglied vo däm Team.",
|
||||
"message.team-not-found": "Team nöd gfunde.",
|
||||
"message.team-websites-info": "Websiite chönd vo jedem im Team agluegt werde",
|
||||
"message.team-websites-info": "Websiite chöi vo jedem im Team agluegt werde",
|
||||
"message.tracking-code": "Tracking Code",
|
||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
||||
"message.triggered-event": "Triggered event",
|
||||
"message.user-deleted": "Benutzer glöscht.",
|
||||
"message.viewed-page": "Viewed page",
|
||||
"message.visitor-log": "Bsuecher us {country} benutzt {browser} uf {os} {device}",
|
||||
"message.visitors-dropped-off": "Visitors dropped off"
|
||||
"message.transfer-team-website-to-user": "Websiite uf zu Ihrem Account transferiere?",
|
||||
"message.transfer-user-website-to-team": "Wähled Sie s Team zum däm Websiite transferiert werde söll.",
|
||||
"message.transfer-website": "Übertraged Sie d Websiite Eigetümerrecht uf Ihre Account oder uf es anders Team",
|
||||
"message.triggered-event": "Usglösts Ereigniss",
|
||||
"message.user-deleted": "Bnutzer glöscht.",
|
||||
"message.viewed-page": "Siite agluegt",
|
||||
"message.visitor-log": "Bsuecher us {country} nutzt {browser} uf {os} {device}",
|
||||
"message.visitors-dropped-off": "Bsuercher verlore"
|
||||
}
|
||||
|
|
@ -67,8 +67,8 @@
|
|||
"label.enable-share-url": "Freigabe-URL aktivieren",
|
||||
"label.end-step": "Schlussschritt",
|
||||
"label.entry": "Eingangs-URL",
|
||||
"label.event": "Ereigniss",
|
||||
"label.event-data": "Ereignissdaten",
|
||||
"label.event": "Ereignis",
|
||||
"label.event-data": "Ereignisdaten",
|
||||
"label.events": "Ereignisse",
|
||||
"label.exit": "Ausgangs-URL",
|
||||
"label.false": "Falsch",
|
||||
|
|
@ -271,7 +271,7 @@
|
|||
"message.transfer-team-website-to-user": "Diese Website zu Ihrem Account transferieren?",
|
||||
"message.transfer-user-website-to-team": "Wählen Sie ein Team aus, zu dem die Website transferiert werden soll.",
|
||||
"message.transfer-website": "Übertragen Sie die Eigentümerrechte zu Ihrem Account oder einem anderen Team.",
|
||||
"message.triggered-event": "Ausgelöstes Ereigniss",
|
||||
"message.triggered-event": "Ereignis ausgelöst",
|
||||
"message.user-deleted": "Benutzer gelöscht.",
|
||||
"message.viewed-page": "Seite besucht",
|
||||
"message.visitor-log": "Besucher aus {country} benutzt {browser} auf {os} {device}",
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@
|
|||
"label.reset": "초기화",
|
||||
"label.reset-website": "웹사이트 초기화",
|
||||
"label.retention": "리텐션",
|
||||
"label.retention-description": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하십시오.",
|
||||
"label.retention-description": "사용자가 얼마나 자주 돌아오는지를 추적하여 웹사이트의 리텐션을 측정하세요.",
|
||||
"label.revenue": "수익",
|
||||
"label.revenue-description": "시간대별 수익을 살펴보세요.",
|
||||
"label.revenue-property": "수익 속성",
|
||||
|
|
@ -220,14 +220,14 @@
|
|||
"label.username": "사용자 이름",
|
||||
"label.users": "사용자",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "UTM 매개변수를 통해 캠페인을 추적합니다.",
|
||||
"label.utm-description": "UTM 매개변수를 통해 캠페인을 추적하세요.",
|
||||
"label.value": "값",
|
||||
"label.view": "보기",
|
||||
"label.view-details": "자세히 보기",
|
||||
"label.view-only": "보기 전용",
|
||||
"label.views": "조회",
|
||||
"label.views-per-visit": "방문당 조회",
|
||||
"label.visit-duration": "평균 방문 시간",
|
||||
"label.visit-duration": "방문 시간",
|
||||
"label.visitors": "방문자",
|
||||
"label.visits": "방문",
|
||||
"label.website": "웹사이트",
|
||||
|
|
@ -244,7 +244,7 @@
|
|||
"message.confirm-reset": "{target}을(를) 초기화하시겠습니까?",
|
||||
"message.delete-team-warning": "팀을 삭제하면 팀에 등록된 모든 웹사이트도 삭제됩니다.",
|
||||
"message.delete-website-warning": "관련된 모든 데이터가 삭제됩니다.",
|
||||
"message.error": "오류가 발생했습니다.",
|
||||
"message.error": "문제가 발생했습니다.",
|
||||
"message.event-log": "{event} - {url}",
|
||||
"message.go-to-settings": "설정으로 이동",
|
||||
"message.incorrect-username-password": "사용자 이름 또는 비밀번호를 잘못 입력했습니다.",
|
||||
|
|
|
|||
|
|
@ -2,194 +2,194 @@
|
|||
"label.access-code": "Код доступа",
|
||||
"label.actions": "Действия",
|
||||
"label.activity": "Журнал активности",
|
||||
"label.add": "Add",
|
||||
"label.add-description": "Add description",
|
||||
"label.add-member": "Add member",
|
||||
"label.add-step": "Add step",
|
||||
"label.add": "Добавить",
|
||||
"label.add-description": "Добавить описание",
|
||||
"label.add-member": "Добавить участника",
|
||||
"label.add-step": "Добавить шаг",
|
||||
"label.add-website": "Добавить сайт",
|
||||
"label.admin": "Администратор",
|
||||
"label.after": "After",
|
||||
"label.after": "После",
|
||||
"label.all": "Все",
|
||||
"label.all-time": "Все время",
|
||||
"label.analytics": "Аналитика",
|
||||
"label.average": "Average",
|
||||
"label.average": "Средний",
|
||||
"label.back": "Назад",
|
||||
"label.before": "Before",
|
||||
"label.before": "До",
|
||||
"label.bounce-rate": "Отказы",
|
||||
"label.breakdown": "Breakdown",
|
||||
"label.browser": "Browser",
|
||||
"label.breakdown": "Авария",
|
||||
"label.browser": "Браузер",
|
||||
"label.browsers": "Браузеры",
|
||||
"label.cancel": "Отменить",
|
||||
"label.change-password": "Изменить пароль",
|
||||
"label.cities": "Города",
|
||||
"label.city": "City",
|
||||
"label.city": "Город",
|
||||
"label.clear-all": "Очистить все",
|
||||
"label.compare": "Compare",
|
||||
"label.compare": "Сравнить",
|
||||
"label.confirm": "Подтвердить",
|
||||
"label.confirm-password": "Подтвердить пароль",
|
||||
"label.contains": "Contains",
|
||||
"label.contains": "Содержит",
|
||||
"label.continue": "Продолжить",
|
||||
"label.count": "Count",
|
||||
"label.count": "Считать",
|
||||
"label.countries": "Страны",
|
||||
"label.country": "Country",
|
||||
"label.create": "Create",
|
||||
"label.create-report": "Create report",
|
||||
"label.country": "Страна",
|
||||
"label.create": "Создать",
|
||||
"label.create-report": "Создать отчет",
|
||||
"label.create-team": "Создать команду",
|
||||
"label.create-user": "Создать пользователя",
|
||||
"label.created": "Создано",
|
||||
"label.created-by": "Created By",
|
||||
"label.current": "Current",
|
||||
"label.created-by": "Создано",
|
||||
"label.current": "Текущий",
|
||||
"label.current-password": "Текущий пароль",
|
||||
"label.custom-range": "Другой период",
|
||||
"label.dashboard": "Информационная панель",
|
||||
"label.data": "Данные",
|
||||
"label.date": "Date",
|
||||
"label.date": "Дата",
|
||||
"label.date-range": "Диапазон дат",
|
||||
"label.day": "Day",
|
||||
"label.day": "День",
|
||||
"label.default-date-range": "Диапазон дат по-умолчанию",
|
||||
"label.delete": "Удалить",
|
||||
"label.delete-report": "Delete report",
|
||||
"label.delete-report": "Удалить отчет",
|
||||
"label.delete-team": "Удалить команду",
|
||||
"label.delete-user": "Удалить пользователя",
|
||||
"label.delete-website": "Удалить сайт",
|
||||
"label.description": "Description",
|
||||
"label.description": "Описание",
|
||||
"label.desktop": "Настольный компьютер",
|
||||
"label.details": "Подробности",
|
||||
"label.device": "Device",
|
||||
"label.device": "Устройство",
|
||||
"label.devices": "Устройства",
|
||||
"label.dismiss": "Отклонить",
|
||||
"label.does-not-contain": "Does not contain",
|
||||
"label.does-not-contain": "Не содержит",
|
||||
"label.domain": "Домен",
|
||||
"label.dropoff": "Dropoff",
|
||||
"label.dropoff": "Высадка",
|
||||
"label.edit": "Изменить",
|
||||
"label.edit-dashboard": "Редактировать дашборд",
|
||||
"label.edit-member": "Edit member",
|
||||
"label.edit-member": "Редактировать участника",
|
||||
"label.enable-share-url": "Разрешить делиться ссылкой",
|
||||
"label.end-step": "End Step",
|
||||
"label.entry": "Entry URL",
|
||||
"label.event": "Event",
|
||||
"label.event-data": "Event data",
|
||||
"label.end-step": "Конечный шаг",
|
||||
"label.entry": "URL-адрес входа",
|
||||
"label.event": "Событие",
|
||||
"label.event-data": "Данные о событии",
|
||||
"label.events": "События",
|
||||
"label.exit": "Exit URL",
|
||||
"label.false": "False",
|
||||
"label.field": "Field",
|
||||
"label.fields": "Fields",
|
||||
"label.filter": "Filter",
|
||||
"label.exit": "URL-адрес выхода",
|
||||
"label.false": "Ложь",
|
||||
"label.field": "Поле",
|
||||
"label.fields": "Поля",
|
||||
"label.filter": "Фильтр",
|
||||
"label.filter-combined": "Объединенные",
|
||||
"label.filter-raw": "Сырые данные",
|
||||
"label.filters": "Filters",
|
||||
"label.first-seen": "First seen",
|
||||
"label.funnel": "Funnel",
|
||||
"label.funnel-description": "Understand the conversion and drop-off rate of users.",
|
||||
"label.goal": "Goal",
|
||||
"label.goals": "Goals",
|
||||
"label.goals-description": "Track your goals for pageviews and events.",
|
||||
"label.greater-than": "Greater than",
|
||||
"label.greater-than-equals": "Greater than or equals",
|
||||
"label.filters": "Фильтры",
|
||||
"label.first-seen": "Первый вход",
|
||||
"label.funnel": "Воронка",
|
||||
"label.funnel-description": "Изучите коэффициент конверсии и ухода пользователей.",
|
||||
"label.goal": "Цель",
|
||||
"label.goals": "Цели",
|
||||
"label.goals-description": "Отслеживайте свои цели по просмотрам страниц и событиям.",
|
||||
"label.greater-than": "Больше, чем",
|
||||
"label.greater-than-equals": "Больше или равно",
|
||||
"label.host": "Host",
|
||||
"label.hosts": "Hosts",
|
||||
"label.insights": "Insights",
|
||||
"label.insights-description": "Dive deeper into your data by using segments and filters.",
|
||||
"label.is": "Is",
|
||||
"label.is-not": "Is not",
|
||||
"label.is-not-set": "Is not set",
|
||||
"label.is-set": "Is set",
|
||||
"label.insights": "Информация",
|
||||
"label.insights-description": "Погрузитесь глубже в свои данные с помощью сегментов и фильтров.",
|
||||
"label.is": "Является",
|
||||
"label.is-not": "Не установлен",
|
||||
"label.is-not-set": "Не установлено",
|
||||
"label.is-set": "Установлен",
|
||||
"label.join": "Присоединиться",
|
||||
"label.join-team": "Присоединиться к команде",
|
||||
"label.journey": "Journey",
|
||||
"label.journey-description": "Understand how users navigate through your website.",
|
||||
"label.journey-description": "Поймите, как пользователи перемещаются по вашему сайту.",
|
||||
"label.language": "Язык",
|
||||
"label.languages": "Языки",
|
||||
"label.laptop": "Ноутбук",
|
||||
"label.last-days": "Последние {x} дней",
|
||||
"label.last-hours": "Последние {x} часа",
|
||||
"label.last-months": "Last {x} months",
|
||||
"label.last-seen": "Last seen",
|
||||
"label.last-months": "Последние {x} месяцев",
|
||||
"label.last-seen": "Последний вход",
|
||||
"label.leave": "Уйти",
|
||||
"label.leave-team": "Покинуть команду",
|
||||
"label.less-than": "Less than",
|
||||
"label.less-than-equals": "Less than or equals",
|
||||
"label.less-than": "Меньше, чем",
|
||||
"label.less-than-equals": "Меньше или равно",
|
||||
"label.login": "Войти",
|
||||
"label.logout": "Выйти",
|
||||
"label.manage": "Manage",
|
||||
"label.manager": "Manager",
|
||||
"label.max": "Max",
|
||||
"label.member": "Member",
|
||||
"label.manage": "Управление",
|
||||
"label.manager": "Менеджер",
|
||||
"label.max": "Максимум",
|
||||
"label.member": "Участник",
|
||||
"label.members": "Участники",
|
||||
"label.min": "Min",
|
||||
"label.min": "Минимум",
|
||||
"label.mobile": "Смартфон",
|
||||
"label.more": "Больше",
|
||||
"label.my-account": "My account",
|
||||
"label.my-websites": "My websites",
|
||||
"label.my-account": "Мой профиль",
|
||||
"label.my-websites": "Мои сайты",
|
||||
"label.name": "Имя",
|
||||
"label.new-password": "Новый пароль",
|
||||
"label.none": "Не указано",
|
||||
"label.number-of-records": "{x} {x, plural, one {record} other {records}}",
|
||||
"label.number-of-records": "{x} {x, множественное число, один {запись} другие {записи}}",
|
||||
"label.ok": "OK",
|
||||
"label.os": "OS",
|
||||
"label.overview": "Overview",
|
||||
"label.overview": "Обзор",
|
||||
"label.owner": "Владелец",
|
||||
"label.page-of": "Page {current} of {total}",
|
||||
"label.page-of": "Страница {current} из {total}",
|
||||
"label.page-views": "Просмотры страниц",
|
||||
"label.pageTitle": "Page title",
|
||||
"label.pageTitle": "Название страницы",
|
||||
"label.pages": "Страницы",
|
||||
"label.password": "Пароль",
|
||||
"label.path": "Path",
|
||||
"label.paths": "Paths",
|
||||
"label.path": "Путь",
|
||||
"label.paths": "Пути",
|
||||
"label.powered-by": "На движке {name}",
|
||||
"label.previous": "Previous",
|
||||
"label.previous-period": "Previous period",
|
||||
"label.previous-year": "Previous year",
|
||||
"label.previous": "Предыдущий",
|
||||
"label.previous-period": "Предыдущий период",
|
||||
"label.previous-year": "Предыдущий год",
|
||||
"label.profile": "Профиль",
|
||||
"label.properties": "Properties",
|
||||
"label.property": "Property",
|
||||
"label.properties": "Свойства",
|
||||
"label.property": "Свойство",
|
||||
"label.queries": "Запросы",
|
||||
"label.query": "Query",
|
||||
"label.query": "Запрос",
|
||||
"label.query-parameters": "Параметры запроса",
|
||||
"label.realtime": "Реальное время",
|
||||
"label.referrer": "Referrer",
|
||||
"label.referrer": "Реферер",
|
||||
"label.referrers": "Источники",
|
||||
"label.refresh": "Обновить",
|
||||
"label.regenerate": "Обновить",
|
||||
"label.region": "Region",
|
||||
"label.region": "Регион",
|
||||
"label.regions": "Регионы",
|
||||
"label.remove": "Удалить",
|
||||
"label.remove-member": "Remove member",
|
||||
"label.reports": "Reports",
|
||||
"label.remove-member": "Удалить участника",
|
||||
"label.reports": "Отчеты",
|
||||
"label.required": "Обязательное",
|
||||
"label.reset": "Сбросить",
|
||||
"label.reset-website": "Сбросить статистику",
|
||||
"label.retention": "Retention",
|
||||
"label.retention-description": "Measure your website stickiness by tracking how often users return.",
|
||||
"label.revenue": "Revenue",
|
||||
"label.revenue-description": "Look into your revenue across time.",
|
||||
"label.revenue-property": "Revenue Property",
|
||||
"label.retention": "Удержание",
|
||||
"label.retention-description": "Измерьте «прилипаемость» вашего сайта, отслеживая, как часто пользователи возвращаются на него.",
|
||||
"label.revenue": "Выручка",
|
||||
"label.revenue-description": "Изучите свои доходы за определенное время.",
|
||||
"label.revenue-property": "Доходная недвижимость",
|
||||
"label.role": "Роль",
|
||||
"label.run-query": "Run query",
|
||||
"label.run-query": "Выполнить запрос",
|
||||
"label.save": "Сохранить",
|
||||
"label.screens": "Экраны",
|
||||
"label.search": "Search",
|
||||
"label.select": "Select",
|
||||
"label.select-date": "Select date",
|
||||
"label.select-role": "Select role",
|
||||
"label.search": "Поиск",
|
||||
"label.select": "Выберите",
|
||||
"label.select-date": "Выберите дату",
|
||||
"label.select-role": "Выберите роль",
|
||||
"label.select-website": "Выбрать сайт",
|
||||
"label.session": "Session",
|
||||
"label.session": "Сессия",
|
||||
"label.sessions": "Сессии",
|
||||
"label.settings": "Настройки",
|
||||
"label.share-url": "Поделиться ссылкой",
|
||||
"label.single-day": "Один день",
|
||||
"label.start-step": "Start Step",
|
||||
"label.steps": "Steps",
|
||||
"label.sum": "Sum",
|
||||
"label.start-step": "Начальный этап",
|
||||
"label.steps": "Шаги",
|
||||
"label.sum": "Сумма",
|
||||
"label.tablet": "Планшет",
|
||||
"label.team": "Команда",
|
||||
"label.team-id": "ID команды",
|
||||
"label.team-manager": "Team manager",
|
||||
"label.team-manager": "Менеджер команды",
|
||||
"label.team-member": "Член команды",
|
||||
"label.team-name": "Team name",
|
||||
"label.team-name": "Название команды",
|
||||
"label.team-owner": "Владелец команды",
|
||||
"label.team-view-only": "Team view only",
|
||||
"label.team-websites": "Team websites",
|
||||
"label.team-view-only": "Только командный просмотр",
|
||||
"label.team-websites": "Веб-сайты команды",
|
||||
"label.teams": "Команды",
|
||||
"label.theme": "Тема",
|
||||
"label.this-month": "Этот месяц",
|
||||
|
|
@ -199,62 +199,62 @@
|
|||
"label.title": "Заголовок",
|
||||
"label.today": "Сегодня",
|
||||
"label.toggle-charts": "Показать/скрыть графики",
|
||||
"label.total": "Total",
|
||||
"label.total-records": "Total records",
|
||||
"label.total": "Всего",
|
||||
"label.total-records": "Всего записей",
|
||||
"label.tracking-code": "Код отслеживания",
|
||||
"label.transactions": "Transactions",
|
||||
"label.transfer": "Transfer",
|
||||
"label.transfer-website": "Transfer website",
|
||||
"label.true": "True",
|
||||
"label.type": "Type",
|
||||
"label.unique": "Unique",
|
||||
"label.transactions": "Транзакции",
|
||||
"label.transfer": "Передача",
|
||||
"label.transfer-website": "Передать сайт",
|
||||
"label.true": "Правда",
|
||||
"label.type": "Тип",
|
||||
"label.unique": "Уникальный",
|
||||
"label.unique-visitors": "Уникальные посетители",
|
||||
"label.uniqueCustomers": "Unique Customers",
|
||||
"label.uniqueCustomers": "Уникальные клиенты",
|
||||
"label.unknown": "Неизвестно",
|
||||
"label.untitled": "Untitled",
|
||||
"label.update": "Update",
|
||||
"label.untitled": "Без названия",
|
||||
"label.update": "Обновление",
|
||||
"label.url": "URL",
|
||||
"label.urls": "URLs",
|
||||
"label.user": "Пользователь",
|
||||
"label.user-property": "User Property",
|
||||
"label.user-property": "Собственность пользователя",
|
||||
"label.username": "Имя пользователя",
|
||||
"label.users": "Пользователи",
|
||||
"label.utm": "UTM",
|
||||
"label.utm-description": "Track your campaigns through UTM parameters.",
|
||||
"label.value": "Value",
|
||||
"label.utm-description": "Отслеживайте свои кампании с помощью UTM-параметров.",
|
||||
"label.value": "Значение",
|
||||
"label.view": "Просмотреть",
|
||||
"label.view-details": "Посмотреть детали",
|
||||
"label.view-only": "View only",
|
||||
"label.view-only": "Только просмотр",
|
||||
"label.views": "Просмотры",
|
||||
"label.views-per-visit": "Views per visit",
|
||||
"label.views-per-visit": "Просмотров за посещение",
|
||||
"label.visit-duration": "Среднее время посещения",
|
||||
"label.visitors": "Посетители",
|
||||
"label.visits": "Visits",
|
||||
"label.website": "Website",
|
||||
"label.visits": "Посещения",
|
||||
"label.website": "Сайт",
|
||||
"label.website-id": "ID сайта",
|
||||
"label.websites": "Сайты",
|
||||
"label.window": "Window",
|
||||
"label.window": "Окно",
|
||||
"label.yesterday": "Вчера",
|
||||
"message.action-confirmation": "Type {confirmation} in the box below to confirm.",
|
||||
"message.action-confirmation": "Введите {confirmation} в поле ниже, чтобы подтвердить.",
|
||||
"message.active-users": "{x} текущих посетителей",
|
||||
"message.collected-data": "Collected data",
|
||||
"message.collected-data": "Собранные данные",
|
||||
"message.confirm-delete": "Вы уверены, что хотите удалить {target}?",
|
||||
"message.confirm-leave": "Вы уверены, что хотите уйти {target}?",
|
||||
"message.confirm-remove": "Are you sure you want to remove {target}?",
|
||||
"message.confirm-remove": "Вы уверены, что хотите удалить {target}?",
|
||||
"message.confirm-reset": "Вы уверены, что хотите сбросить статистику {target}?",
|
||||
"message.delete-team-warning": "Deleting a team will also delete all team websites.",
|
||||
"message.delete-team-warning": "При удалении команды будут удалены и все ее веб-сайты.",
|
||||
"message.delete-website-warning": "Все связанные данные будут также удалены.",
|
||||
"message.error": "Что-то пошло не так.",
|
||||
"message.event-log": "{event} on {url}",
|
||||
"message.event-log": "{event} на {url}",
|
||||
"message.go-to-settings": "Перейти к настройкам",
|
||||
"message.incorrect-username-password": "Неверное имя пользователя/пароль.",
|
||||
"message.invalid-domain": "Некорректный домен",
|
||||
"message.min-password-length": "Минимальная длина {n} символов",
|
||||
"message.new-version-available": "A new version of Umami {version} is available!",
|
||||
"message.new-version-available": "Вышла новая версия Umami {version}!",
|
||||
"message.no-data-available": "Нет данных.",
|
||||
"message.no-event-data": "No event data is available.",
|
||||
"message.no-event-data": "Данные о событиях отсутствуют.",
|
||||
"message.no-match-password": "Пароли не совпадают",
|
||||
"message.no-results-found": "No results were found.",
|
||||
"message.no-results-found": "Результаты не найдены.",
|
||||
"message.no-team-websites": "У этой команды нет ни одного сайта.",
|
||||
"message.no-teams": "Вы не создали ни одной команды.",
|
||||
"message.no-users": "Нет пользователей.",
|
||||
|
|
@ -268,12 +268,12 @@
|
|||
"message.team-not-found": "Команда не найдена.",
|
||||
"message.team-websites-info": "Сайты могут просматривать все члены команды.",
|
||||
"message.tracking-code": "Код отслеживания",
|
||||
"message.transfer-team-website-to-user": "Transfer this website to your account?",
|
||||
"message.transfer-user-website-to-team": "Select the team to transfer this website to.",
|
||||
"message.transfer-website": "Transfer website ownership to your account or another team.",
|
||||
"message.triggered-event": "Triggered event",
|
||||
"message.transfer-team-website-to-user": "Перенести этот сайт в свой прфоиль?",
|
||||
"message.transfer-user-website-to-team": "Выберите команду, которой нужно передать этот сайт.",
|
||||
"message.transfer-website": "Передайте право владения сайтом своей учетной записи или другой команде.",
|
||||
"message.triggered-event": "Запущенное событие",
|
||||
"message.user-deleted": "Пользователь удален.",
|
||||
"message.viewed-page": "Viewed page",
|
||||
"message.viewed-page": "Просмотренная страница",
|
||||
"message.visitor-log": "Посетитель из {country} используя {browser} на {os} {device}",
|
||||
"message.visitors-dropped-off": "Visitors dropped off"
|
||||
"message.visitors-dropped-off": "Высадка посетителей"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { ClickHouseClient, createClient } from '@clickhouse/client';
|
|||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import debug from 'debug';
|
||||
import { CLICKHOUSE } from 'lib/db';
|
||||
import { getWebsite } from 'queries/index';
|
||||
import { DEFAULT_PAGE_SIZE, OPERATORS } from './constants';
|
||||
import { maxDate } from './date';
|
||||
import { fetchWebsite } from './load';
|
||||
import { filtersToArray } from './params';
|
||||
import { PageParams, QueryFilters, QueryOptions } from './types';
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ function getFilterParams(filters: QueryFilters = {}) {
|
|||
}
|
||||
|
||||
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
|
||||
const website = await fetchWebsite(websiteId);
|
||||
const website = await getWebsite(websiteId);
|
||||
|
||||
return {
|
||||
filterQuery: getFilterQuery(filters, options),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const FILTER_REFERRERS = 'filter-referrers';
|
|||
export const FILTER_PAGES = 'filter-pages';
|
||||
|
||||
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
|
||||
export const EVENT_COLUMNS = ['url', 'entry', 'exit', 'referrer', 'title', 'query', 'event'];
|
||||
export const EVENT_COLUMNS = ['url', 'entry', 'exit', 'referrer', 'title', 'query', 'event', 'tag'];
|
||||
|
||||
export const SESSION_COLUMNS = [
|
||||
'browser',
|
||||
|
|
@ -63,6 +63,7 @@ export const FILTER_COLUMNS = {
|
|||
city: 'city',
|
||||
language: 'language',
|
||||
event: 'event_name',
|
||||
tag: 'tag',
|
||||
};
|
||||
|
||||
export const COLLECTION_TYPE = {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export function formatNumber(n: string | number) {
|
|||
export function formatLongNumber(value: number) {
|
||||
const n = Number(value);
|
||||
|
||||
if (n >= 1000000000) {
|
||||
return `${(n / 1000000).toFixed(1)}b`;
|
||||
}
|
||||
if (n >= 1000000) {
|
||||
return `${(n / 1000000).toFixed(1)}m`;
|
||||
}
|
||||
|
|
@ -78,3 +81,38 @@ export function stringToColor(str: string) {
|
|||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
export function formatCurrency(value: number, currency: string, locale = 'en-US') {
|
||||
let formattedValue;
|
||||
|
||||
try {
|
||||
formattedValue = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback to default currency format if an error occurs
|
||||
formattedValue = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
});
|
||||
}
|
||||
|
||||
return formattedValue.format(value);
|
||||
}
|
||||
|
||||
export function formatLongCurrency(value: number, currency: string, locale = 'en-US') {
|
||||
const n = Number(value);
|
||||
|
||||
if (n >= 1000000000) {
|
||||
return `${formatCurrency(n / 1000000000, currency, locale)}b`;
|
||||
}
|
||||
if (n >= 1000000) {
|
||||
return `${formatCurrency(n / 1000000, currency, locale)}m`;
|
||||
}
|
||||
if (n >= 1000) {
|
||||
return `${formatCurrency(n / 1000, currency, locale)}k`;
|
||||
}
|
||||
|
||||
return formatCurrency(n, currency, locale);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ export interface QueryFilters {
|
|||
language?: string;
|
||||
event?: string;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
|
|
|
|||
|
|
@ -5,49 +5,60 @@ import { TimezoneTest, UnitTypeTest } from 'lib/yup';
|
|||
import { NextApiResponse } from 'next';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getRevenue } from 'queries/analytics/reports/getRevenue';
|
||||
import { getRevenueValues } from 'queries/analytics/reports/getRevenueValues';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export interface RetentionRequestBody {
|
||||
export interface RevenueRequestBody {
|
||||
websiteId: string;
|
||||
dateRange: { startDate: string; endDate: string; unit?: string; timezone?: string };
|
||||
eventName: string;
|
||||
revenueProperty: string;
|
||||
userProperty: string;
|
||||
currency?: string;
|
||||
timezone?: string;
|
||||
dateRange: { startDate: string; endDate: string; unit?: string };
|
||||
}
|
||||
|
||||
const schema = {
|
||||
POST: yup.object().shape({
|
||||
websiteId: yup.string().uuid().required(),
|
||||
timezone: TimezoneTest,
|
||||
dateRange: yup
|
||||
.object()
|
||||
.shape({
|
||||
startDate: yup.date().required(),
|
||||
endDate: yup.date().required(),
|
||||
unit: UnitTypeTest,
|
||||
timezone: TimezoneTest,
|
||||
})
|
||||
.required(),
|
||||
eventName: yup.string().required(),
|
||||
revenueProperty: yup.string().required(),
|
||||
userProperty: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default async (
|
||||
req: NextApiRequestQueryBody<any, RetentionRequestBody>,
|
||||
req: NextApiRequestQueryBody<any, RevenueRequestBody>,
|
||||
res: NextApiResponse,
|
||||
) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
await useValidate(schema, req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { websiteId, startDate, endDate } = req.query;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const data = await getRevenueValues(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
});
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate, unit, timezone },
|
||||
eventName,
|
||||
revenueProperty,
|
||||
userProperty,
|
||||
currency,
|
||||
timezone,
|
||||
dateRange: { startDate, endDate, unit },
|
||||
} = req.body;
|
||||
|
||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||
|
|
@ -59,9 +70,7 @@ export default async (
|
|||
endDate: new Date(endDate),
|
||||
unit,
|
||||
timezone,
|
||||
eventName,
|
||||
revenueProperty,
|
||||
userProperty,
|
||||
currency,
|
||||
});
|
||||
|
||||
return ok(res, data);
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
|||
}
|
||||
|
||||
const { type, payload } = req.body;
|
||||
const { url, referrer, name: eventName, data, title } = payload;
|
||||
const { url, referrer, name: eventName, data, title, tag } = payload;
|
||||
const pageTitle = safeDecodeURI(title);
|
||||
|
||||
await useSession(req, res);
|
||||
|
|
@ -143,6 +143,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
|
|||
eventData: data,
|
||||
...session,
|
||||
sessionId: session.id,
|
||||
tag,
|
||||
});
|
||||
} else if (type === COLLECTION_TYPE.identify) {
|
||||
if (!data) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface WebsiteEventsRequestQuery {
|
|||
country?: string;
|
||||
region: string;
|
||||
city?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
|
|
@ -43,6 +44,7 @@ const schema = {
|
|||
country: yup.string(),
|
||||
region: yup.string(),
|
||||
city: yup.string(),
|
||||
tag: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface WebsiteMetricsRequestQuery {
|
|||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
|
|
@ -53,6 +54,7 @@ const schema = {
|
|||
limit: yup.number(),
|
||||
offset: yup.number(),
|
||||
search: yup.string(),
|
||||
tag: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export interface WebsitePageviewRequestQuery {
|
|||
country?: string;
|
||||
region: string;
|
||||
city?: string;
|
||||
tag?: string;
|
||||
compare?: string;
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ const schema = {
|
|||
country: yup.string(),
|
||||
region: yup.string(),
|
||||
city: yup.string(),
|
||||
tag: yup.string(),
|
||||
compare: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface WebsiteSessionStatsRequestQuery {
|
|||
country?: string;
|
||||
region?: string;
|
||||
city?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
|
|
@ -42,6 +43,7 @@ const schema = {
|
|||
country: yup.string(),
|
||||
region: yup.string(),
|
||||
city: yup.string(),
|
||||
tag: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface WebsiteStatsRequestQuery {
|
|||
country?: string;
|
||||
region?: string;
|
||||
city?: string;
|
||||
tag?: string;
|
||||
compare?: string;
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ const schema = {
|
|||
country: yup.string(),
|
||||
region: yup.string(),
|
||||
city: yup.string(),
|
||||
tag: yup.string(),
|
||||
compare: yup.string(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function saveEvent(args: {
|
|||
subdivision1?: string;
|
||||
subdivision2?: string;
|
||||
city?: string;
|
||||
tag?: string;
|
||||
}) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(args),
|
||||
|
|
@ -47,6 +48,7 @@ async function relationalQuery(data: {
|
|||
pageTitle?: string;
|
||||
eventName?: string;
|
||||
eventData?: any;
|
||||
tag?: string;
|
||||
}) {
|
||||
const {
|
||||
websiteId,
|
||||
|
|
@ -60,6 +62,7 @@ async function relationalQuery(data: {
|
|||
eventName,
|
||||
eventData,
|
||||
pageTitle,
|
||||
tag,
|
||||
} = data;
|
||||
const websiteEventId = uuid();
|
||||
|
||||
|
|
@ -77,6 +80,7 @@ async function relationalQuery(data: {
|
|||
pageTitle: pageTitle?.substring(0, PAGE_TITLE_LENGTH),
|
||||
eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||
tag,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -116,6 +120,7 @@ async function clickhouseQuery(data: {
|
|||
subdivision1?: string;
|
||||
subdivision2?: string;
|
||||
city?: string;
|
||||
tag?: string;
|
||||
}) {
|
||||
const {
|
||||
websiteId,
|
||||
|
|
@ -133,6 +138,7 @@ async function clickhouseQuery(data: {
|
|||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
tag,
|
||||
...args
|
||||
} = data;
|
||||
const { insert, getUTCString } = clickhouse;
|
||||
|
|
@ -145,7 +151,7 @@ async function clickhouseQuery(data: {
|
|||
website_id: websiteId,
|
||||
session_id: sessionId,
|
||||
visit_id: visitId,
|
||||
event_id: uuid(),
|
||||
event_id: eventId,
|
||||
country: country,
|
||||
subdivision1:
|
||||
country && subdivision1
|
||||
|
|
@ -163,6 +169,7 @@ async function clickhouseQuery(data: {
|
|||
page_title: pageTitle?.substring(0, PAGE_TITLE_LENGTH),
|
||||
event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
|
||||
event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
|
||||
tag: tag,
|
||||
created_at: createdAt,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import clickhouse from 'lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import { CLICKHOUSE, getDatabaseType, POSTGRESQL, PRISMA, runQuery } from 'lib/db';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getRevenue(
|
||||
|
|
@ -10,9 +10,7 @@ export async function getRevenue(
|
|||
endDate: Date;
|
||||
unit: string;
|
||||
timezone: string;
|
||||
eventName: string;
|
||||
revenueProperty: string;
|
||||
userProperty: string;
|
||||
currency: string;
|
||||
},
|
||||
]
|
||||
) {
|
||||
|
|
@ -29,61 +27,115 @@ async function relationalQuery(
|
|||
endDate: Date;
|
||||
unit: string;
|
||||
timezone: string;
|
||||
eventName: string;
|
||||
revenueProperty: string;
|
||||
userProperty: string;
|
||||
currency: string;
|
||||
},
|
||||
): Promise<{
|
||||
chart: { time: string; sum: number; avg: number; count: number; uniqueCount: number }[];
|
||||
total: { sum: number; avg: number; count: number; uniqueCount: number };
|
||||
chart: { x: string; t: string; y: number }[];
|
||||
country: { name: string; value: number }[];
|
||||
total: { sum: number; count: number; unique_count: number };
|
||||
table: {
|
||||
currency: string;
|
||||
sum: number;
|
||||
count: number;
|
||||
unique_count: number;
|
||||
}[];
|
||||
}> {
|
||||
const {
|
||||
startDate,
|
||||
endDate,
|
||||
eventName,
|
||||
revenueProperty,
|
||||
userProperty,
|
||||
timezone = 'UTC',
|
||||
unit = 'day',
|
||||
} = criteria;
|
||||
const { startDate, endDate, timezone = 'UTC', unit = 'day', currency } = criteria;
|
||||
const { getDateSQL, rawQuery } = prisma;
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
|
||||
const chartRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
${getDateSQL('website_event.created_at', unit, timezone)} time,
|
||||
sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum,
|
||||
avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg,
|
||||
count(case when data_key = {{revenueProperty}} then 1 else 0 end) count,
|
||||
count(distinct {{userProperty}}) uniqueCount
|
||||
from event_data
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and event_name = {{eventType}}
|
||||
and data_key in ({{revenueProperty}} , {{userProperty}})
|
||||
group by 1
|
||||
we.event_name x,
|
||||
${getDateSQL('ed.created_at', unit, timezone)} t,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) y
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
group by x, t
|
||||
order by t
|
||||
`,
|
||||
{ websiteId, startDate, endDate, eventName, revenueProperty, userProperty },
|
||||
{ websiteId, startDate, endDate, unit, timezone, currency },
|
||||
);
|
||||
|
||||
const countryRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
s.country as name,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) value
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join session s
|
||||
on s.session_id = we.session_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
group by s.country
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
);
|
||||
|
||||
const totalRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
sum(case when data_key = {{revenueProperty}} then number_value else 0 end) sum,
|
||||
avg(case when data_key = {{revenueProperty}} then number_value else 0 end) avg,
|
||||
count(case when data_key = {{revenueProperty}} then 1 else 0 end) count,
|
||||
count(distinct {{userProperty}}) uniqueCount
|
||||
from event_data
|
||||
where website_event.website_id = {{websiteId::uuid}}
|
||||
and website_event.created_at between {{startDate}} and {{endDate}}
|
||||
and event_name = {{eventType}}
|
||||
and data_key in ({{revenueProperty}} , {{userProperty}})
|
||||
group by 1
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) as sum,
|
||||
count(distinct event_id) as count,
|
||||
count(distinct session_id) as unique_count
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join (select website_event_id
|
||||
from event_data
|
||||
where data_key ${like} '%currency%'
|
||||
and string_value = {{currency}}) currency
|
||||
on currency.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
).then(result => result?.[0]);
|
||||
|
||||
const tableRes = await rawQuery(
|
||||
`
|
||||
select
|
||||
c.currency,
|
||||
sum(coalesce(cast(number_value as decimal(10,2)), cast(string_value as decimal(10,2)))) as sum,
|
||||
count(distinct ed.website_event_id) as count,
|
||||
count(distinct we.session_id) as unique_count
|
||||
from event_data ed
|
||||
join website_event we
|
||||
on we.event_id = ed.website_event_id
|
||||
join (select website_event_id, string_value as currency
|
||||
from event_data
|
||||
where data_key ${like} '%currency%') c
|
||||
on c.website_event_id = ed.website_event_id
|
||||
where ed.website_id = {{websiteId::uuid}}
|
||||
and ed.created_at between {{startDate}} and {{endDate}}
|
||||
and ed.data_key ${like} '%revenue%'
|
||||
group by c.currency
|
||||
order by sum desc;
|
||||
`,
|
||||
{ websiteId, startDate, endDate, eventName, revenueProperty, userProperty },
|
||||
{ websiteId, startDate, endDate, unit, timezone, currency },
|
||||
);
|
||||
|
||||
return { chart: chartRes, total: totalRes };
|
||||
return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
|
|
@ -91,80 +143,132 @@ async function clickhouseQuery(
|
|||
criteria: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
eventName: string;
|
||||
revenueProperty: string;
|
||||
userProperty: string;
|
||||
unit: string;
|
||||
timezone: string;
|
||||
currency: string;
|
||||
},
|
||||
): Promise<{
|
||||
chart: { time: string; sum: number; avg: number; count: number; uniqueCount: number }[];
|
||||
total: { sum: number; avg: number; count: number; uniqueCount: number };
|
||||
}> {
|
||||
const {
|
||||
startDate,
|
||||
endDate,
|
||||
eventName,
|
||||
revenueProperty,
|
||||
userProperty = '',
|
||||
timezone = 'UTC',
|
||||
unit = 'day',
|
||||
} = criteria;
|
||||
const { getDateStringSQL, getDateSQL, rawQuery } = clickhouse;
|
||||
|
||||
const chartRes = await rawQuery<{
|
||||
time: string;
|
||||
chart: { x: string; t: string; y: number }[];
|
||||
country: { name: string; value: number }[];
|
||||
total: { sum: number; count: number; unique_count: number };
|
||||
table: {
|
||||
currency: string;
|
||||
sum: number;
|
||||
avg: number;
|
||||
count: number;
|
||||
uniqueCount: number;
|
||||
}>(
|
||||
unique_count: number;
|
||||
}[];
|
||||
}> {
|
||||
const { startDate, endDate, timezone = 'UTC', unit = 'day', currency } = criteria;
|
||||
const { getDateSQL, rawQuery } = clickhouse;
|
||||
|
||||
const chartRes = await rawQuery<
|
||||
{
|
||||
x: string;
|
||||
t: string;
|
||||
y: number;
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
select
|
||||
${getDateStringSQL('g.time', unit)} as time,
|
||||
g.sum as sum,
|
||||
g.avg as avg,
|
||||
g.count as count,
|
||||
g.uniqueCount as uniqueCount
|
||||
from (
|
||||
select
|
||||
${getDateSQL('created_at', unit, timezone)} as time,
|
||||
sumIf(number_value, data_key = {revenueProperty:String}) as sum,
|
||||
avgIf(number_value, data_key = {revenueProperty:String}) as avg,
|
||||
countIf(data_key = {revenueProperty:String}) as count,
|
||||
uniqExactIf(string_value, data_key = {userProperty:String}) as uniqueCount
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_name = {eventName:String}
|
||||
and data_key in ({revenueProperty:String}, {userProperty:String})
|
||||
group by time
|
||||
) as g
|
||||
order by time
|
||||
event_name x,
|
||||
${getDateSQL('created_at', unit, timezone)} t,
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) y
|
||||
from event_data
|
||||
join (select event_id
|
||||
from event_data
|
||||
where positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) currency
|
||||
on currency.event_id = event_data.event_id
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'revenue') > 0
|
||||
group by x, t
|
||||
order by t
|
||||
`,
|
||||
{ websiteId, startDate, endDate, eventName, revenueProperty, userProperty },
|
||||
).then(result => result?.[0]);
|
||||
{ websiteId, startDate, endDate, unit, timezone, currency },
|
||||
);
|
||||
|
||||
const countryRes = await rawQuery<
|
||||
{
|
||||
name: string;
|
||||
value: number;
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
select
|
||||
s.country as name,
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as value
|
||||
from event_data ed
|
||||
join (select event_id
|
||||
from event_data
|
||||
where positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) c
|
||||
on c.event_id = ed.event_id
|
||||
join (select distinct website_id, session_id, country
|
||||
from website_event_stats_hourly
|
||||
where website_id = {websiteId:UUID}) s
|
||||
on ed.website_id = s.website_id
|
||||
and ed.session_id = s.session_id
|
||||
where ed.website_id = {websiteId:UUID}
|
||||
and ed.created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(ed.data_key, 'revenue') > 0
|
||||
group by s.country
|
||||
`,
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
);
|
||||
|
||||
const totalRes = await rawQuery<{
|
||||
sum: number;
|
||||
avg: number;
|
||||
count: number;
|
||||
uniqueCount: number;
|
||||
unique_count: number;
|
||||
}>(
|
||||
`
|
||||
select
|
||||
sumIf(number_value, data_key = {revenueProperty:String}) as sum,
|
||||
avgIf(number_value, data_key = {revenueProperty:String}) as avg,
|
||||
countIf(data_key = {revenueProperty:String}) as count,
|
||||
uniqExactIf(string_value, data_key = {userProperty:String}) as uniqueCount
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and event_name = {eventName:String}
|
||||
and data_key in ({revenueProperty:String}, {userProperty:String})
|
||||
select
|
||||
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as sum,
|
||||
uniqExact(event_id) as count,
|
||||
uniqExact(session_id) as unique_count
|
||||
from event_data
|
||||
join (select event_id
|
||||
from event_data
|
||||
where positionCaseInsensitive(data_key, 'currency') > 0
|
||||
and string_value = {currency:String}) currency
|
||||
on currency.event_id = event_data.event_id
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'revenue') > 0
|
||||
`,
|
||||
{ websiteId, startDate, endDate, eventName, revenueProperty, userProperty },
|
||||
{ websiteId, startDate, endDate, currency },
|
||||
).then(result => result?.[0]);
|
||||
|
||||
const tableRes = await rawQuery<
|
||||
{
|
||||
currency: string;
|
||||
sum: number;
|
||||
avg: number;
|
||||
count: number;
|
||||
unique_count: number;
|
||||
}[]
|
||||
>(
|
||||
`
|
||||
select
|
||||
c.currency,
|
||||
sum(coalesce(toDecimal64(ed.number_value, 2), toDecimal64(ed.string_value, 2))) as sum,
|
||||
uniqExact(ed.event_id) as count,
|
||||
uniqExact(ed.session_id) as unique_count
|
||||
from event_data ed
|
||||
join (select event_id, string_value as currency
|
||||
from event_data
|
||||
where positionCaseInsensitive(data_key, 'currency') > 0) c
|
||||
on c.event_id = ed.event_id
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'revenue') > 0
|
||||
group by c.currency
|
||||
order by sum desc;
|
||||
`,
|
||||
{ websiteId, startDate, endDate, unit, timezone, currency },
|
||||
);
|
||||
|
||||
return { chart: chartRes, total: totalRes };
|
||||
return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
|
||||
}
|
||||
|
|
|
|||
75
src/queries/analytics/reports/getRevenueValues.ts
Normal file
75
src/queries/analytics/reports/getRevenueValues.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, CLICKHOUSE, PRISMA, getDatabaseType, POSTGRESQL } from 'lib/db';
|
||||
|
||||
export async function getRevenueValues(
|
||||
...args: [
|
||||
websiteId: string,
|
||||
criteria: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
]
|
||||
) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
websiteId: string,
|
||||
criteria: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
) {
|
||||
const { rawQuery } = prisma;
|
||||
const { startDate, endDate } = criteria;
|
||||
|
||||
const db = getDatabaseType();
|
||||
const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select distinct string_value as currency
|
||||
from event_data
|
||||
where website_id = {{websiteId::uuid}}
|
||||
and created_at between {{startDate}} and {{endDate}}
|
||||
and data_key ${like} '%currency%'
|
||||
order by currency
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
criteria: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
},
|
||||
) {
|
||||
const { rawQuery } = clickhouse;
|
||||
const { startDate, endDate } = criteria;
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select distinct string_value as currency
|
||||
from event_data
|
||||
where website_id = {websiteId:UUID}
|
||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||
order by currency
|
||||
`,
|
||||
{
|
||||
websiteId,
|
||||
startDate,
|
||||
endDate,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ async function relationalQuery(websiteId: string, sessionId: string) {
|
|||
min(website_event.created_at) as min_time,
|
||||
max(website_event.created_at) as max_time,
|
||||
sum(case when website_event.event_type = 1 then 1 else 0 end) as views,
|
||||
sum(case when website_event.event_type = 1 then 1 else 0 end) as events
|
||||
sum(case when website_event.event_type = 2 then 1 else 0 end) as events
|
||||
from session
|
||||
join website_event on website_event.session_id = session.session_id
|
||||
where session.website_id = {{websiteId::uuid}}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
|
||||
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { timezone = 'utc' } = filters;
|
||||
const { rawQuery } = clickhouse;
|
||||
const { startDate, endDate } = filters;
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { params } = await parseFilters(websiteId, filters);
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -48,7 +48,7 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
|
|||
group by time
|
||||
order by time
|
||||
`,
|
||||
{ websiteId, startDate, endDate },
|
||||
params,
|
||||
).then(formatResults);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
.zh-CN {
|
||||
font-family: '方体', 'PingFang SC', '黑体', 'Heiti SC', 'Microsoft JhengHei UI',
|
||||
'Microsoft JhengHei', Roboto, Noto, 'Noto Sans CJK SC', sans-serif !important;
|
||||
}
|
||||
|
||||
.zh-TW {
|
||||
font-family: '方體', 'PingFang TC', '黑體', 'Heiti TC', 'Microsoft JhengHei UI',
|
||||
'Microsoft JhengHei', Roboto, Noto, 'Noto Sans CJK TC', sans-serif !important;
|
||||
}
|
||||
|
||||
.ja-JP {
|
||||
font-family: '游ゴシック体', YuGothic, 'ヒラギノ丸ゴ', 'Hiragino Sans', 'Yu Gothic UI',
|
||||
'Meiryo UI', 'MS Gothic', Roboto, Noto, 'Noto Sans CJK JP', sans-serif !important;
|
||||
}
|
||||
|
||||
.ko-KR {
|
||||
font-family: 'Nanum Gothic', 'Apple SD Gothic Neo', 'Malgun Gothic', Roboto, Noto,
|
||||
'Noto Sans CJK KR', sans-serif !important;
|
||||
}
|
||||
|
||||
.ar-SA {
|
||||
font-family: 'Geeza Pro', 'Arabic Typesetting', Roboto, Noto, 'Noto Naskh Arabic',
|
||||
'Times New Roman', serif !important;
|
||||
}
|
||||
|
||||
.he-IL {
|
||||
font-family: 'New Peninim MT', 'Arial Hebrew', Gisha, 'Times New Roman', Roboto, Noto,
|
||||
'Noto Sans Hebrew', sans-serif !important;
|
||||
}
|
||||
|
|
@ -3,12 +3,12 @@
|
|||
screen: { width, height },
|
||||
navigator: { language },
|
||||
location,
|
||||
localStorage,
|
||||
document,
|
||||
history,
|
||||
} = window;
|
||||
const { hostname, href } = location;
|
||||
const { currentScript, referrer } = document;
|
||||
const localStorage = href.startsWith('data:') ? undefined : window.localStorage;
|
||||
|
||||
if (!currentScript) return;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue