-
+
+
+ item.currency)}>
+ {item => - {item}
}
+
-
+
{formatMessage(labels.runQuery)}
diff --git a/src/app/(main)/reports/revenue/RevenueReport.module.css b/src/app/(main)/reports/revenue/RevenueReport.module.css
deleted file mode 100644
index aed66b74ef..0000000000
--- a/src/app/(main)/reports/revenue/RevenueReport.module.css
+++ /dev/null
@@ -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;
-}
diff --git a/src/app/(main)/reports/revenue/RevenueReport.tsx b/src/app/(main)/reports/revenue/RevenueReport.tsx
index d67f57c0d8..7b75ebd24a 100644
--- a/src/app/(main)/reports/revenue/RevenueReport.tsx
+++ b/src/app/(main)/reports/revenue/RevenueReport.tsx
@@ -1,15 +1,15 @@
-import RevenueChart from './RevenueChart';
-import RevenueParameters from './RevenueParameters';
+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 ReportBody from '../[reportId]/ReportBody';
-import Money from 'assets/money.svg';
-import { REPORT_TYPES } from 'lib/constants';
+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 }) {
-
+
);
diff --git a/src/app/(main)/reports/revenue/RevenueTable.tsx b/src/app/(main)/reports/revenue/RevenueTable.tsx
new file mode 100644
index 0000000000..0b9fcdc3d5
--- /dev/null
+++ b/src/app/(main)/reports/revenue/RevenueTable.tsx
@@ -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 ;
+ }
+
+ return (
+
+
+ {row => row.currency}
+
+
+ {row => formatLongCurrency(row.sum, row.currency)}
+
+
+ {row => formatLongCurrency(row.count ? row.sum / row.count : 0, row.currency)}
+
+
+ {row => row.count}
+
+
+ {row => row.unique_count}
+
+
+ );
+}
+
+export default RevenueTable;
diff --git a/src/app/(main)/reports/revenue/RevenueView.module.css b/src/app/(main)/reports/revenue/RevenueView.module.css
new file mode 100644
index 0000000000..9b35260e20
--- /dev/null
+++ b/src/app/(main)/reports/revenue/RevenueView.module.css
@@ -0,0 +1,11 @@
+.container {
+ display: grid;
+ gap: 20px;
+ margin-bottom: 40px;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
diff --git a/src/app/(main)/reports/revenue/RevenueView.tsx b/src/app/(main)/reports/revenue/RevenueView.tsx
new file mode 100644
index 0000000000..2d559893f5
--- /dev/null
+++ b/src/app/(main)/reports/revenue/RevenueView.tsx
@@ -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 }) => (
+
+
+ {countryNames[code]}
+
+ ),
+ [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 (
+ <>
+
+
+ {metricData?.map(({ label, value, formatValue }) => {
+ return ;
+ })}
+
+ {data && (
+ <>
+
+
+ ({
+ x: name,
+ y: Number(value),
+ z: (value / data?.total.sum) * 100,
+ }))}
+ renderLabel={renderCountryName}
+ />
+
+
+ >
+ )}
+ {showTable &&
}
+
+ >
+ );
+}
+
+export default RevenueView;
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
index 86a7717fa8..95e718b42f 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteExpandedView.tsx
@@ -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);
diff --git a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx
index 1b21103da9..af5a06d49a 100644
--- a/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx
+++ b/src/app/(main)/websites/[websiteId]/compare/WebsiteCompareTables.tsx
@@ -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 { Grid, GridRow } from 'components/layout/Grid';
+import SideNav from 'components/layout/SideNav';
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 ChangeLabel from 'components/metrics/ChangeLabel';
import CitiesTable from 'components/metrics/CitiesTable';
-import LanguagesTable from 'components/metrics/LanguagesTable';
+import CountriesTable from 'components/metrics/CountriesTable';
+import DevicesTable from 'components/metrics/DevicesTable';
import EventsTable from 'components/metrics/EventsTable';
-import QueryParametersTable from 'components/metrics/QueryParametersTable';
-import { Grid, GridRow } from 'components/layout/Grid';
+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 }) => {
diff --git a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx
index 794a5053d5..760f34f90e 100644
--- a/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx
+++ b/src/app/(main)/websites/[websiteId]/events/EventProperties.tsx
@@ -54,7 +54,7 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
{propertyName && (
)}
diff --git a/src/components/charts/BarChart.tsx b/src/components/charts/BarChart.tsx
index 7624ba1c43..eadc4af738 100644
--- a/src/components/charts/BarChart.tsx
+++ b/src/components/charts/BarChart.tsx
@@ -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 ? : null);
+ setTooltip(
+ opacity ? : null,
+ );
};
return (
diff --git a/src/components/charts/BarChartTooltip.tsx b/src/components/charts/BarChartTooltip.tsx
index fed5af9247..201c6e4c1f 100644
--- a/src/components/charts/BarChartTooltip.tsx
+++ b/src/components/charts/BarChartTooltip.tsx
@@ -1,7 +1,7 @@
+import { useLocale } from 'components/hooks';
import { formatDate } from 'lib/date';
+import { formatLongCurrency, formatLongNumber } from 'lib/format';
import { Flexbox, StatusLight } from 'react-basics';
-import { formatLongNumber } from 'lib/format';
-import { useLocale } from 'components/hooks';
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 }) {
- {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}
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
index 1be99732e0..7c16eeee38 100644
--- a/src/components/hooks/index.ts
+++ b/src/components/hooks/index.ts
@@ -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';
diff --git a/src/components/hooks/queries/useConfig.ts b/src/components/hooks/queries/useConfig.ts
index 72fe095d0f..f6293a44d6 100644
--- a/src/components/hooks/queries/useConfig.ts
+++ b/src/components/hooks/queries/useConfig.ts
@@ -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;
diff --git a/src/components/hooks/queries/useEventDataEvents.ts b/src/components/hooks/queries/useEventDataEvents.ts
index 1d7ccf2da1..5cad991695 100644
--- a/src/components/hooks/queries/useEventDataEvents.ts
+++ b/src/components/hooks/queries/useEventDataEvents.ts
@@ -1,4 +1,4 @@
-import useApi from './useApi';
+import { useApi } from '../useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import { useFilterParams } from '../useFilterParams';
diff --git a/src/components/hooks/queries/useEventDataProperties.ts b/src/components/hooks/queries/useEventDataProperties.ts
index 4eabd0519e..b841a8f405 100644
--- a/src/components/hooks/queries/useEventDataProperties.ts
+++ b/src/components/hooks/queries/useEventDataProperties.ts
@@ -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(
diff --git a/src/components/hooks/queries/useEventDataValues.ts b/src/components/hooks/queries/useEventDataValues.ts
index 61aea58e05..de6783a0a4 100644
--- a/src/components/hooks/queries/useEventDataValues.ts
+++ b/src/components/hooks/queries/useEventDataValues.ts
@@ -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({
- 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),
diff --git a/src/components/hooks/queries/useLogin.ts b/src/components/hooks/queries/useLogin.ts
index c17687b041..a54f38d117 100644
--- a/src/components/hooks/queries/useLogin.ts
+++ b/src/components/hooks/queries/useLogin.ts
@@ -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;
diff --git a/src/components/hooks/queries/useRealtime.ts b/src/components/hooks/queries/useRealtime.ts
index 9c665e4f06..b87f74c468 100644
--- a/src/components/hooks/queries/useRealtime.ts
+++ b/src/components/hooks/queries/useRealtime.ts
@@ -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();
diff --git a/src/components/hooks/queries/useReport.ts b/src/components/hooks/queries/useReport.ts
index 2e63e4e6b8..f7d2a1a0cc 100644
--- a/src/components/hooks/queries/useReport.ts
+++ b/src/components/hooks/queries/useReport.ts
@@ -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';
diff --git a/src/components/hooks/queries/useReports.ts b/src/components/hooks/queries/useReports.ts
index 88e4f02ebf..21db153689 100644
--- a/src/components/hooks/queries/useReports.ts
+++ b/src/components/hooks/queries/useReports.ts
@@ -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 }) {
diff --git a/src/components/hooks/queries/useRevenueValues.ts b/src/components/hooks/queries/useRevenueValues.ts
new file mode 100644
index 0000000000..007ca3c53b
--- /dev/null
+++ b/src/components/hooks/queries/useRevenueValues.ts
@@ -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;
diff --git a/src/components/hooks/queries/useSessionActivity.ts b/src/components/hooks/queries/useSessionActivity.ts
index 16c139ab38..1c9c8f57d5 100644
--- a/src/components/hooks/queries/useSessionActivity.ts
+++ b/src/components/hooks/queries/useSessionActivity.ts
@@ -1,4 +1,4 @@
-import { useApi } from './useApi';
+import { useApi } from '../useApi';
export function useSessionActivity(
websiteId: string,
diff --git a/src/components/hooks/queries/useSessionData.ts b/src/components/hooks/queries/useSessionData.ts
index 14e046d1d2..521ba7d562 100644
--- a/src/components/hooks/queries/useSessionData.ts
+++ b/src/components/hooks/queries/useSessionData.ts
@@ -1,4 +1,4 @@
-import { useApi } from './useApi';
+import { useApi } from '../useApi';
export function useSessionData(websiteId: string, sessionId: string) {
const { get, useQuery } = useApi();
diff --git a/src/components/hooks/queries/useSessionDataProperties.ts b/src/components/hooks/queries/useSessionDataProperties.ts
index 459dccd6a8..45590b3902 100644
--- a/src/components/hooks/queries/useSessionDataProperties.ts
+++ b/src/components/hooks/queries/useSessionDataProperties.ts
@@ -1,4 +1,4 @@
-import useApi from './useApi';
+import { useApi } from '../useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import { useFilterParams } from '../useFilterParams';
diff --git a/src/components/hooks/queries/useSessionDataValues.ts b/src/components/hooks/queries/useSessionDataValues.ts
index ce9a67f3ec..85529fc09f 100644
--- a/src/components/hooks/queries/useSessionDataValues.ts
+++ b/src/components/hooks/queries/useSessionDataValues.ts
@@ -1,4 +1,4 @@
-import useApi from './useApi';
+import { useApi } from '../useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import { useFilterParams } from '../useFilterParams';
diff --git a/src/components/hooks/queries/useShareToken.ts b/src/components/hooks/queries/useShareToken.ts
index 189657be42..f9db7dbfa4 100644
--- a/src/components/hooks/queries/useShareToken.ts
+++ b/src/components/hooks/queries/useShareToken.ts
@@ -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;
diff --git a/src/components/hooks/queries/useTeam.ts b/src/components/hooks/queries/useTeam.ts
index e348531c6a..d0ce7499ce 100644
--- a/src/components/hooks/queries/useTeam.ts
+++ b/src/components/hooks/queries/useTeam.ts
@@ -1,4 +1,4 @@
-import useApi from './useApi';
+import { useApi } from '../useApi';
export function useTeam(teamId: string) {
const { get, useQuery } = useApi();
diff --git a/src/components/hooks/queries/useTeamMembers.ts b/src/components/hooks/queries/useTeamMembers.ts
index a3e8bcd235..b6353afc19 100644
--- a/src/components/hooks/queries/useTeamMembers.ts
+++ b/src/components/hooks/queries/useTeamMembers.ts
@@ -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) {
diff --git a/src/components/hooks/queries/useTeamWebsites.ts b/src/components/hooks/queries/useTeamWebsites.ts
index fac82d13bf..5606407ebd 100644
--- a/src/components/hooks/queries/useTeamWebsites.ts
+++ b/src/components/hooks/queries/useTeamWebsites.ts
@@ -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) {
diff --git a/src/components/hooks/queries/useTeams.ts b/src/components/hooks/queries/useTeams.ts
index e1f7790a54..e5197c97ba 100644
--- a/src/components/hooks/queries/useTeams.ts
+++ b/src/components/hooks/queries/useTeams.ts
@@ -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) {
diff --git a/src/components/hooks/queries/useUser.ts b/src/components/hooks/queries/useUser.ts
index 61c22ecdf3..8541a2207e 100644
--- a/src/components/hooks/queries/useUser.ts
+++ b/src/components/hooks/queries/useUser.ts
@@ -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();
diff --git a/src/components/hooks/queries/useUsers.ts b/src/components/hooks/queries/useUsers.ts
index 519fb89916..3d70d262d1 100644
--- a/src/components/hooks/queries/useUsers.ts
+++ b/src/components/hooks/queries/useUsers.ts
@@ -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() {
diff --git a/src/components/hooks/queries/useWebsite.ts b/src/components/hooks/queries/useWebsite.ts
index 4121f97b2b..9151b55d73 100644
--- a/src/components/hooks/queries/useWebsite.ts
+++ b/src/components/hooks/queries/useWebsite.ts
@@ -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();
diff --git a/src/components/hooks/queries/useWebsiteEvents.ts b/src/components/hooks/queries/useWebsiteEvents.ts
index 5d9bd19660..2a47c3eb26 100644
--- a/src/components/hooks/queries/useWebsiteEvents.ts
+++ b/src/components/hooks/queries/useWebsiteEvents.ts
@@ -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,
diff --git a/src/components/hooks/queries/useWebsiteEventsSeries.ts b/src/components/hooks/queries/useWebsiteEventsSeries.ts
index 88b493759a..91c50ffffd 100644
--- a/src/components/hooks/queries/useWebsiteEventsSeries.ts
+++ b/src/components/hooks/queries/useWebsiteEventsSeries.ts
@@ -1,4 +1,4 @@
-import useApi from './useApi';
+import { useApi } from '../useApi';
import { UseQueryOptions } from '@tanstack/react-query';
import { useFilterParams } from '../useFilterParams';
diff --git a/src/components/hooks/queries/useWebsiteMetrics.ts b/src/components/hooks/queries/useWebsiteMetrics.ts
index 184fd4d44c..8a3976e103 100644
--- a/src/components/hooks/queries/useWebsiteMetrics.ts
+++ b/src/components/hooks/queries/useWebsiteMetrics.ts
@@ -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(
diff --git a/src/components/hooks/queries/useWebsitePageviews.ts b/src/components/hooks/queries/useWebsitePageviews.ts
index c9260bcbfb..42fb527e34 100644
--- a/src/components/hooks/queries/useWebsitePageviews.ts
+++ b/src/components/hooks/queries/useWebsitePageviews.ts
@@ -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(
diff --git a/src/components/hooks/queries/useWebsiteSession.ts b/src/components/hooks/queries/useWebsiteSession.ts
index 64c7be58bf..93e9057c67 100644
--- a/src/components/hooks/queries/useWebsiteSession.ts
+++ b/src/components/hooks/queries/useWebsiteSession.ts
@@ -1,4 +1,4 @@
-import { useApi } from './useApi';
+import { useApi } from '../useApi';
export function useWebsiteSession(websiteId: string, sessionId: string) {
const { get, useQuery } = useApi();
diff --git a/src/components/hooks/queries/useWebsiteSessionStats.ts b/src/components/hooks/queries/useWebsiteSessionStats.ts
index 7671b2ebee..5c02cfdced 100644
--- a/src/components/hooks/queries/useWebsiteSessionStats.ts
+++ b/src/components/hooks/queries/useWebsiteSessionStats.ts
@@ -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 }) {
diff --git a/src/components/hooks/queries/useWebsiteSessions.ts b/src/components/hooks/queries/useWebsiteSessions.ts
index ce65512c2f..ad7bb616d9 100644
--- a/src/components/hooks/queries/useWebsiteSessions.ts
+++ b/src/components/hooks/queries/useWebsiteSessions.ts
@@ -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';
diff --git a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts
index 5df543f507..c4e83f9871 100644
--- a/src/components/hooks/queries/useWebsiteSessionsWeekly.ts
+++ b/src/components/hooks/queries/useWebsiteSessionsWeekly.ts
@@ -1,4 +1,4 @@
-import { useApi } from './useApi';
+import { useApi } from '../useApi';
import useModified from '../useModified';
import { useFilterParams } from 'components/hooks/useFilterParams';
diff --git a/src/components/hooks/queries/useWebsiteStats.ts b/src/components/hooks/queries/useWebsiteStats.ts
index b24399fa91..6d42009ee6 100644
--- a/src/components/hooks/queries/useWebsiteStats.ts
+++ b/src/components/hooks/queries/useWebsiteStats.ts
@@ -1,4 +1,4 @@
-import { useApi } from './useApi';
+import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams';
export function useWebsiteStats(
diff --git a/src/components/hooks/queries/useWebsiteValues.ts b/src/components/hooks/queries/useWebsiteValues.ts
index c5358df233..68e950f266 100644
--- a/src/components/hooks/queries/useWebsiteValues.ts
+++ b/src/components/hooks/queries/useWebsiteValues.ts
@@ -1,4 +1,4 @@
-import { useApi } from './useApi';
+import { useApi } from '../useApi';
export function useWebsiteValues({
websiteId,
diff --git a/src/components/hooks/queries/useWebsites.ts b/src/components/hooks/queries/useWebsites.ts
index d6fe00df79..7a5004d75e 100644
--- a/src/components/hooks/queries/useWebsites.ts
+++ b/src/components/hooks/queries/useWebsites.ts
@@ -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';
diff --git a/src/components/hooks/queries/useApi.ts b/src/components/hooks/useApi.ts
similarity index 100%
rename from src/components/hooks/queries/useApi.ts
rename to src/components/hooks/useApi.ts
diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts
index 248070f47f..23cb6e70e0 100644
--- a/src/components/hooks/useDateRange.ts
+++ b/src/components/hooks/useDateRange.ts
@@ -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;
diff --git a/src/components/hooks/useFields.ts b/src/components/hooks/useFields.ts
index e6fc54b3dc..859ca1ce45 100644
--- a/src/components/hooks/useFields.ts
+++ b/src/components/hooks/useFields.ts
@@ -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 };
diff --git a/src/components/hooks/useFilterParams.ts b/src/components/hooks/useFilterParams.ts
index 525f349298..55deed1415 100644
--- a/src/components/hooks/useFilterParams.ts
+++ b/src/components/hooks/useFilterParams.ts
@@ -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,
};
}
diff --git a/src/components/hooks/queries/usePagedQuery.ts b/src/components/hooks/usePagedQuery.ts
similarity index 94%
rename from src/components/hooks/queries/usePagedQuery.ts
rename to src/components/hooks/usePagedQuery.ts
index 253f809254..19471432d7 100644
--- a/src/components/hooks/queries/usePagedQuery.ts
+++ b/src/components/hooks/usePagedQuery.ts
@@ -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({
queryKey,
diff --git a/src/components/messages.ts b/src/components/messages.ts
index f711a4f6cf..688dd11d56 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -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' },
@@ -153,6 +152,7 @@ export const labels = defineMessages({
regions: { id: 'label.regions', defaultMessage: 'Regions' },
reports: { id: 'label.reports', defaultMessage: 'Reports' },
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
+ sessionData: { id: 'label.session-data', defaultMessage: 'Session data' },
funnel: { id: 'label.funnel', defaultMessage: 'Funnel' },
funnelDescription: {
id: 'label.funnel-description',
@@ -161,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' },
@@ -220,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}' },
diff --git a/src/components/metrics/HostsTable.tsx b/src/components/metrics/HostsTable.tsx
index d3a0f3bf39..45147eac58 100644
--- a/src/components/metrics/HostsTable.tsx
+++ b/src/components/metrics/HostsTable.tsx
@@ -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}
/>
>
diff --git a/src/components/metrics/TagsTable.tsx b/src/components/metrics/TagsTable.tsx
new file mode 100644
index 0000000000..a1130bb4fa
--- /dev/null
+++ b/src/components/metrics/TagsTable.tsx
@@ -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 (
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+ >
+ );
+}
+
+export default TagsTable;
diff --git a/src/lang/ko-KR.json b/src/lang/ko-KR.json
index 07494d93f9..933e2883b6 100644
--- a/src/lang/ko-KR.json
+++ b/src/lang/ko-KR.json
@@ -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": "사용자 이름 또는 비밀번호를 잘못 입력했습니다.",
diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts
index 5502b5babf..b588ec84e5 100644
--- a/src/lib/clickhouse.ts
+++ b/src/lib/clickhouse.ts
@@ -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),
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 5d3a9776b5..7f8acf886a 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -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 = {
diff --git a/src/lib/format.ts b/src/lib/format.ts
index a662a9eb49..e497464a65 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -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);
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 7fabbfc859..615882ef2d 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -179,6 +179,7 @@ export interface QueryFilters {
language?: string;
event?: string;
search?: string;
+ tag?: string;
}
export interface QueryOptions {
diff --git a/src/pages/api/reports/revenue.ts b/src/pages/api/reports/revenue.ts
index ac4dc6b364..d23ce55aec 100644
--- a/src/pages/api/reports/revenue.ts
+++ b/src/pages/api/reports/revenue.ts
@@ -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,
+ req: NextApiRequestQueryBody,
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);
diff --git a/src/pages/api/send.ts b/src/pages/api/send.ts
index 23640de9f0..fb4e90c744 100644
--- a/src/pages/api/send.ts
+++ b/src/pages/api/send.ts
@@ -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) {
diff --git a/src/pages/api/websites/[websiteId]/event-data/fields.ts b/src/pages/api/websites/[websiteId]/event-data/fields.ts
new file mode 100644
index 0000000000..c5075c5eeb
--- /dev/null
+++ b/src/pages/api/websites/[websiteId]/event-data/fields.ts
@@ -0,0 +1,51 @@
+import { canViewWebsite } from 'lib/auth';
+import { useAuth, useCors, useValidate } from 'lib/middleware';
+import { NextApiRequestQueryBody } from 'lib/types';
+import { NextApiResponse } from 'next';
+import { methodNotAllowed, ok, unauthorized } from 'next-basics';
+import { getEventDataFields } from 'queries';
+
+import * as yup from 'yup';
+
+export interface EventDataFieldsRequestQuery {
+ websiteId: string;
+ startAt: string;
+ endAt: string;
+}
+
+const schema = {
+ GET: yup.object().shape({
+ websiteId: yup.string().uuid().required(),
+ startAt: yup.number().integer().required(),
+ endAt: yup.number().integer().min(yup.ref('startAt')).required(),
+ }),
+};
+
+export default async (
+ req: NextApiRequestQueryBody,
+ res: NextApiResponse,
+) => {
+ await useCors(req, res);
+ await useAuth(req, res);
+ await useValidate(schema, req, res);
+
+ if (req.method === 'GET') {
+ const { websiteId, startAt, endAt } = req.query;
+
+ if (!(await canViewWebsite(req.auth, websiteId))) {
+ return unauthorized(res);
+ }
+
+ const startDate = new Date(+startAt);
+ const endDate = new Date(+endAt);
+
+ const data = await getEventDataFields(websiteId, {
+ startDate,
+ endDate,
+ });
+
+ return ok(res, data);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/src/pages/api/websites/[websiteId]/events/stats.ts b/src/pages/api/websites/[websiteId]/event-data/stats.ts
similarity index 100%
rename from src/pages/api/websites/[websiteId]/events/stats.ts
rename to src/pages/api/websites/[websiteId]/event-data/stats.ts
diff --git a/src/pages/api/websites/[websiteId]/events/series.ts b/src/pages/api/websites/[websiteId]/events/series.ts
index 08cade1208..6d67a26441 100644
--- a/src/pages/api/websites/[websiteId]/events/series.ts
+++ b/src/pages/api/websites/[websiteId]/events/series.ts
@@ -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(),
}),
};
diff --git a/src/pages/api/websites/[websiteId]/metrics.ts b/src/pages/api/websites/[websiteId]/metrics.ts
index 3dac217b21..1996a61a1b 100644
--- a/src/pages/api/websites/[websiteId]/metrics.ts
+++ b/src/pages/api/websites/[websiteId]/metrics.ts
@@ -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(),
}),
};
diff --git a/src/pages/api/websites/[websiteId]/pageviews.ts b/src/pages/api/websites/[websiteId]/pageviews.ts
index badb8a47da..c3b6b7979d 100644
--- a/src/pages/api/websites/[websiteId]/pageviews.ts
+++ b/src/pages/api/websites/[websiteId]/pageviews.ts
@@ -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(),
}),
};
diff --git a/src/pages/api/websites/[websiteId]/sessions/stats.ts b/src/pages/api/websites/[websiteId]/sessions/stats.ts
index a522bd6bf5..fe92ce6fb6 100644
--- a/src/pages/api/websites/[websiteId]/sessions/stats.ts
+++ b/src/pages/api/websites/[websiteId]/sessions/stats.ts
@@ -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(),
}),
};
diff --git a/src/pages/api/websites/[websiteId]/stats.ts b/src/pages/api/websites/[websiteId]/stats.ts
index 9ca84c7440..dfc9198dfd 100644
--- a/src/pages/api/websites/[websiteId]/stats.ts
+++ b/src/pages/api/websites/[websiteId]/stats.ts
@@ -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(),
}),
};
diff --git a/src/queries/analytics/events/getEventDataFields.ts b/src/queries/analytics/events/getEventDataFields.ts
new file mode 100644
index 0000000000..05fee07280
--- /dev/null
+++ b/src/queries/analytics/events/getEventDataFields.ts
@@ -0,0 +1,69 @@
+import prisma from 'lib/prisma';
+import clickhouse from 'lib/clickhouse';
+import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
+import { QueryFilters, WebsiteEventData } from 'lib/types';
+
+export async function getEventDataFields(
+ ...args: [websiteId: string, filters: QueryFilters]
+): Promise {
+ return runQuery({
+ [PRISMA]: () => relationalQuery(...args),
+ [CLICKHOUSE]: () => clickhouseQuery(...args),
+ });
+}
+
+async function relationalQuery(websiteId: string, filters: QueryFilters) {
+ const { rawQuery, parseFilters, getDateSQL } = prisma;
+ const { filterQuery, params } = await parseFilters(websiteId, filters);
+
+ return rawQuery(
+ `
+ select
+ data_key as "propertyName",
+ data_type as "dataType",
+ case
+ when data_type = 2 then replace(string_value, '.0000', '')
+ when data_type = 4 then ${getDateSQL('date_value', 'hour')}
+ else string_value
+ end as "value",
+ count(*) as "total"
+ from event_data
+ join website_event on website_event.event_id = event_data.website_event_id
+ where event_data.website_id = {{websiteId::uuid}}
+ and event_data.created_at between {{startDate}} and {{endDate}}
+ ${filterQuery}
+ group by data_key, data_type, value
+ order by 2 desc
+ limit 100
+ `,
+ params,
+ );
+}
+
+async function clickhouseQuery(
+ websiteId: string,
+ filters: QueryFilters,
+): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
+ const { rawQuery, parseFilters } = clickhouse;
+ const { filterQuery, params } = await parseFilters(websiteId, filters);
+
+ return rawQuery(
+ `
+ select
+ data_key as propertyName,
+ data_type as dataType,
+ multiIf(data_type = 2, replaceAll(string_value, '.0000', ''),
+ data_type = 4, toString(date_trunc('hour', date_value)),
+ string_value) as "value",
+ count(*) as "total"
+ from event_data
+ where website_id = {websiteId:UUID}
+ and created_at between {startDate:DateTime64} and {endDate:DateTime64}
+ ${filterQuery}
+ group by data_key, data_type, value
+ order by 2 desc
+ limit 100
+ `,
+ params,
+ );
+}
diff --git a/src/queries/analytics/events/getEventDataValues.ts b/src/queries/analytics/events/getEventDataValues.ts
index 07e423075b..631018248b 100644
--- a/src/queries/analytics/events/getEventDataValues.ts
+++ b/src/queries/analytics/events/getEventDataValues.ts
@@ -49,7 +49,7 @@ async function relationalQuery(
async function clickhouseQuery(
websiteId: string,
filters: QueryFilters & { eventName?: string; propertyName?: string },
-): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
+): Promise<{ value: string; total: number }[]> {
const { rawQuery, parseFilters } = clickhouse;
const { filterQuery, params } = await parseFilters(websiteId, filters);
@@ -66,7 +66,7 @@ async function clickhouseQuery(
and data_key = {propertyName:String}
and event_name = {eventName:String}
${filterQuery}
- group by event_name, value
+ group by value
order by 2 desc
limit 100
`,
diff --git a/src/queries/analytics/events/saveEvent.ts b/src/queries/analytics/events/saveEvent.ts
index 6c0f917baa..6a3391d674 100644
--- a/src/queries/analytics/events/saveEvent.ts
+++ b/src/queries/analytics/events/saveEvent.ts
@@ -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;
@@ -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,
};
diff --git a/src/queries/analytics/reports/getRevenue.ts b/src/queries/analytics/reports/getRevenue.ts
index 502505f44f..caef94694e 100644
--- a/src/queries/analytics/reports/getRevenue.ts
+++ b/src/queries/analytics/reports/getRevenue.ts
@@ -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 = 'USD') 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 = 'USD') 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, 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, eventName, revenueProperty, userProperty },
+ { websiteId, startDate, endDate, unit, timezone, currency },
);
- return { chart: chartRes, total: totalRes };
+ return { chart: chartRes, country: countryRes, total: totalRes, table: tableRes };
}
diff --git a/src/queries/analytics/reports/getRevenueValues.ts b/src/queries/analytics/reports/getRevenueValues.ts
new file mode 100644
index 0000000000..4dcc4a2208
--- /dev/null
+++ b/src/queries/analytics/reports/getRevenueValues.ts
@@ -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,
+ },
+ );
+}
diff --git a/src/queries/analytics/sessions/getWebsiteSession.ts b/src/queries/analytics/sessions/getWebsiteSession.ts
index f9b9f39a08..2c16741eee 100644
--- a/src/queries/analytics/sessions/getWebsiteSession.ts
+++ b/src/queries/analytics/sessions/getWebsiteSession.ts
@@ -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}}
diff --git a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts
index 153c9bb385..48d4f7a9fe 100644
--- a/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts
+++ b/src/queries/analytics/sessions/getWebsiteSessionsWeekly.ts
@@ -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);
}
diff --git a/src/queries/index.ts b/src/queries/index.ts
index a2697ced58..8c7e564abc 100644
--- a/src/queries/index.ts
+++ b/src/queries/index.ts
@@ -4,6 +4,7 @@ export * from 'queries/prisma/teamUser';
export * from 'queries/prisma/user';
export * from 'queries/prisma/website';
export * from './analytics/events/getEventDataEvents';
+export * from './analytics/events/getEventDataFields';
export * from './analytics/events/getEventDataProperties';
export * from './analytics/events/getEventDataValues';
export * from './analytics/events/getEventDataStats';
diff --git a/src/queries/prisma/user.ts b/src/queries/prisma/user.ts
index 9b471787e4..0c8e3520de 100644
--- a/src/queries/prisma/user.ts
+++ b/src/queries/prisma/user.ts
@@ -171,6 +171,9 @@ export async function deleteUser(
client.eventData.deleteMany({
where: { websiteId: { in: websiteIds } },
}),
+ client.sessionData.deleteMany({
+ where: { websiteId: { in: websiteIds } },
+ }),
client.websiteEvent.deleteMany({
where: { websiteId: { in: websiteIds } },
}),
diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts
index 0814a137b6..c24cdd0d23 100644
--- a/src/queries/prisma/website.ts
+++ b/src/queries/prisma/website.ts
@@ -164,6 +164,9 @@ export async function resetWebsite(
client.eventData.deleteMany({
where: { websiteId },
}),
+ client.sessionData.deleteMany({
+ where: { websiteId },
+ }),
client.websiteEvent.deleteMany({
where: { websiteId },
}),
@@ -195,6 +198,9 @@ export async function deleteWebsite(
client.eventData.deleteMany({
where: { websiteId },
}),
+ client.sessionData.deleteMany({
+ where: { websiteId },
+ }),
client.websiteEvent.deleteMany({
where: { websiteId },
}),
diff --git a/src/tracker/index.js b/src/tracker/index.js
index 707594cf40..16f714e45a 100644
--- a/src/tracker/index.js
+++ b/src/tracker/index.js
@@ -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;
diff --git a/yarn.lock b/yarn.lock
index 910f0c69f2..a53045fef1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2197,10 +2197,10 @@
resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-5.6.0.tgz#970f96b11bee4fe115fad8e3e4f3c6121f97a370"
integrity sha512-PBrsd/GJZ9MN8BdyIoleTkY22lAUMfcRxrbb8wgxGzXtTW0RU0GW2mc99ISB6zOwWMZ11rSjeN0GS6znnukvww==
-"@next/env@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.5.tgz#1d9328ab828711d3517d0a1d505acb55e5ef7ad0"
- integrity sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==
+"@next/env@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.10.tgz#1d3178340028ced2d679f84140877db4f420333c"
+ integrity sha512-dZIu93Bf5LUtluBXIv4woQw2cZVZ2DJTjax5/5DOs3lzEOeKLy7GxRSr4caK9/SCPdaW6bCgpye6+n4Dh9oJPw==
"@next/eslint-plugin-next@14.2.5":
version "14.2.5"
@@ -2209,50 +2209,50 @@
dependencies:
glob "10.3.10"
-"@next/swc-darwin-arm64@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz#d0a160cf78c18731c51cc0bff131c706b3e9bb05"
- integrity sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==
-
-"@next/swc-darwin-x64@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz#eb832a992407f6e6352eed05a073379f1ce0589c"
- integrity sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==
-
-"@next/swc-linux-arm64-gnu@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz#098fdab57a4664969bc905f5801ef5a89582c689"
- integrity sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==
-
-"@next/swc-linux-arm64-musl@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz#243a1cc1087fb75481726dd289c7b219fa01f2b5"
- integrity sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==
-
-"@next/swc-linux-x64-gnu@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz#b8a2e436387ee4a52aa9719b718992e0330c4953"
- integrity sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==
-
-"@next/swc-linux-x64-musl@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz#cb8a9adad5fb8df86112cfbd363aab5c6d32757b"
- integrity sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==
-
-"@next/swc-win32-arm64-msvc@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz#81f996c1c38ea0900d4e7719cc8814be8a835da0"
- integrity sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==
-
-"@next/swc-win32-ia32-msvc@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz#f61c74ce823e10b2bc150e648fc192a7056422e0"
- integrity sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==
-
-"@next/swc-win32-x64-msvc@14.2.5":
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz#ed199a920efb510cfe941cd75ed38a7be21e756f"
- integrity sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==
+"@next/swc-darwin-arm64@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.10.tgz#49d10ca4086fbd59ee68e204f75d7136eda2aa80"
+ integrity sha512-V3z10NV+cvMAfxQUMhKgfQnPbjw+Ew3cnr64b0lr8MDiBJs3eLnM6RpGC46nhfMZsiXgQngCJKWGTC/yDcgrDQ==
+
+"@next/swc-darwin-x64@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.10.tgz#0ebeae3afb8eac433882b79543295ab83624a1a8"
+ integrity sha512-Y0TC+FXbFUQ2MQgimJ/7Ina2mXIKhE7F+GUe1SgnzRmwFY3hX2z8nyVCxE82I2RicspdkZnSWMn4oTjIKz4uzA==
+
+"@next/swc-linux-arm64-gnu@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.10.tgz#7e602916d2fb55a3c532f74bed926a0137c16f20"
+ integrity sha512-ZfQ7yOy5zyskSj9rFpa0Yd7gkrBnJTkYVSya95hX3zeBG9E55Z6OTNPn1j2BTFWvOVVj65C3T+qsjOyVI9DQpA==
+
+"@next/swc-linux-arm64-musl@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.10.tgz#6b143f628ccee490b527562e934f8de578d4be47"
+ integrity sha512-n2i5o3y2jpBfXFRxDREr342BGIQCJbdAUi/K4q6Env3aSx8erM9VuKXHw5KNROK9ejFSPf0LhoSkU/ZiNdacpQ==
+
+"@next/swc-linux-x64-gnu@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.10.tgz#086f2f16a0678890a1eb46518c4dda381b046082"
+ integrity sha512-GXvajAWh2woTT0GKEDlkVhFNxhJS/XdDmrVHrPOA83pLzlGPQnixqxD8u3bBB9oATBKB//5e4vpACnx5Vaxdqg==
+
+"@next/swc-linux-x64-musl@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.10.tgz#1befef10ed8dbcc5047b5d637a25ae3c30a0bfc3"
+ integrity sha512-opFFN5B0SnO+HTz4Wq4HaylXGFV+iHrVxd3YvREUX9K+xfc4ePbRrxqOuPOFjtSuiVouwe6uLeDtabjEIbkmDA==
+
+"@next/swc-win32-arm64-msvc@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.10.tgz#731f52c3ae3c56a26cf21d474b11ae1529531209"
+ integrity sha512-9NUzZuR8WiXTvv+EiU/MXdcQ1XUvFixbLIMNQiVHuzs7ZIFrJDLJDaOF1KaqttoTujpcxljM/RNAOmw1GhPPQQ==
+
+"@next/swc-win32-ia32-msvc@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.10.tgz#32723ef7f04e25be12af357cc72ddfdd42fd1041"
+ integrity sha512-fr3aEbSd1GeW3YUMBkWAu4hcdjZ6g4NBl1uku4gAn661tcxd1bHs1THWYzdsbTRLcCKLjrDZlNp6j2HTfrw+Bg==
+
+"@next/swc-win32-x64-msvc@14.2.10":
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.10.tgz#ee1d036cb5ec871816f96baee7991035bb242455"
+ integrity sha512-UjeVoRGKNL2zfbcQ6fscmgjBAS/inHBh63mjIlfPg/NG8Yn2ztqylXt5qilYb6hoHIwaU2ogHknHWWmahJjgZQ==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -7870,12 +7870,12 @@ next-basics@^0.39.0:
jsonwebtoken "^9.0.0"
pure-rand "^6.0.2"
-next@14.2.5:
- version "14.2.5"
- resolved "https://registry.yarnpkg.com/next/-/next-14.2.5.tgz#afe4022bb0b752962e2205836587a289270efbea"
- integrity sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==
+next@14.2.10:
+ version "14.2.10"
+ resolved "https://registry.yarnpkg.com/next/-/next-14.2.10.tgz#331981a4fecb1ae8af1817d4db98fc9687ee1cb6"
+ integrity sha512-sDDExXnh33cY3RkS9JuFEKaS4HmlWmDKP1VJioucCG6z5KuA008DPsDZOzi8UfqEk3Ii+2NCQSJrfbEWtZZfww==
dependencies:
- "@next/env" "14.2.5"
+ "@next/env" "14.2.10"
"@swc/helpers" "0.5.5"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
@@ -7883,15 +7883,15 @@ next@14.2.5:
postcss "8.4.31"
styled-jsx "5.1.1"
optionalDependencies:
- "@next/swc-darwin-arm64" "14.2.5"
- "@next/swc-darwin-x64" "14.2.5"
- "@next/swc-linux-arm64-gnu" "14.2.5"
- "@next/swc-linux-arm64-musl" "14.2.5"
- "@next/swc-linux-x64-gnu" "14.2.5"
- "@next/swc-linux-x64-musl" "14.2.5"
- "@next/swc-win32-arm64-msvc" "14.2.5"
- "@next/swc-win32-ia32-msvc" "14.2.5"
- "@next/swc-win32-x64-msvc" "14.2.5"
+ "@next/swc-darwin-arm64" "14.2.10"
+ "@next/swc-darwin-x64" "14.2.10"
+ "@next/swc-linux-arm64-gnu" "14.2.10"
+ "@next/swc-linux-arm64-musl" "14.2.10"
+ "@next/swc-linux-x64-gnu" "14.2.10"
+ "@next/swc-linux-x64-musl" "14.2.10"
+ "@next/swc-win32-arm64-msvc" "14.2.10"
+ "@next/swc-win32-ia32-msvc" "14.2.10"
+ "@next/swc-win32-x64-msvc" "14.2.10"
nice-try@^1.0.4:
version "1.0.5"
@@ -8860,7 +8860,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
-postcss@8.4.31, postcss@^8.1.10:
+postcss@8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
@@ -8869,25 +8869,7 @@ postcss@8.4.31, postcss@^8.1.10:
picocolors "^1.0.0"
source-map-js "^1.0.2"
-postcss@^8.4.21:
- version "8.4.32"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
- integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
- dependencies:
- nanoid "^3.3.7"
- picocolors "^1.0.0"
- source-map-js "^1.0.2"
-
-postcss@^8.4.28:
- version "8.4.35"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7"
- integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==
- dependencies:
- nanoid "^3.3.7"
- picocolors "^1.0.0"
- source-map-js "^1.0.2"
-
-postcss@^8.4.31:
+postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.28, postcss@^8.4.31:
version "8.4.41"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681"
integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==