mirror of
https://github.com/umami-software/umami.git
synced 2026-02-14 09:35:36 +01:00
Merge branch 'feat/attribution-report' into dev
This commit is contained in:
commit
40ca861c69
20 changed files with 1488 additions and 15 deletions
332
db/clickhouse/migrations/05_add_utm_clid.sql
Normal file
332
db/clickhouse/migrations/05_add_utm_clid.sql
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
-- Create Event
|
||||||
|
CREATE TABLE umami.website_event_new
|
||||||
|
(
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
event_id UUID,
|
||||||
|
--sessions
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String),
|
||||||
|
subdivision1 LowCardinality(String),
|
||||||
|
subdivision2 LowCardinality(String),
|
||||||
|
city String,
|
||||||
|
--pageviews
|
||||||
|
url_path String,
|
||||||
|
url_query String,
|
||||||
|
utm_source String,
|
||||||
|
utm_medium String,
|
||||||
|
utm_campaign String,
|
||||||
|
utm_content String,
|
||||||
|
utm_term String,
|
||||||
|
referrer_path String,
|
||||||
|
referrer_query String,
|
||||||
|
referrer_domain String,
|
||||||
|
page_title String,
|
||||||
|
--clickIDs
|
||||||
|
gclid String,
|
||||||
|
fbclid String,
|
||||||
|
msclkid String,
|
||||||
|
ttclid String,
|
||||||
|
li_fat_id String,
|
||||||
|
twclid String,
|
||||||
|
--events
|
||||||
|
event_type UInt32,
|
||||||
|
event_name String,
|
||||||
|
tag String,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
job_id Nullable(UUID)
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
PARTITION BY toYYYYMM(created_at)
|
||||||
|
ORDER BY (toStartOfHour(created_at), website_id, session_id, visit_id, created_at)
|
||||||
|
PRIMARY KEY (toStartOfHour(created_at), website_id, session_id, visit_id)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
-- stats hourly
|
||||||
|
CREATE TABLE umami.website_event_stats_hourly_new
|
||||||
|
(
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
visit_id UUID,
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String),
|
||||||
|
subdivision1 LowCardinality(String),
|
||||||
|
city String,
|
||||||
|
entry_url AggregateFunction(argMin, String, DateTime('UTC')),
|
||||||
|
exit_url AggregateFunction(argMax, String, DateTime('UTC')),
|
||||||
|
url_path SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
url_query SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_source SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_medium SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_campaign SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_content SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_term SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
referrer_domain SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
page_title SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
gclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
fbclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
msclkid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
ttclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
li_fat_id SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
twclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
event_type UInt32,
|
||||||
|
event_name SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
views SimpleAggregateFunction(sum, UInt64),
|
||||||
|
min_time SimpleAggregateFunction(min, DateTime('UTC')),
|
||||||
|
max_time SimpleAggregateFunction(max, DateTime('UTC')),
|
||||||
|
tag SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
created_at Datetime('UTC')
|
||||||
|
)
|
||||||
|
ENGINE = AggregatingMergeTree
|
||||||
|
PARTITION BY toYYYYMM(created_at)
|
||||||
|
ORDER BY (
|
||||||
|
website_id,
|
||||||
|
event_type,
|
||||||
|
toStartOfHour(created_at),
|
||||||
|
cityHash64(visit_id),
|
||||||
|
visit_id
|
||||||
|
)
|
||||||
|
SAMPLE BY cityHash64(visit_id);
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv_new
|
||||||
|
TO umami.website_event_stats_hourly_new
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
city,
|
||||||
|
entry_url,
|
||||||
|
exit_url,
|
||||||
|
url_paths as url_path,
|
||||||
|
url_query,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
utm_content,
|
||||||
|
utm_term,
|
||||||
|
referrer_domain,
|
||||||
|
page_title,
|
||||||
|
gclid,
|
||||||
|
fbclid,
|
||||||
|
msclkid,
|
||||||
|
ttclid,
|
||||||
|
li_fat_id,
|
||||||
|
twclid,
|
||||||
|
event_type,
|
||||||
|
event_name,
|
||||||
|
views,
|
||||||
|
min_time,
|
||||||
|
max_time,
|
||||||
|
tag,
|
||||||
|
timestamp as created_at
|
||||||
|
FROM (SELECT
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
city,
|
||||||
|
argMinState(url_path, created_at) entry_url,
|
||||||
|
argMaxState(url_path, created_at) exit_url,
|
||||||
|
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
||||||
|
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_source)) utm_source,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
|
||||||
|
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
|
||||||
|
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
||||||
|
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
|
||||||
|
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||||
|
event_type,
|
||||||
|
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||||
|
sumIf(1, event_type = 1) views,
|
||||||
|
min(created_at) min_time,
|
||||||
|
max(created_at) max_time,
|
||||||
|
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||||
|
toStartOfHour(created_at) timestamp
|
||||||
|
FROM umami.website_event_new
|
||||||
|
GROUP BY website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
city,
|
||||||
|
event_type,
|
||||||
|
timestamp);
|
||||||
|
|
||||||
|
-- projections
|
||||||
|
ALTER TABLE umami.website_event_new
|
||||||
|
ADD PROJECTION website_event_url_path_projection (
|
||||||
|
SELECT * ORDER BY toStartOfDay(created_at), website_id, url_path, created_at
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE umami.website_event_new MATERIALIZE PROJECTION website_event_url_path_projection_new;
|
||||||
|
|
||||||
|
ALTER TABLE umami.website_event_new
|
||||||
|
ADD PROJECTION website_event_referrer_domain_projection (
|
||||||
|
SELECT * ORDER BY toStartOfDay(created_at), website_id, referrer_domain, created_at
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE umami.website_event_new MATERIALIZE PROJECTION website_event_referrer_domain_projection;
|
||||||
|
|
||||||
|
-- migration
|
||||||
|
INSERT INTO umami.website_event_new
|
||||||
|
SELECT website_id, session_id, visit_id, event_id, hostname, browser, os, device, screen, language, country, subdivision1, subdivision2, city, url_path, url_query,
|
||||||
|
extract(url_query, 'utm_source=([^&]*)') AS utm_source,
|
||||||
|
extract(url_query, 'utm_medium=([^&]*)') AS utm_medium,
|
||||||
|
extract(url_query, 'utm_campaign=([^&]*)') AS utm_campaign,
|
||||||
|
extract(url_query, 'utm_content=([^&]*)') AS utm_content,
|
||||||
|
extract(url_query, 'utm_term=([^&]*)') AS utm_term,referrer_path, referrer_query, referrer_domain,
|
||||||
|
page_title,
|
||||||
|
extract(url_query, 'gclid=([^&]*)') gclid,
|
||||||
|
extract(url_query, 'fbclid=([^&]*)') fbclid,
|
||||||
|
extract(url_query, 'msclkid=([^&]*)') msclkid,
|
||||||
|
extract(url_query, 'ttclid=([^&]*)') ttclid,
|
||||||
|
extract(url_query, 'li_fat_id=([^&]*)') li_fat_id,
|
||||||
|
extract(url_query, 'twclid=([^&]*)') twclid,
|
||||||
|
event_type, event_name, tag, created_at, job_id
|
||||||
|
FROM umami.website_event
|
||||||
|
|
||||||
|
-- rename tables
|
||||||
|
RENAME TABLE umami.website_event TO umami.website_event_old;
|
||||||
|
RENAME TABLE umami.website_event_new TO umami.website_event;
|
||||||
|
|
||||||
|
RENAME TABLE umami.website_event_stats_hourly TO umami.website_event_stats_hourly_old;
|
||||||
|
RENAME TABLE umami.website_event_stats_hourly_new TO umami.website_event_stats_hourly;
|
||||||
|
|
||||||
|
RENAME TABLE umami.website_event_stats_hourly_mv TO umami.website_event_stats_hourly_mv_old;
|
||||||
|
RENAME TABLE umami.website_event_stats_hourly_mv_new TO umami.website_event_stats_hourly_mv;
|
||||||
|
|
||||||
|
-- recreate view
|
||||||
|
DROP TABLE umami.website_event_stats_hourly_mv;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW umami.website_event_stats_hourly_mv
|
||||||
|
TO umami.website_event_stats_hourly
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
city,
|
||||||
|
entry_url,
|
||||||
|
exit_url,
|
||||||
|
url_paths as url_path,
|
||||||
|
url_query,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
utm_content,
|
||||||
|
utm_term,
|
||||||
|
referrer_domain,
|
||||||
|
page_title,
|
||||||
|
gclid,
|
||||||
|
fbclid,
|
||||||
|
msclkid,
|
||||||
|
ttclid,
|
||||||
|
li_fat_id,
|
||||||
|
twclid,
|
||||||
|
event_type,
|
||||||
|
event_name,
|
||||||
|
views,
|
||||||
|
min_time,
|
||||||
|
max_time,
|
||||||
|
tag,
|
||||||
|
timestamp as created_at
|
||||||
|
FROM (SELECT
|
||||||
|
website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
city,
|
||||||
|
argMinState(url_path, created_at) entry_url,
|
||||||
|
argMaxState(url_path, created_at) exit_url,
|
||||||
|
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
||||||
|
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_source)) utm_source,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
|
||||||
|
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
|
||||||
|
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
||||||
|
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
|
||||||
|
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||||
|
event_type,
|
||||||
|
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||||
|
sumIf(1, event_type = 1) views,
|
||||||
|
min(created_at) min_time,
|
||||||
|
max(created_at) max_time,
|
||||||
|
arrayFilter(x -> x != '', groupArray(tag)) tag,
|
||||||
|
toStartOfHour(created_at) timestamp
|
||||||
|
FROM umami.website_event
|
||||||
|
GROUP BY website_id,
|
||||||
|
session_id,
|
||||||
|
visit_id,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
city,
|
||||||
|
event_type,
|
||||||
|
timestamp);
|
||||||
|
|
@ -19,10 +19,22 @@ CREATE TABLE umami.website_event
|
||||||
--pageviews
|
--pageviews
|
||||||
url_path String,
|
url_path String,
|
||||||
url_query String,
|
url_query String,
|
||||||
|
utm_source String,
|
||||||
|
utm_medium String,
|
||||||
|
utm_campaign String,
|
||||||
|
utm_content String,
|
||||||
|
utm_term String,
|
||||||
referrer_path String,
|
referrer_path String,
|
||||||
referrer_query String,
|
referrer_query String,
|
||||||
referrer_domain String,
|
referrer_domain String,
|
||||||
page_title String,
|
page_title String,
|
||||||
|
--clickIDs
|
||||||
|
gclid String,
|
||||||
|
fbclid String,
|
||||||
|
msclkid String,
|
||||||
|
ttclid String,
|
||||||
|
li_fat_id String,
|
||||||
|
twclid String,
|
||||||
--events
|
--events
|
||||||
event_type UInt32,
|
event_type UInt32,
|
||||||
event_name String,
|
event_name String,
|
||||||
|
|
@ -90,8 +102,19 @@ CREATE TABLE umami.website_event_stats_hourly
|
||||||
exit_url AggregateFunction(argMax, String, DateTime('UTC')),
|
exit_url AggregateFunction(argMax, String, DateTime('UTC')),
|
||||||
url_path SimpleAggregateFunction(groupArrayArray, Array(String)),
|
url_path SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
url_query SimpleAggregateFunction(groupArrayArray, Array(String)),
|
url_query SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_source SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_medium SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_campaign SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_content SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
utm_term SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
referrer_domain SimpleAggregateFunction(groupArrayArray, Array(String)),
|
referrer_domain SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
page_title SimpleAggregateFunction(groupArrayArray, Array(String)),
|
page_title SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
gclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
fbclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
msclkid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
ttclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
li_fat_id SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
|
twclid SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
event_type UInt32,
|
event_type UInt32,
|
||||||
event_name SimpleAggregateFunction(groupArrayArray, Array(String)),
|
event_name SimpleAggregateFunction(groupArrayArray, Array(String)),
|
||||||
views SimpleAggregateFunction(sum, UInt64),
|
views SimpleAggregateFunction(sum, UInt64),
|
||||||
|
|
@ -131,8 +154,19 @@ SELECT
|
||||||
exit_url,
|
exit_url,
|
||||||
url_paths as url_path,
|
url_paths as url_path,
|
||||||
url_query,
|
url_query,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
utm_content,
|
||||||
|
utm_term,
|
||||||
referrer_domain,
|
referrer_domain,
|
||||||
page_title,
|
page_title,
|
||||||
|
gclid,
|
||||||
|
fbclid,
|
||||||
|
msclkid,
|
||||||
|
ttclid,
|
||||||
|
li_fat_id,
|
||||||
|
twclid,
|
||||||
event_type,
|
event_type,
|
||||||
event_name,
|
event_name,
|
||||||
views,
|
views,
|
||||||
|
|
@ -157,8 +191,19 @@ FROM (SELECT
|
||||||
argMaxState(url_path, created_at) exit_url,
|
argMaxState(url_path, created_at) exit_url,
|
||||||
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
arrayFilter(x -> x != '', groupArray(url_path)) as url_paths,
|
||||||
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
arrayFilter(x -> x != '', groupArray(url_query)) url_query,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_source)) utm_source,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_medium)) utm_medium,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_campaign)) utm_campaign,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_content)) utm_content,
|
||||||
|
arrayFilter(x -> x != '', groupArray(utm_term)) utm_term,
|
||||||
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
|
arrayFilter(x -> x != '', groupArray(referrer_domain)) referrer_domain,
|
||||||
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
arrayFilter(x -> x != '', groupArray(page_title)) page_title,
|
||||||
|
arrayFilter(x -> x != '', groupArray(gclid)) gclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(fbclid)) fbclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(msclkid)) msclkid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(ttclid)) ttclid,
|
||||||
|
arrayFilter(x -> x != '', groupArray(li_fat_id)) li_fat_id,
|
||||||
|
arrayFilter(x -> x != '', groupArray(twclid)) twclid,
|
||||||
event_type,
|
event_type,
|
||||||
if(event_type = 2, groupArray(event_name), []) event_name,
|
if(event_type = 2, groupArray(event_name), []) event_name,
|
||||||
sumIf(1, event_type = 1) views,
|
sumIf(1, event_type = 1) views,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
188
src/app/(main)/reports/attribution/AttributionParameters.tsx
Normal file
188
src/app/(main)/reports/attribution/AttributionParameters.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import Icons from '@/components/icons';
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
Form,
|
||||||
|
FormButtons,
|
||||||
|
FormInput,
|
||||||
|
FormRow,
|
||||||
|
Icon,
|
||||||
|
Item,
|
||||||
|
Popup,
|
||||||
|
PopupTrigger,
|
||||||
|
SubmitButton,
|
||||||
|
Toggle,
|
||||||
|
} from 'react-basics';
|
||||||
|
import BaseParameters from '../[reportId]/BaseParameters';
|
||||||
|
import ParameterList from '../[reportId]/ParameterList';
|
||||||
|
import PopupForm from '../[reportId]/PopupForm';
|
||||||
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
|
import FunnelStepAddForm from '../funnel/FunnelStepAddForm';
|
||||||
|
import styles from './AttributionParameters.module.css';
|
||||||
|
import AttributionStepAddForm from './AttributionStepAddForm';
|
||||||
|
import useRevenueValues from '@/components/hooks/queries/useRevenueValues';
|
||||||
|
|
||||||
|
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 = [] } = useRevenueValues(
|
||||||
|
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 (
|
||||||
|
<PopupTrigger disabled={steps.length > 0}>
|
||||||
|
<Button disabled={steps.length > 0}>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Plus />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
<Popup alignment="start">
|
||||||
|
<PopupForm>
|
||||||
|
<FunnelStepAddForm onChange={handleAddStep} />
|
||||||
|
</PopupForm>
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ label: 'First-Click', value: 'firstClick' },
|
||||||
|
{ label: 'Last-Click', value: 'lastClick' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderModelValue = (value: any) => {
|
||||||
|
return items.find(item => item.value === value)?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModelChange = (value: any) => {
|
||||||
|
setModel(value);
|
||||||
|
updateReport({ parameters: { model } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||||
|
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
|
||||||
|
<FormRow label={formatMessage(labels.model)}>
|
||||||
|
<FormInput name="model" rules={{ required: formatMessage(labels.required) }}>
|
||||||
|
<Dropdown
|
||||||
|
items={items}
|
||||||
|
value={model}
|
||||||
|
renderValue={renderModelValue}
|
||||||
|
onChange={onModelChange}
|
||||||
|
>
|
||||||
|
{({ value, label }) => {
|
||||||
|
return <Item key={value}>{label}</Item>;
|
||||||
|
}}
|
||||||
|
</Dropdown>
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow label={formatMessage(labels.conversionStep)} action={<AddStepButton />}>
|
||||||
|
<ParameterList>
|
||||||
|
{steps.map((step: { type: string; value: string }, index: number) => {
|
||||||
|
return (
|
||||||
|
<PopupTrigger key={index}>
|
||||||
|
<ParameterList.Item
|
||||||
|
className={styles.item}
|
||||||
|
icon={step.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
|
||||||
|
onRemove={() => handleRemoveStep(index)}
|
||||||
|
>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<div>{step.value}</div>
|
||||||
|
</div>
|
||||||
|
</ParameterList.Item>
|
||||||
|
<Popup alignment="start">
|
||||||
|
{(close: () => void) => (
|
||||||
|
<PopupForm>
|
||||||
|
<AttributionStepAddForm
|
||||||
|
type={step.type}
|
||||||
|
value={step.value}
|
||||||
|
onChange={handleUpdateStep.bind(null, close, index)}
|
||||||
|
/>
|
||||||
|
</PopupForm>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ParameterList>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<Toggle
|
||||||
|
checked={revenueMode}
|
||||||
|
onChecked={handleCheck}
|
||||||
|
disabled={currencyValues.length === 0 || steps[0]?.type === 'url'}
|
||||||
|
>
|
||||||
|
<b>Revenue Mode</b>
|
||||||
|
</Toggle>
|
||||||
|
</FormRow>
|
||||||
|
{revenueMode && (
|
||||||
|
<FormRow label={formatMessage(labels.currency)}>
|
||||||
|
<FormInput name="currency" rules={{ required: formatMessage(labels.required) }}>
|
||||||
|
<Dropdown items={currencyValues.map(item => item.currency)}>
|
||||||
|
{item => <Item key={item}>{item}</Item>}
|
||||||
|
</Dropdown>
|
||||||
|
</FormInput>
|
||||||
|
</FormRow>
|
||||||
|
)}
|
||||||
|
<FormButtons>
|
||||||
|
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}>
|
||||||
|
{formatMessage(labels.runQuery)}
|
||||||
|
</SubmitButton>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttributionParameters;
|
||||||
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 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 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={<Money />} />
|
||||||
|
<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,7 @@
|
||||||
|
.dropdown {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
|
||||||
|
import styles from './AttributionStepAddForm.module.css';
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
{ label: formatMessage(labels.url), value: 'url' },
|
||||||
|
{ 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTypeValue = (value: any) => {
|
||||||
|
return items.find(item => item.value === value)?.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox direction="column" gap={10}>
|
||||||
|
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
|
||||||
|
<Flexbox gap={10}>
|
||||||
|
<Dropdown
|
||||||
|
className={styles.dropdown}
|
||||||
|
items={items}
|
||||||
|
value={type}
|
||||||
|
renderValue={renderTypeValue}
|
||||||
|
onChange={(value: any) => setType(value)}
|
||||||
|
>
|
||||||
|
{({ value, label }) => {
|
||||||
|
return <Item key={value}>{label}</Item>;
|
||||||
|
}}
|
||||||
|
</Dropdown>
|
||||||
|
<TextField
|
||||||
|
className={styles.input}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
autoFocus={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</Flexbox>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<Button variant="primary" onClick={handleSave} disabled={isDisabled}>
|
||||||
|
{formatMessage(defaultValue ? labels.update : labels.add)}
|
||||||
|
</Button>
|
||||||
|
</FormRow>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttributionStepAddForm;
|
||||||
38
src/app/(main)/reports/attribution/AttributionTable.tsx
Normal file
38
src/app/(main)/reports/attribution/AttributionTable.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import EmptyPlaceholder from '@/components/common/EmptyPlaceholder';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { GridColumn, GridTable } from 'react-basics';
|
||||||
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
|
import { formatLongCurrency } from '@/lib/format';
|
||||||
|
|
||||||
|
export function AttributionTable() {
|
||||||
|
const { report } = useContext(ReportContext);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { data } = report || {};
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <EmptyPlaceholder />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridTable data={data.table || []}>
|
||||||
|
<GridColumn name="currency" label={formatMessage(labels.currency)} alignment="end">
|
||||||
|
{row => row.currency}
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn name="currency" label={formatMessage(labels.total)} width="300px" alignment="end">
|
||||||
|
{row => formatLongCurrency(row.sum, row.currency)}
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn name="currency" label={formatMessage(labels.average)} alignment="end">
|
||||||
|
{row => formatLongCurrency(row.count ? row.sum / row.count : 0, row.currency)}
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn name="currency" label={formatMessage(labels.transactions)} alignment="end">
|
||||||
|
{row => row.count}
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn name="currency" label={formatMessage(labels.uniqueCustomers)} alignment="end">
|
||||||
|
{row => row.unique_count}
|
||||||
|
</GridColumn>
|
||||||
|
</GridTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttributionTable;
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50% 50%;
|
||||||
|
gap: 20px;
|
||||||
|
border-top: 1px solid var(--base300);
|
||||||
|
padding-top: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
134
src/app/(main)/reports/attribution/AttributionView.tsx
Normal file
134
src/app/(main)/reports/attribution/AttributionView.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import PieChart from '@/components/charts/PieChart';
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { Grid, GridRow } from '@/components/layout/Grid';
|
||||||
|
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 { useContext } from 'react';
|
||||||
|
import { ReportContext } from '../[reportId]/Report';
|
||||||
|
import styles from './AttributionView.module.css';
|
||||||
|
|
||||||
|
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 className={styles.container}>
|
||||||
|
<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} className={styles.row}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>{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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Grid>
|
||||||
|
<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>
|
||||||
|
</Grid>
|
||||||
|
</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',
|
||||||
|
};
|
||||||
|
|
@ -58,6 +58,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
|
||||||
url: renderTeamUrl('/reports/revenue'),
|
url: renderTeamUrl('/reports/revenue'),
|
||||||
icon: <Money />,
|
icon: <Money />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: formatMessage(labels.attribution),
|
||||||
|
description: formatMessage(labels.attributionDescription),
|
||||||
|
url: renderTeamUrl('/reports/attribution'),
|
||||||
|
icon: <Money />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
@ -163,7 +163,13 @@ export const labels = defineMessages({
|
||||||
id: 'label.revenue-description',
|
id: 'label.revenue-description',
|
||||||
defaultMessage: 'Look into your revenue data and how users are spending.',
|
defaultMessage: 'Look into your revenue data and how users are spending.',
|
||||||
},
|
},
|
||||||
|
attribution: { id: 'label.attribution', defaultMessage: 'Attribution' },
|
||||||
|
attributionDescription: {
|
||||||
|
id: 'label.attribution-description',
|
||||||
|
defaultMessage: 'See how users engage with your marketing and what drives conversions.',
|
||||||
|
},
|
||||||
currency: { id: 'label.currency', defaultMessage: 'Currency' },
|
currency: { id: 'label.currency', defaultMessage: 'Currency' },
|
||||||
|
model: { id: 'label.model', defaultMessage: 'Model' },
|
||||||
url: { id: 'label.url', defaultMessage: 'URL' },
|
url: { id: 'label.url', defaultMessage: 'URL' },
|
||||||
urls: { id: 'label.urls', defaultMessage: 'URLs' },
|
urls: { id: 'label.urls', defaultMessage: 'URLs' },
|
||||||
path: { id: 'label.path', defaultMessage: 'Path' },
|
path: { id: 'label.path', defaultMessage: 'Path' },
|
||||||
|
|
@ -257,6 +263,7 @@ export const labels = defineMessages({
|
||||||
id: 'label.utm-description',
|
id: 'label.utm-description',
|
||||||
defaultMessage: 'Track your campaigns through UTM parameters.',
|
defaultMessage: 'Track your campaigns through UTM parameters.',
|
||||||
},
|
},
|
||||||
|
conversionStep: { id: 'label.conversion-step', defaultMessage: 'Conversion Step' },
|
||||||
steps: { id: 'label.steps', defaultMessage: 'Steps' },
|
steps: { id: 'label.steps', defaultMessage: 'Steps' },
|
||||||
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
|
startStep: { id: 'label.start-step', defaultMessage: 'Start Step' },
|
||||||
endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
|
endStep: { id: 'label.end-step', defaultMessage: 'End Step' },
|
||||||
|
|
@ -281,6 +288,11 @@ export const labels = defineMessages({
|
||||||
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
|
firstSeen: { id: 'label.first-seen', defaultMessage: 'First seen' },
|
||||||
properties: { id: 'label.properties', defaultMessage: 'Properties' },
|
properties: { id: 'label.properties', defaultMessage: 'Properties' },
|
||||||
channels: { id: 'label.channels', defaultMessage: 'Channels' },
|
channels: { id: 'label.channels', defaultMessage: 'Channels' },
|
||||||
|
sources: { id: 'label.sources', defaultMessage: 'Sources' },
|
||||||
|
medium: { id: 'label.medium', defaultMessage: 'Medium' },
|
||||||
|
campaigns: { id: 'label.campaigns', defaultMessage: 'Campaigns' },
|
||||||
|
content: { id: 'label.content', defaultMessage: 'Content' },
|
||||||
|
terms: { id: 'label.terms', defaultMessage: 'Terms' },
|
||||||
direct: { id: 'label.direct', defaultMessage: 'Direct' },
|
direct: { id: 'label.direct', defaultMessage: 'Direct' },
|
||||||
referral: { id: 'label.referral', defaultMessage: 'Referral' },
|
referral: { id: 'label.referral', defaultMessage: 'Referral' },
|
||||||
affiliate: { id: 'label.affiliate', defaultMessage: 'Affiliate' },
|
affiliate: { id: 'label.affiliate', defaultMessage: 'Affiliate' },
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { FixedSizeList } from 'react-window';
|
|
||||||
import { useSpring, animated, config } from '@react-spring/web';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import Empty from '@/components/common/Empty';
|
import Empty from '@/components/common/Empty';
|
||||||
import { formatLongNumber } from '@/lib/format';
|
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
import styles from './ListTable.module.css';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
|
import { animated, config, useSpring } from '@react-spring/web';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { FixedSizeList } from 'react-window';
|
||||||
|
import styles from './ListTable.module.css';
|
||||||
|
|
||||||
const ITEM_SIZE = 30;
|
const ITEM_SIZE = 30;
|
||||||
|
|
||||||
|
|
@ -20,6 +20,7 @@ export interface ListTableProps {
|
||||||
virtualize?: boolean;
|
virtualize?: boolean;
|
||||||
showPercentage?: boolean;
|
showPercentage?: boolean;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
|
currency?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListTable({
|
export function ListTable({
|
||||||
|
|
@ -33,6 +34,7 @@ export function ListTable({
|
||||||
virtualize = false,
|
virtualize = false,
|
||||||
showPercentage = true,
|
showPercentage = true,
|
||||||
itemCount = 10,
|
itemCount = 10,
|
||||||
|
currency,
|
||||||
}: ListTableProps) {
|
}: ListTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
|
@ -48,6 +50,7 @@ export function ListTable({
|
||||||
animate={animate && !virtualize}
|
animate={animate && !virtualize}
|
||||||
showPercentage={showPercentage}
|
showPercentage={showPercentage}
|
||||||
change={renderChange ? renderChange(row, index) : null}
|
change={renderChange ? renderChange(row, index) : null}
|
||||||
|
currency={currency}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -81,7 +84,15 @@ export function ListTable({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedRow = ({ label, value = 0, percent, change, animate, showPercentage = true }) => {
|
const AnimatedRow = ({
|
||||||
|
label,
|
||||||
|
value = 0,
|
||||||
|
percent,
|
||||||
|
change,
|
||||||
|
animate,
|
||||||
|
showPercentage = true,
|
||||||
|
currency,
|
||||||
|
}) => {
|
||||||
const props = useSpring({
|
const props = useSpring({
|
||||||
width: percent,
|
width: percent,
|
||||||
y: value,
|
y: value,
|
||||||
|
|
@ -95,7 +106,9 @@ const AnimatedRow = ({ label, value = 0, percent, change, animate, showPercentag
|
||||||
<div className={styles.value}>
|
<div className={styles.value}>
|
||||||
{change}
|
{change}
|
||||||
<animated.div className={styles.value} title={props?.y as any}>
|
<animated.div className={styles.value} title={props?.y as any}>
|
||||||
{props.y?.to(formatLongNumber)}
|
{currency
|
||||||
|
? props.y?.to(n => formatLongCurrency(n, currency))
|
||||||
|
: props.y?.to(formatLongNumber)}
|
||||||
</animated.div>
|
</animated.div>
|
||||||
</div>
|
</div>
|
||||||
{showPercentage && (
|
{showPercentage && (
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ export const REPORT_TYPES = {
|
||||||
utm: 'utm',
|
utm: 'utm',
|
||||||
journey: 'journey',
|
journey: 'journey',
|
||||||
revenue: 'revenue',
|
revenue: 'revenue',
|
||||||
|
attribution: 'attribution',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const REPORT_PARAMETERS = {
|
export const REPORT_PARAMETERS = {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export const reportTypeParam = z.enum([
|
||||||
'goals',
|
'goals',
|
||||||
'journey',
|
'journey',
|
||||||
'revenue',
|
'revenue',
|
||||||
|
'attribution',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const reportParms = {
|
export const reportParms = {
|
||||||
|
|
|
||||||
475
src/queries/sql/reports/getAttribution.ts
Normal file
475
src/queries/sql/reports/getAttribution.ts
Normal file
|
|
@ -0,0 +1,475 @@
|
||||||
|
import clickhouse from '@/lib/clickhouse';
|
||||||
|
import { EVENT_TYPE } from '@/lib/constants';
|
||||||
|
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
|
||||||
|
export async function getAttribution(
|
||||||
|
...args: [
|
||||||
|
websiteId: string,
|
||||||
|
criteria: {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
model: string;
|
||||||
|
steps: { type: string; value: string }[];
|
||||||
|
currency: string;
|
||||||
|
},
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
return runQuery({
|
||||||
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationalQuery(
|
||||||
|
websiteId: string,
|
||||||
|
criteria: {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
model: string;
|
||||||
|
steps: { type: string; value: string }[];
|
||||||
|
currency: string;
|
||||||
|
},
|
||||||
|
): Promise<{
|
||||||
|
referrer: { name: string; value: number }[];
|
||||||
|
paidAds: { name: string; value: number }[];
|
||||||
|
utm_source: { name: string; value: number }[];
|
||||||
|
utm_medium: { name: string; value: number }[];
|
||||||
|
utm_campaign: { name: string; value: number }[];
|
||||||
|
utm_content: { name: string; value: number }[];
|
||||||
|
utm_term: { name: string; value: number }[];
|
||||||
|
total: { pageviews: number; visitors: number; visits: number };
|
||||||
|
}> {
|
||||||
|
const { startDate, endDate, model, steps, currency } = criteria;
|
||||||
|
const { rawQuery } = prisma;
|
||||||
|
const conversionStep = steps[0].value;
|
||||||
|
const eventType = steps[0].type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||||
|
const column = steps[0].type === 'url' ? 'url_path' : 'event_name';
|
||||||
|
//const db = getDatabaseType();
|
||||||
|
//const like = db === POSTGRESQL ? 'ilike' : 'like';
|
||||||
|
|
||||||
|
function getUTMQuery(utmColumn: string) {
|
||||||
|
return `
|
||||||
|
select
|
||||||
|
we.${utmColumn} name,
|
||||||
|
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||||
|
from events e
|
||||||
|
join model m
|
||||||
|
on m.session_id = e.session_id
|
||||||
|
join website_event we
|
||||||
|
on we.created_at = m.created_at
|
||||||
|
and we.session_id = m.session_id
|
||||||
|
${currency ? '' : `where we.${utmColumn} != ''`}
|
||||||
|
group by 1
|
||||||
|
order by name desc
|
||||||
|
limit 20`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventQuery = `WITH events AS (
|
||||||
|
select distinct
|
||||||
|
session_id,
|
||||||
|
max(created_at) max_dt
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and ${column} = {conversionStep:String}
|
||||||
|
and event_type = {eventType:UInt32}
|
||||||
|
group by 1),`;
|
||||||
|
|
||||||
|
const revenueEventQuery = `WITH events AS (
|
||||||
|
select
|
||||||
|
ed.session_id,
|
||||||
|
max(ed.created_at) max_dt,
|
||||||
|
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as value
|
||||||
|
from event_data ed
|
||||||
|
join (select event_id
|
||||||
|
from event_data
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||||
|
and string_value = {currency:String}) c
|
||||||
|
on c.event_id = ed.event_id
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and ${column} = {conversionStep:String}
|
||||||
|
and positionCaseInsensitive(ed.data_key, 'revenue') > 0
|
||||||
|
group by 1),`;
|
||||||
|
|
||||||
|
function getModelQuery(model: string) {
|
||||||
|
return model === 'firstClick'
|
||||||
|
? `\n
|
||||||
|
model AS (select e.session_id,
|
||||||
|
min(we.created_at) created_at
|
||||||
|
from events e
|
||||||
|
join website_event we
|
||||||
|
on we.session_id = e.session_id
|
||||||
|
group by e.session_id)`
|
||||||
|
: `\n
|
||||||
|
model AS (select e.session_id,
|
||||||
|
max(we.created_at) created_at
|
||||||
|
from events e
|
||||||
|
join website_event we
|
||||||
|
on we.session_id = e.session_id
|
||||||
|
where we.created_at < e.max_dt
|
||||||
|
group by e.session_id)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referrerRes = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
select we.referrer_domain name,
|
||||||
|
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||||
|
from events e
|
||||||
|
join model m
|
||||||
|
on m.session_id = e.session_id
|
||||||
|
join website_event we
|
||||||
|
on we.created_at = m.created_at
|
||||||
|
and we.session_id = m.session_id
|
||||||
|
${
|
||||||
|
currency
|
||||||
|
? ''
|
||||||
|
: `where referrer_domain != hostname
|
||||||
|
and we.referrer_domain != ''`
|
||||||
|
}
|
||||||
|
group by 1
|
||||||
|
order by 2 desc
|
||||||
|
limit 20
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const paidAdsres = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
select multiIf(gclid != '', 'Google', fbclid != '', 'Facebook', '') name,
|
||||||
|
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||||
|
from events e
|
||||||
|
join model m
|
||||||
|
on m.session_id = e.session_id
|
||||||
|
join website_event we
|
||||||
|
on we.created_at = m.created_at
|
||||||
|
and we.session_id = m.session_id
|
||||||
|
${currency ? '' : `WHERE name != ''`}
|
||||||
|
group by 1
|
||||||
|
order by 2 desc
|
||||||
|
limit 20
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceRes = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_source')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediumRes = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_medium')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const campaignRes = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_campaign')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentRes = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_content')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const termRes = await rawQuery(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_term')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalRes = await rawQuery(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
count(*) as "pageviews",
|
||||||
|
uniqExact(session_id) as "visitors",
|
||||||
|
uniqExact(visit_id) as "visits"
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and ${column} = {conversionStep:String}
|
||||||
|
and event_type = {eventType:UInt32}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
).then(result => result?.[0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
referrer: referrerRes,
|
||||||
|
paidAds: paidAdsres,
|
||||||
|
utm_source: sourceRes,
|
||||||
|
utm_medium: mediumRes,
|
||||||
|
utm_campaign: campaignRes,
|
||||||
|
utm_content: contentRes,
|
||||||
|
utm_term: termRes,
|
||||||
|
total: totalRes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickhouseQuery(
|
||||||
|
websiteId: string,
|
||||||
|
criteria: {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
model: string;
|
||||||
|
steps: { type: string; value: string }[];
|
||||||
|
currency: string;
|
||||||
|
},
|
||||||
|
): Promise<{
|
||||||
|
referrer: { name: string; value: number }[];
|
||||||
|
paidAds: { name: string; value: number }[];
|
||||||
|
utm_source: { name: string; value: number }[];
|
||||||
|
utm_medium: { name: string; value: number }[];
|
||||||
|
utm_campaign: { name: string; value: number }[];
|
||||||
|
utm_content: { name: string; value: number }[];
|
||||||
|
utm_term: { name: string; value: number }[];
|
||||||
|
total: { pageviews: number; visitors: number; visits: number };
|
||||||
|
}> {
|
||||||
|
const { startDate, endDate, model, steps, currency } = criteria;
|
||||||
|
const { rawQuery } = clickhouse;
|
||||||
|
const conversionStep = steps[0].value;
|
||||||
|
const eventType = steps[0].type === 'url' ? EVENT_TYPE.pageView : EVENT_TYPE.customEvent;
|
||||||
|
const column = steps[0].type === 'url' ? 'url_path' : 'event_name';
|
||||||
|
|
||||||
|
function getUTMQuery(utmColumn: string) {
|
||||||
|
return `
|
||||||
|
select
|
||||||
|
we.${utmColumn} name,
|
||||||
|
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||||
|
from events e
|
||||||
|
join model m
|
||||||
|
on m.session_id = e.session_id
|
||||||
|
join website_event we
|
||||||
|
on we.created_at = m.created_at
|
||||||
|
and we.session_id = m.session_id
|
||||||
|
${currency ? '' : `where we.${utmColumn} != ''`}
|
||||||
|
group by 1
|
||||||
|
order by name desc
|
||||||
|
limit 20`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventQuery = `WITH events AS (
|
||||||
|
select distinct
|
||||||
|
session_id,
|
||||||
|
max(created_at) max_dt
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and ${column} = {conversionStep:String}
|
||||||
|
and event_type = {eventType:UInt32}
|
||||||
|
group by 1),`;
|
||||||
|
|
||||||
|
const revenueEventQuery = `WITH events AS (
|
||||||
|
select
|
||||||
|
ed.session_id,
|
||||||
|
max(ed.created_at) max_dt,
|
||||||
|
sum(coalesce(toDecimal64(number_value, 2), toDecimal64(string_value, 2))) as value
|
||||||
|
from event_data ed
|
||||||
|
join (select event_id
|
||||||
|
from event_data
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||||
|
and string_value = {currency:String}) c
|
||||||
|
on c.event_id = ed.event_id
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and ${column} = {conversionStep:String}
|
||||||
|
and positionCaseInsensitive(ed.data_key, 'revenue') > 0
|
||||||
|
group by 1),`;
|
||||||
|
|
||||||
|
function getModelQuery(model: string) {
|
||||||
|
return model === 'firstClick'
|
||||||
|
? `\n
|
||||||
|
model AS (select e.session_id,
|
||||||
|
min(we.created_at) created_at
|
||||||
|
from events e
|
||||||
|
join website_event we
|
||||||
|
on we.session_id = e.session_id
|
||||||
|
group by e.session_id)`
|
||||||
|
: `\n
|
||||||
|
model AS (select e.session_id,
|
||||||
|
max(we.created_at) created_at
|
||||||
|
from events e
|
||||||
|
join website_event we
|
||||||
|
on we.session_id = e.session_id
|
||||||
|
where we.created_at < e.max_dt
|
||||||
|
group by e.session_id)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const referrerRes = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
select we.referrer_domain name,
|
||||||
|
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||||
|
from events e
|
||||||
|
join model m
|
||||||
|
on m.session_id = e.session_id
|
||||||
|
join website_event we
|
||||||
|
on we.created_at = m.created_at
|
||||||
|
and we.session_id = m.session_id
|
||||||
|
${
|
||||||
|
currency
|
||||||
|
? ''
|
||||||
|
: `where referrer_domain != hostname
|
||||||
|
and we.referrer_domain != ''`
|
||||||
|
}
|
||||||
|
group by 1
|
||||||
|
order by 2 desc
|
||||||
|
limit 20
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const paidAdsres = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
select multiIf(gclid != '', 'Google', fbclid != '', 'Facebook', '') name,
|
||||||
|
${currency ? 'sum(e.value)' : 'uniqExact(we.session_id)'} value
|
||||||
|
from events e
|
||||||
|
join model m
|
||||||
|
on m.session_id = e.session_id
|
||||||
|
join website_event we
|
||||||
|
on we.created_at = m.created_at
|
||||||
|
and we.session_id = m.session_id
|
||||||
|
${currency ? '' : `WHERE name != ''`}
|
||||||
|
group by 1
|
||||||
|
order by 2 desc
|
||||||
|
limit 20
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceRes = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_source')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const mediumRes = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_medium')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const campaignRes = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_campaign')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentRes = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_content')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const termRes = await rawQuery<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
${currency ? revenueEventQuery : eventQuery}
|
||||||
|
${getModelQuery(model)}
|
||||||
|
${getUTMQuery('utm_term')}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalRes = await rawQuery<{ pageviews: number; visitors: number; visits: number }>(
|
||||||
|
`
|
||||||
|
select
|
||||||
|
count(*) as "pageviews",
|
||||||
|
uniqExact(session_id) as "visitors",
|
||||||
|
uniqExact(visit_id) as "visits"
|
||||||
|
from website_event
|
||||||
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and ${column} = {conversionStep:String}
|
||||||
|
and event_type = {eventType:UInt32}
|
||||||
|
`,
|
||||||
|
{ websiteId, startDate, endDate, conversionStep, eventType, currency },
|
||||||
|
).then(result => result?.[0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
referrer: referrerRes,
|
||||||
|
paidAds: paidAdsres,
|
||||||
|
utm_source: sourceRes,
|
||||||
|
utm_medium: mediumRes,
|
||||||
|
utm_campaign: campaignRes,
|
||||||
|
utm_content: contentRes,
|
||||||
|
utm_term: termRes,
|
||||||
|
total: totalRes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -56,7 +56,9 @@ async function relationalQuery(
|
||||||
on we.event_id = ed.website_event_id
|
on we.event_id = ed.website_event_id
|
||||||
join (select website_event_id
|
join (select website_event_id
|
||||||
from event_data
|
from event_data
|
||||||
where data_key ${like} '%currency%'
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
and data_key ${like} '%currency%'
|
||||||
and string_value = {{currency}}) currency
|
and string_value = {{currency}}) currency
|
||||||
on currency.website_event_id = ed.website_event_id
|
on currency.website_event_id = ed.website_event_id
|
||||||
where ed.website_id = {{websiteId::uuid}}
|
where ed.website_id = {{websiteId::uuid}}
|
||||||
|
|
@ -80,7 +82,9 @@ async function relationalQuery(
|
||||||
on s.session_id = we.session_id
|
on s.session_id = we.session_id
|
||||||
join (select website_event_id
|
join (select website_event_id
|
||||||
from event_data
|
from event_data
|
||||||
where data_key ${like} '%currency%'
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
and data_key ${like} '%currency%'
|
||||||
and string_value = {{currency}}) currency
|
and string_value = {{currency}}) currency
|
||||||
on currency.website_event_id = ed.website_event_id
|
on currency.website_event_id = ed.website_event_id
|
||||||
where ed.website_id = {{websiteId::uuid}}
|
where ed.website_id = {{websiteId::uuid}}
|
||||||
|
|
@ -102,7 +106,9 @@ async function relationalQuery(
|
||||||
on we.event_id = ed.website_event_id
|
on we.event_id = ed.website_event_id
|
||||||
join (select website_event_id
|
join (select website_event_id
|
||||||
from event_data
|
from event_data
|
||||||
where data_key ${like} '%currency%'
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
and data_key ${like} '%currency%'
|
||||||
and string_value = {{currency}}) currency
|
and string_value = {{currency}}) currency
|
||||||
on currency.website_event_id = ed.website_event_id
|
on currency.website_event_id = ed.website_event_id
|
||||||
where ed.website_id = {{websiteId::uuid}}
|
where ed.website_id = {{websiteId::uuid}}
|
||||||
|
|
@ -124,7 +130,9 @@ async function relationalQuery(
|
||||||
on we.event_id = ed.website_event_id
|
on we.event_id = ed.website_event_id
|
||||||
join (select website_event_id, string_value as currency
|
join (select website_event_id, string_value as currency
|
||||||
from event_data
|
from event_data
|
||||||
where data_key ${like} '%currency%') c
|
where website_id = {{websiteId::uuid}}
|
||||||
|
and created_at between {{startDate}} and {{endDate}}
|
||||||
|
and data_key ${like} '%currency%') c
|
||||||
on c.website_event_id = ed.website_event_id
|
on c.website_event_id = ed.website_event_id
|
||||||
where ed.website_id = {{websiteId::uuid}}
|
where ed.website_id = {{websiteId::uuid}}
|
||||||
and ed.created_at between {{startDate}} and {{endDate}}
|
and ed.created_at between {{startDate}} and {{endDate}}
|
||||||
|
|
@ -176,7 +184,9 @@ async function clickhouseQuery(
|
||||||
from event_data
|
from event_data
|
||||||
join (select event_id
|
join (select event_id
|
||||||
from event_data
|
from event_data
|
||||||
where positionCaseInsensitive(data_key, 'currency') > 0
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||||
and string_value = {currency:String}) currency
|
and string_value = {currency:String}) currency
|
||||||
on currency.event_id = event_data.event_id
|
on currency.event_id = event_data.event_id
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
|
|
@ -201,7 +211,9 @@ async function clickhouseQuery(
|
||||||
from event_data ed
|
from event_data ed
|
||||||
join (select event_id
|
join (select event_id
|
||||||
from event_data
|
from event_data
|
||||||
where positionCaseInsensitive(data_key, 'currency') > 0
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||||
and string_value = {currency:String}) c
|
and string_value = {currency:String}) c
|
||||||
on c.event_id = ed.event_id
|
on c.event_id = ed.event_id
|
||||||
join (select distinct website_id, session_id, country
|
join (select distinct website_id, session_id, country
|
||||||
|
|
@ -231,7 +243,9 @@ async function clickhouseQuery(
|
||||||
from event_data
|
from event_data
|
||||||
join (select event_id
|
join (select event_id
|
||||||
from event_data
|
from event_data
|
||||||
where positionCaseInsensitive(data_key, 'currency') > 0
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and positionCaseInsensitive(data_key, 'currency') > 0
|
||||||
and string_value = {currency:String}) currency
|
and string_value = {currency:String}) currency
|
||||||
on currency.event_id = event_data.event_id
|
on currency.event_id = event_data.event_id
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
|
|
@ -259,7 +273,9 @@ async function clickhouseQuery(
|
||||||
from event_data ed
|
from event_data ed
|
||||||
join (select event_id, string_value as currency
|
join (select event_id, string_value as currency
|
||||||
from event_data
|
from event_data
|
||||||
where positionCaseInsensitive(data_key, 'currency') > 0) c
|
where website_id = {websiteId:UUID}
|
||||||
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
and positionCaseInsensitive(data_key, 'currency') > 0) c
|
||||||
on c.event_id = ed.event_id
|
on c.event_id = ed.event_id
|
||||||
where website_id = {websiteId:UUID}
|
where website_id = {websiteId:UUID}
|
||||||
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue