mirror of
https://github.com/umami-software/umami.git
synced 2026-02-06 13:47:15 +01:00
Merge dev.
This commit is contained in:
commit
be1b2fc272
88 changed files with 4120 additions and 21010 deletions
|
|
@ -1,6 +0,0 @@
|
|||
'use client';
|
||||
import { TestConsole } from './TestConsole';
|
||||
|
||||
export function ConsolePage({ websiteId }) {
|
||||
return <TestConsole websiteId={websiteId} />;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Metadata } from 'next';
|
||||
import { ConsolePage } from '../ConsolePage';
|
||||
import { TestConsole } from '../TestConsole';
|
||||
|
||||
async function getEnabled() {
|
||||
return !!process.env.ENABLE_TEST_CONSOLE;
|
||||
|
|
@ -14,7 +14,7 @@ export default async function ({ params }: { params: Promise<{ websiteId: string
|
|||
return null;
|
||||
}
|
||||
|
||||
return <ConsolePage websiteId={websiteId?.[0]} />;
|
||||
return <TestConsole websiteId={websiteId?.[0]} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { JourneyReport } from '../journey/JourneyReport';
|
|||
import { RetentionReport } from '../retention/RetentionReport';
|
||||
import { UTMReport } from '../utm/UTMReport';
|
||||
import { RevenueReport } from '../revenue/RevenueReport';
|
||||
import AttributionReport from '../attribution/AttributionReport';
|
||||
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
|
|
@ -18,6 +19,7 @@ const reports = {
|
|||
goals: GoalsReport,
|
||||
journey: JourneyReport,
|
||||
revenue: RevenueReport,
|
||||
attribution: AttributionReport,
|
||||
};
|
||||
|
||||
export function ReportPage({ reportId }: { reportId: string }) {
|
||||
|
|
|
|||
174
src/app/(main)/reports/attribution/AttributionParameters.tsx
Normal file
174
src/app/(main)/reports/attribution/AttributionParameters.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { useMessages } from '@/components/hooks';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { useContext, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormField,
|
||||
Icon,
|
||||
ListItem,
|
||||
Popover,
|
||||
DialogTrigger,
|
||||
Toggle,
|
||||
FormSubmitButton,
|
||||
} from '@umami/react-zen';
|
||||
import { BaseParameters } from '../[reportId]/BaseParameters';
|
||||
import { ParameterList } from '../[reportId]/ParameterList';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
import { FunnelStepAddForm } from '../funnel/FunnelStepAddForm';
|
||||
import { AttributionStepAddForm } from './AttributionStepAddForm';
|
||||
import { useRevenueValuesQuery } from '@/components/hooks/queries/useRevenueValuesQuery';
|
||||
|
||||
export function AttributionParameters() {
|
||||
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { id, parameters } = report || {};
|
||||
const { websiteId, dateRange, steps } = parameters || {};
|
||||
const queryEnabled = websiteId && dateRange && steps.length > 0;
|
||||
const [model, setModel] = useState('');
|
||||
const [revenueMode, setRevenueMode] = useState(false);
|
||||
|
||||
const { data: currencyValues = [] } = useRevenueValuesQuery(
|
||||
websiteId,
|
||||
dateRange?.startDate,
|
||||
dateRange?.endDate,
|
||||
);
|
||||
|
||||
const handleSubmit = (data: any, e: any) => {
|
||||
if (revenueMode === false) {
|
||||
delete data.currency;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
runReport(data);
|
||||
};
|
||||
|
||||
const handleCheck = () => {
|
||||
setRevenueMode(!revenueMode);
|
||||
};
|
||||
|
||||
const handleAddStep = (step: { type: string; value: string }) => {
|
||||
if (step.type === 'url') {
|
||||
setRevenueMode(false);
|
||||
}
|
||||
updateReport({ parameters: { steps: parameters.steps.concat(step) } });
|
||||
};
|
||||
|
||||
const handleUpdateStep = (
|
||||
close: () => void,
|
||||
index: number,
|
||||
step: { type: string; value: string },
|
||||
) => {
|
||||
if (step.type === 'url') {
|
||||
setRevenueMode(false);
|
||||
}
|
||||
const steps = [...parameters.steps];
|
||||
steps[index] = step;
|
||||
updateReport({ parameters: { steps } });
|
||||
close();
|
||||
};
|
||||
|
||||
const handleRemoveStep = (index: number) => {
|
||||
const steps = [...parameters.steps];
|
||||
delete steps[index];
|
||||
updateReport({ parameters: { steps: steps.filter(n => n) } });
|
||||
};
|
||||
|
||||
const AddStepButton = () => {
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button isDisabled={steps.length > 0}>
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popover placement="right top">
|
||||
<FunnelStepAddForm onChange={handleAddStep} />
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
const items = [
|
||||
{ id: 'first-click', label: 'First-Click', value: 'firstClick' },
|
||||
{ id: 'last-click', label: 'Last-Click', value: 'lastClick' },
|
||||
];
|
||||
|
||||
const onModelChange = (value: any) => {
|
||||
setModel(value);
|
||||
updateReport({ parameters: { model } });
|
||||
};
|
||||
|
||||
return (
|
||||
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||
<FormField
|
||||
name="model"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
label={formatMessage(labels.model)}
|
||||
>
|
||||
<Select items={items} value={model} onChange={onModelChange}>
|
||||
{({ value, label }: any) => {
|
||||
return <ListItem key={value}>{label}</ListItem>;
|
||||
}}
|
||||
</Select>
|
||||
</FormField>
|
||||
<FormField name="step" label={formatMessage(labels.conversionStep)}>
|
||||
<ParameterList>
|
||||
{steps.map((step: { type: string; value: string }, index: number) => {
|
||||
return (
|
||||
<DialogTrigger key={index}>
|
||||
<ParameterList.Item
|
||||
icon={step.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||
onRemove={() => handleRemoveStep(index)}
|
||||
>
|
||||
<div>{step.value}</div>
|
||||
</ParameterList.Item>
|
||||
<Popover placement="right top">
|
||||
<AttributionStepAddForm
|
||||
type={step.type}
|
||||
value={step.value}
|
||||
onChange={handleUpdateStep.bind(null, close, index)}
|
||||
/>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
);
|
||||
})}
|
||||
</ParameterList>
|
||||
<AddStepButton />
|
||||
</FormField>
|
||||
|
||||
<Toggle
|
||||
isSelected={revenueMode}
|
||||
onClick={handleCheck}
|
||||
isDisabled={currencyValues.length === 0 || steps[0]?.type === 'url'}
|
||||
>
|
||||
<b>Revenue Mode</b>
|
||||
</Toggle>
|
||||
|
||||
{revenueMode && (
|
||||
<FormField
|
||||
name="currency"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
label={formatMessage(labels.currency)}
|
||||
>
|
||||
<Select items={currencyValues.map(item => ({ id: item.currency, value: item.currency }))}>
|
||||
{({ id, value }: any) => (
|
||||
<ListItem key={id} id={id}>
|
||||
{value}
|
||||
</ListItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormField>
|
||||
)}
|
||||
<FormButtons>
|
||||
<FormSubmitButton variant="primary" isDisabled={!queryEnabled} isLoading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
27
src/app/(main)/reports/attribution/AttributionReport.tsx
Normal file
27
src/app/(main)/reports/attribution/AttributionReport.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Icons } from '@/components/icons';
|
||||
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 { AttributionParameters } from './AttributionParameters';
|
||||
import { AttributionView } from './AttributionView';
|
||||
|
||||
const defaultParameters = {
|
||||
type: REPORT_TYPES.attribution,
|
||||
parameters: { model: 'firstClick', steps: [] },
|
||||
};
|
||||
|
||||
export default function AttributionReport({ reportId }: { reportId?: string }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Icons.Network />} />
|
||||
<ReportMenu>
|
||||
<AttributionParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<AttributionView />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
'use client';
|
||||
import AttributionReport from './AttributionReport';
|
||||
|
||||
export default function AttributionReportPage() {
|
||||
return <AttributionReport />;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { useState } from 'react';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import {
|
||||
Button,
|
||||
FormButtons,
|
||||
FormField,
|
||||
TextField,
|
||||
Row,
|
||||
Column,
|
||||
Select,
|
||||
ListItem,
|
||||
} from '@umami/react-zen';
|
||||
|
||||
export interface AttributionStepAddFormProps {
|
||||
type?: string;
|
||||
value?: string;
|
||||
onChange?: (step: { type: string; value: string }) => void;
|
||||
}
|
||||
|
||||
export function AttributionStepAddForm({
|
||||
type: defaultType = 'url',
|
||||
value: defaultValue = '',
|
||||
onChange,
|
||||
}: AttributionStepAddFormProps) {
|
||||
const [type, setType] = useState(defaultType);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const items = [
|
||||
{ id: 'url', label: formatMessage(labels.url), value: 'url' },
|
||||
{ id: 'event', label: formatMessage(labels.event), value: 'event' },
|
||||
];
|
||||
const isDisabled = !type || !value;
|
||||
|
||||
const handleSave = () => {
|
||||
onChange({ type, value });
|
||||
setValue('');
|
||||
};
|
||||
|
||||
const handleChange = e => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<FormField name="steps" label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||
<Row>
|
||||
<Select items={items} value={type} onChange={(value: any) => setType(value)}>
|
||||
{({ value, label }: any) => {
|
||||
return <ListItem key={value}>{label}</ListItem>;
|
||||
}}
|
||||
</Select>
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Row>
|
||||
</FormField>
|
||||
<FormButtons>
|
||||
<Button variant="primary" onClick={handleSave} isDisabled={isDisabled}>
|
||||
{formatMessage(defaultValue ? labels.update : labels.add)}
|
||||
</Button>
|
||||
</FormButtons>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttributionStepAddForm;
|
||||
133
src/app/(main)/reports/attribution/AttributionView.tsx
Normal file
133
src/app/(main)/reports/attribution/AttributionView.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { useContext } from 'react';
|
||||
import { PieChart } from '@/components/charts/PieChart';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { GridRow } from '@/components/common/GridRow';
|
||||
import { ListTable } from '@/components/metrics/ListTable';
|
||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||
import { CHART_COLORS } from '@/lib/constants';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { ReportContext } from '../[reportId]/Report';
|
||||
|
||||
export interface AttributionViewProps {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function AttributionView({ isLoading }: AttributionViewProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { report } = useContext(ReportContext);
|
||||
const {
|
||||
data,
|
||||
parameters: { currency },
|
||||
} = report || {};
|
||||
const ATTRIBUTION_PARAMS = [
|
||||
{ value: 'referrer', label: formatMessage(labels.referrers) },
|
||||
{ value: 'paidAds', label: formatMessage(labels.paidAds) },
|
||||
];
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { pageviews, visitors, visits } = data.total;
|
||||
|
||||
const metrics = data
|
||||
? [
|
||||
{
|
||||
value: pageviews,
|
||||
label: formatMessage(labels.views),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: visits,
|
||||
label: formatMessage(labels.visits),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: visitors,
|
||||
label: formatMessage(labels.visitors),
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
function UTMTable(UTMTableProps: { data: any; title: string; utm: string }) {
|
||||
const { data, title, utm } = UTMTableProps;
|
||||
const total = data[utm].reduce((sum, { value }) => {
|
||||
return +sum + +value;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<ListTable
|
||||
title={title}
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
currency={currency}
|
||||
data={data[utm].map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
z: (value / total) * 100,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MetricsBar isFetched={data}>
|
||||
{metrics?.map(({ label, value, formatValue }) => {
|
||||
return <MetricCard key={label} value={value} label={label} formatValue={formatValue} />;
|
||||
})}
|
||||
</MetricsBar>
|
||||
{ATTRIBUTION_PARAMS.map(({ value, label }) => {
|
||||
const items = data[value];
|
||||
const total = items.reduce((sum, { value }) => {
|
||||
return +sum + +value;
|
||||
}, 0);
|
||||
|
||||
const chartData = {
|
||||
labels: items.map(({ name }) => name),
|
||||
datasets: [
|
||||
{
|
||||
data: items.map(({ value }) => value),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={value}>
|
||||
<div>
|
||||
<div>{label}</div>
|
||||
<ListTable
|
||||
metric={formatMessage(currency ? labels.revenue : labels.visitors)}
|
||||
currency={currency}
|
||||
data={items.map(({ name, value }) => ({
|
||||
x: name,
|
||||
y: Number(value),
|
||||
z: (value / total) * 100,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PieChart type="doughnut" data={chartData} isLoading={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<>
|
||||
<GridRow columns="two">
|
||||
<UTMTable data={data} title={formatMessage(labels.sources)} utm={'utm_source'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.medium)} utm={'utm_medium'} />
|
||||
</GridRow>
|
||||
<GridRow columns="three">
|
||||
<UTMTable data={data} title={formatMessage(labels.campaigns)} utm={'utm_campaign'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.content)} utm={'utm_content'} />
|
||||
<UTMTable data={data} title={formatMessage(labels.terms)} utm={'utm_term'} />
|
||||
</GridRow>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttributionView;
|
||||
10
src/app/(main)/reports/attribution/page.tsx
Normal file
10
src/app/(main)/reports/attribution/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import AttributionReportPage from './AttributionReportPage';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export default function () {
|
||||
return <AttributionReportPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Attribution Report',
|
||||
};
|
||||
|
|
@ -51,6 +51,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
|||
url: renderTeamUrl('/reports/revenue'),
|
||||
icon: <Icons.Money />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.attribution),
|
||||
description: formatMessage(labels.attributionDescription),
|
||||
url: renderTeamUrl('/reports/attribution'),
|
||||
icon: <Icons.Network />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export function FunnelStepAddForm({
|
|||
const [value, setValue] = useState(defaultValue);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const items = [
|
||||
{ label: formatMessage(labels.url), value: 'url' },
|
||||
{ label: formatMessage(labels.event), value: 'event' },
|
||||
{ id: 'url', label: formatMessage(labels.url), value: 'url' },
|
||||
{ id: 'event', label: formatMessage(labels.event), value: 'event' },
|
||||
];
|
||||
const isDisabled = !type || !value;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
|
||||
import { EventsTable } from '@/components/metrics/EventsTable';
|
||||
import { useState } from 'react';
|
||||
import { WebsiteHeader } from '../WebsiteHeader';
|
||||
import { EventsDataTable } from './EventsDataTable';
|
||||
|
|
@ -12,9 +13,14 @@ import { useMessages } from '@/components/hooks';
|
|||
import { EventProperties } from './EventProperties';
|
||||
|
||||
export function EventsPage({ websiteId }) {
|
||||
const [label, setLabel] = useState(null);
|
||||
const [tab, setTab] = useState('activity');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleLabelClick = (value: string) => {
|
||||
setLabel(value !== label ? value : '');
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteHeader websiteId={websiteId} />
|
||||
|
|
@ -34,6 +40,17 @@ export function EventsPage({ websiteId }) {
|
|||
/>
|
||||
</Panel>
|
||||
</GridRow>
|
||||
<EventsMetricsBar websiteId={websiteId} />
|
||||
<GridRow columns="two-one">
|
||||
<EventsChart websiteId={websiteId} focusLabel={label} />
|
||||
<EventsTable
|
||||
websiteId={websiteId}
|
||||
type="event"
|
||||
title={formatMessage(labels.events)}
|
||||
metric={formatMessage(labels.actions)}
|
||||
onLabelClick={handleLabelClick}
|
||||
/>
|
||||
</GridRow>
|
||||
<Panel marginY="6">
|
||||
<Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
|
||||
<TabList>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { RealtimeCountries } from './RealtimeCountries';
|
|||
import { WebsiteHeader } from '../WebsiteHeader';
|
||||
import { percentFilter } from '@/lib/filters';
|
||||
|
||||
export function WebsiteRealtimePage({ websiteId }) {
|
||||
export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
|
||||
const { data, isLoading, error } = useRealtimeQuery(websiteId);
|
||||
|
||||
if (isLoading || error) {
|
||||
|
|
|
|||
|
|
@ -35,15 +35,15 @@ export function SessionInfo({ data }) {
|
|||
</Row>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Row>
|
||||
<Label>{formatMessage(labels.region)}</Label>
|
||||
<Row gap="3">
|
||||
<Icon>
|
||||
<Icons.Location />
|
||||
</Icon>
|
||||
<Text>{getRegionName(data?.subdivision1)}</Text>
|
||||
{getRegionName(data?.region)}
|
||||
</Row>
|
||||
</Box>
|
||||
</Row>
|
||||
|
||||
<Box>
|
||||
<Label>{formatMessage(labels.city)}</Label>
|
||||
|
|
|
|||
50
src/app/api/reports/attribution/route.ts
Normal file
50
src/app/api/reports/attribution/route.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { canViewWebsite } from '@/lib/auth';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { reportParms } from '@/lib/schema';
|
||||
import { getAttribution } from '@/queries/sql/reports/getAttribution';
|
||||
import { z } from 'zod';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const schema = z.object({
|
||||
...reportParms,
|
||||
model: z.string().regex(/firstClick|lastClick/i),
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
currency: z.string().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
const {
|
||||
websiteId,
|
||||
model,
|
||||
steps,
|
||||
currency,
|
||||
dateRange: { startDate, endDate },
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const data = await getAttribution(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
model: model,
|
||||
steps,
|
||||
currency,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ const schema = z.object({
|
|||
ip: z.string().ip().optional(),
|
||||
userAgent: z.string().optional(),
|
||||
timestamp: z.coerce.number().int().optional(),
|
||||
id: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ export async function POST(request: Request) {
|
|||
title,
|
||||
tag,
|
||||
timestamp,
|
||||
id,
|
||||
} = payload;
|
||||
|
||||
// Cache check
|
||||
|
|
@ -78,8 +80,10 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
// Client info
|
||||
const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } =
|
||||
await getClientInfo(request, payload);
|
||||
const { ip, userAgent, device, browser, os, country, region, city } = await getClientInfo(
|
||||
request,
|
||||
payload,
|
||||
);
|
||||
|
||||
// Bot check
|
||||
if (!process.env.DISABLE_BOT_CHECK && isbot(userAgent)) {
|
||||
|
|
@ -97,7 +101,7 @@ export async function POST(request: Request) {
|
|||
const sessionSalt = hash(startOfMonth(createdAt).toUTCString());
|
||||
const visitSalt = hash(startOfHour(createdAt).toUTCString());
|
||||
|
||||
const sessionId = uuid(websiteId, ip, userAgent, sessionSalt);
|
||||
const sessionId = id ? uuid(websiteId, id) : uuid(websiteId, ip, userAgent, sessionSalt);
|
||||
|
||||
// Find session
|
||||
if (!clickhouse.enabled && !cache?.sessionId) {
|
||||
|
|
@ -109,15 +113,13 @@ export async function POST(request: Request) {
|
|||
await createSession({
|
||||
id: sessionId,
|
||||
websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
region,
|
||||
city,
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
|
@ -146,14 +148,29 @@ export async function POST(request: Request) {
|
|||
const urlQuery = currentUrl.search.substring(1);
|
||||
const urlDomain = currentUrl.hostname.replace(/^www./, '');
|
||||
|
||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||
urlPath = urlPath.replace(/(.+)\/$/, '$1');
|
||||
}
|
||||
|
||||
let referrerPath: string;
|
||||
let referrerQuery: string;
|
||||
let referrerDomain: string;
|
||||
|
||||
// UTM Params
|
||||
const utmSource = currentUrl.searchParams.get('utm_source');
|
||||
const utmMedium = currentUrl.searchParams.get('utm_medium');
|
||||
const utmCampaign = currentUrl.searchParams.get('utm_campaign');
|
||||
const utmContent = currentUrl.searchParams.get('utm_content');
|
||||
const utmTerm = currentUrl.searchParams.get('utm_term');
|
||||
|
||||
// Click IDs
|
||||
const gclid = currentUrl.searchParams.get('gclid');
|
||||
const fbclid = currentUrl.searchParams.get('fbclid');
|
||||
const msclkid = currentUrl.searchParams.get('msclkid');
|
||||
const ttclid = currentUrl.searchParams.get('ttclid');
|
||||
const lifatid = currentUrl.searchParams.get('li_fat_id');
|
||||
const twclid = currentUrl.searchParams.get('twclid');
|
||||
|
||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||
urlPath = urlPath.replace(/(.+)\/$/, '$1');
|
||||
}
|
||||
|
||||
if (referrer) {
|
||||
const referrerUrl = new URL(referrer, base);
|
||||
|
||||
|
|
@ -171,10 +188,21 @@ export async function POST(request: Request) {
|
|||
visitId,
|
||||
urlPath: safeDecodeURI(urlPath),
|
||||
urlQuery,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
utmContent,
|
||||
utmTerm,
|
||||
referrerPath: safeDecodeURI(referrerPath),
|
||||
referrerQuery,
|
||||
referrerDomain,
|
||||
pageTitle: safeDecodeURIComponent(title),
|
||||
gclid,
|
||||
fbclid,
|
||||
msclkid,
|
||||
ttclid,
|
||||
lifatid,
|
||||
twclid,
|
||||
eventName: name,
|
||||
eventData: data,
|
||||
hostname: hostname || urlDomain,
|
||||
|
|
@ -184,8 +212,7 @@ export async function POST(request: Request) {
|
|||
screen,
|
||||
language,
|
||||
country,
|
||||
subdivision1,
|
||||
subdivision2,
|
||||
region,
|
||||
city,
|
||||
tag,
|
||||
createdAt,
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import { json } from '@/lib/response';
|
||||
import { CURRENT_VERSION } from '@/lib/constants';
|
||||
|
||||
export async function GET() {
|
||||
return json({ version: CURRENT_VERSION });
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue