Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow setting custom data on autotracked pageviews via data attribute on script element #2436

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/app/(main)/websites/[websiteId]/WebsiteDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import WebsiteHeader from './WebsiteHeader';
import WebsiteMetricsBar from './WebsiteMetricsBar';
import WebsiteTableView from './WebsiteTableView';

export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
export default function WebsiteDetails({
websiteId,
customDataFields,
}: {
websiteId: string;
customDataFields: string[];
}) {
const { data: website, isLoading, error } = useWebsite(websiteId);
const pathname = usePathname();
const { query } = useNavigation();
Expand All @@ -31,7 +37,13 @@ export default function WebsiteDetails({ websiteId }: { websiteId: string }) {
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />}
{website && (
<>
{!view && <WebsiteTableView websiteId={websiteId} domainName={website.domain} />}
{!view && (
<WebsiteTableView
customDataFields={customDataFields}
websiteId={websiteId}
domainName={website.domain}
/>
)}
{view && <WebsiteExpandedView websiteId={websiteId} domainName={website.domain} />}
</>
)}
Expand Down
14 changes: 14 additions & 0 deletions src/app/(main)/websites/[websiteId]/WebsiteTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import WorldMap from 'components/metrics/WorldMap';
import CountriesTable from 'components/metrics/CountriesTable';
import EventsTable from 'components/metrics/EventsTable';
import EventsChart from 'components/metrics/EventsChart';
import PageviewCustomDataTable from 'components/metrics/PageviewCustomDataTable';
import { chunkArray } from 'next-basics';

export default function WebsiteTableView({
websiteId,
domainName,
customDataFields,
}: {
websiteId: string;
domainName: string;
customDataFields: string[];
}) {
const [countryData, setCountryData] = useState();
const tableProps = {
Expand All @@ -35,6 +39,16 @@ export default function WebsiteTableView({
<OSTable {...tableProps} />
<DevicesTable {...tableProps} />
</GridRow>
{chunkArray(customDataFields, 3).map((fields, index) => (
<GridRow
key={index}
columns={fields.length === 1 ? 'one' : fields.length === 2 ? 'two' : 'three'}
>
{fields.map(field => (
<PageviewCustomDataTable key="custom" {...tableProps} fieldName={field} />
))}
</GridRow>
))}
<GridRow columns="two-one">
<WorldMap data={countryData} />
<CountriesTable {...tableProps} onDataLoad={setCountryData} />
Expand Down
4 changes: 2 additions & 2 deletions src/app/(main)/websites/[websiteId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import WebsiteDetails from './WebsiteDetails';
import { Metadata } from 'next';

export default function WebsitePage({ params: { websiteId } }) {
return <WebsiteDetails websiteId={websiteId} />;
export default function WebsitePage({ params: { websiteId }, searchParams }) {
return <WebsiteDetails customDataFields={searchParams['fields']?.split(',') ?? []} websiteId={websiteId} />;
}

export const metadata: Metadata = {
Expand Down
3 changes: 3 additions & 0 deletions src/components/metrics/MetricsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface MetricsTableProps extends ListTableProps {
onSearch?: (search: string) => void;
allowSearch?: boolean;
children?: ReactNode;
fieldName?: string
}

export function MetricsTable({
Expand All @@ -41,6 +42,7 @@ export function MetricsTable({
delay = null,
allowSearch = false,
children,
fieldName,
...props
}: MetricsTableProps) {
const [search, setSearch] = useState('');
Expand Down Expand Up @@ -70,6 +72,7 @@ export function MetricsTable({
city,
limit,
search,
fieldName,
},
{ retryDelay: delay || DEFAULT_ANIMATION_DURATION, onDataLoad },
);
Expand Down
29 changes: 29 additions & 0 deletions src/components/metrics/PageviewCustomDataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import useMessages from 'components/hooks/useMessages';
import MetricsTable, { MetricsTableProps } from 'components/metrics/MetricsTable';

export function PageviewCustomDataTable(props: MetricsTableProps) {
const { formatMessage, labels } = useMessages();

function renderLink({ x: field }) {
return <>{field}</>;
}

function titleize(fieldName: string) {
return fieldName
.split(/[_-]/)
.map((word, i) => (i === 0 ? word[0].toUpperCase() + word.slice(1) : word))
.join(' ');
}

return (
<MetricsTable
{...props}
title={titleize(props.fieldName)}
type="custom"
metric={formatMessage(labels.visitors)}
renderLabel={renderLink}
/>
);
}

export default PageviewCustomDataTable;
27 changes: 26 additions & 1 deletion src/pages/api/websites/[websiteId]/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface WebsiteMetricsRequestQuery {
limit?: number;
offset?: number;
search?: string;
fieldName?: string;
}

const schema = {
Expand All @@ -51,6 +52,7 @@ const schema = {
limit: yup.number(),
offset: yup.number(),
search: yup.string(),
fieldName: yup.string(),
}),
};

Expand All @@ -62,7 +64,26 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);

const { websiteId, type, limit, offset, search } = req.query;
const {
websiteId,
type,
url,
referrer,
title,
query,
os,
browser,
device,
country,
region,
city,
language,
event,
limit,
offset,
search,
fieldName,
} = req.query;

if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
Expand Down Expand Up @@ -111,6 +132,10 @@ export default async (
if (EVENT_COLUMNS.includes(type)) {
const data = await getPageviewMetrics(websiteId, type, filters, limit, offset);

return ok(res, data);
} else if (type === 'custom') {
const data = await getPageviewMetrics(websiteId, column, filters, limit, offset, fieldName);

return ok(res, data);
}

Expand Down
1 change: 1 addition & 0 deletions src/queries/analytics/eventData/getEventDataEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
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}}
and website_event.event_name is not null
group by website_event.event_name, event_data.data_key, event_data.data_type
order by 1 asc, 2 asc
limit 500
Expand Down
6 changes: 4 additions & 2 deletions src/queries/analytics/eventData/getEventDataStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
data_key,
count(*) as "total"
from event_data
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}
join website_event on website_event.event_id = event_data.website_event_id
where website_event.website_id = {{websiteId::uuid}}
and event_data.created_at between {{startDate}} and {{endDate}}
and website_event.event_name is not null
${filterQuery}
group by website_event_id, data_key
) as t
Expand Down
23 changes: 20 additions & 3 deletions src/queries/analytics/pageviews/getPageviewMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { EVENT_TYPE, FILTER_COLUMNS, SESSION_COLUMNS } from 'lib/constants';
import { QueryFilters } from 'lib/types';

export async function getPageviewMetrics(
...args: [websiteId: string, type: string, filters: QueryFilters, limit?: number, offset?: number]
...args: [
websiteId: string,
column: string,
filters: QueryFilters,
limit?: number,
offset?: number,
fieldName?: string,
]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
Expand All @@ -19,6 +26,7 @@ async function relationalQuery(
filters: QueryFilters,
limit: number = 500,
offset: number = 0,
fieldName?: string,
) {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = prisma;
Expand All @@ -32,27 +40,34 @@ async function relationalQuery(
);

let excludeDomain = '';
let joinEventData = '';
let filterEventDataOnFieldName = '';
if (column === 'referrer_domain') {
excludeDomain =
'and (website_event.referrer_domain != {{websiteDomain}} or website_event.referrer_domain is null)';
} else if (column === 'custom') {
joinEventData = 'join event_data on event_data.website_event_id = website_event.event_id';
filterEventDataOnFieldName = 'and event_data.event_key = {{fieldName}}';
}

return rawQuery(
`
select ${column} x, count(*) y
select ${column === 'custom' ? `event_data.string_value` : column} x, count(*) y
from website_event
${joinEventData}
${joinSession}
where website_event.website_id = {{websiteId::uuid}}
and website_event.created_at between {{startDate}} and {{endDate}}
and event_type = {{eventType}}
${filterEventDataOnFieldName}
${excludeDomain}
${filterQuery}
group by 1
order by 2 desc
limit ${limit}
offset ${offset}
`,
params,
{ ...params, fieldName },
);
}

Expand All @@ -62,6 +77,8 @@ async function clickhouseQuery(
filters: QueryFilters,
limit: number = 500,
offset: number = 0,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
fieldName?: string,
): Promise<{ x: string; y: number }[]> {
const column = FILTER_COLUMNS[type] || type;
const { rawQuery, parseFilters } = clickhouse;
Expand Down
18 changes: 17 additions & 1 deletion src/tracker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
const endpoint = `${host.replace(/\/$/, '')}__COLLECT_API_ENDPOINT__`;
const screen = `${width}x${height}`;
const eventRegex = /data-umami-event-([\w-_]+)/;
const pageviewCustomPropertyRegex = /data-([\w-_]+)/;
const reservedDataAttributes = ['website-id', 'domains', 'umami-event', 'auto-track', 'host-url', 'exclude-search'];
const eventNameAttribute = _data + 'umami-event';
const delayDuration = 300;

Expand Down Expand Up @@ -61,6 +63,20 @@
return excludeSearch ? url.split('?')[0] : url;
};

const getPageviewEventData = () =>
Object.fromEntries(
Array.from(currentScript.attributes)
.filter(attribute => attribute.name.match(pageviewCustomPropertyRegex))
.filter(
attribute =>
!reservedDataAttributes.some(reserved => _data + reserved === attribute.name),
)
.map(attribute => {
const match = attribute.name.match(pageviewCustomPropertyRegex);
return [match[1], attribute.value];
}),
);

const getPayload = () => ({
website,
hostname,
Expand Down Expand Up @@ -230,7 +246,7 @@
} else if (typeof obj === 'function') {
return send(obj(getPayload()));
}
return send(getPayload());
return send({ ...getPayload(), data: getPageviewEventData() });
};

const identify = data => send({ ...getPayload(), data }, 'identify');
Expand Down