Skip to content
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
75 changes: 70 additions & 5 deletions lib/mave_core/analytics/overview.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule MaveCore.Analytics.Overview do
@periods ~w(24h 7d 30d 12m)
@default_period "30d"
@min_watch_ms 1_000
@reach_percents [0, 25, 50, 75, 95]
@clickhouse_identifier ~r/^[A-Za-z_][A-Za-z0-9_]*$/

def periods, do: @periods
Expand All @@ -30,8 +31,16 @@ defmodule MaveCore.Analytics.Overview do
previous: %{views: 100, ...},
series: [%{start: ~U[...], views: 4, watch_ms: 120_000}, ...],
devices: [%{device: "desktop", views: 80}, ...],
browsers: [%{browser: "Chrome", views: 60}, ...]
browsers: [%{browser: "Chrome", views: 60}, ...],
operating_systems: [%{os: "Mac OS", views: 50}, ...],
websites: [%{website: "example.com", views: 70}, ...],
reach: [%{percent: 0, share: 1.0}, %{percent: 25, share: 0.8}, ...],
hours: %{{1, 9} => 12, ...}
}}

`reach` is the share of views that got at least that far into the video;
95% counts as finished. `hours` maps `{weekday, hour}` (Monday is 1) to
views started in that hour.
"""
def space_overview(space_hash, period, now \\ DateTime.utc_now())

Expand Down Expand Up @@ -98,6 +107,31 @@ defmodule MaveCore.Analytics.Overview do
#{dimension_sql("device", "device", window)}
UNION ALL
#{dimension_sql("browser", "browser", window)}
UNION ALL
#{dimension_sql("os", "os", window)}
UNION ALL
#{dimension_sql("website", "website", window)}
UNION ALL
SELECT
'reach' AS kind,
toString(percent) AS key,
toUInt64(countIf(completion * 100 >= percent)) AS views,
toInt64(0) AS watch_ms,
toFloat64(0) AS value
FROM views
ARRAY JOIN #{reach_sql()} AS percent
WHERE started_at >= #{datetime_literal(window.from)} AND video_duration > 0
GROUP BY percent
UNION ALL
SELECT
'hour' AS kind,
concat(toString(toDayOfWeek(started_at, 0, 'UTC')), '-', toString(toHour(started_at, 'UTC'))) AS key,
toUInt64(count()) AS views,
toInt64(0) AS watch_ms,
toFloat64(0) AS value
FROM views
WHERE started_at >= #{datetime_literal(window.from)}
GROUP BY key
"""

query_clickhouse(sql, {:error, :analytics_unavailable}, fn rows ->
Expand All @@ -118,6 +152,8 @@ defmodule MaveCore.Analytics.Overview do
duration,
device,
browser,
os,
source_url,
leadInFrame(timestamp) OVER (
PARTITION BY embed_hash, session_id
ORDER BY timestamp
Expand Down Expand Up @@ -146,8 +182,10 @@ defmodule MaveCore.Analytics.Overview do
max(duration) AS video_duration,
max(video_time) AS furthest,
if(video_duration > 0, least(1, furthest / video_duration), 0) AS completion,
nullIf(argMin(device, timestamp), '') AS device,
nullIf(argMin(browser, timestamp), '') AS browser
nullIf(lower(argMin(device, timestamp)), '') AS device,
nullIf(argMin(browser, timestamp), '') AS browser,
nullIf(argMin(os, timestamp), '') AS os,
nullIf(domainWithoutWWW(argMin(source_url, timestamp)), '') AS website
FROM ordered
GROUP BY embed_hash, session_id
HAVING watch_ms >= #{@min_watch_ms}
Expand Down Expand Up @@ -193,10 +231,33 @@ defmodule MaveCore.Analytics.Overview do
%{start: start, views: to_int(views), watch_ms: to_int(watch_ms)}
end),
devices: dimension_rows(grouped, "device", :device),
browsers: dimension_rows(grouped, "browser", :browser)
browsers: dimension_rows(grouped, "browser", :browser),
operating_systems: dimension_rows(grouped, "os", :os),
websites: dimension_rows(grouped, "website", :website),
reach: reach(Map.get(grouped, "reach", [])),
hours: hours(Map.get(grouped, "hour", []))
}
end

defp reach(rows) do
counts = Map.new(rows, fn [percent, views, _watch_ms, _value] -> {percent, to_int(views)} end)
started = Map.get(counts, "0", 0)

Enum.map(@reach_percents, fn percent ->
views = Map.get(counts, Integer.to_string(percent), 0)
%{percent: percent, share: if(started > 0, do: views / started, else: 0.0)}
end)
end

defp hours(rows) do
Map.new(rows, fn [key, views, _watch_ms, _value] ->
[weekday, hour] = key |> String.split("-") |> Enum.map(&String.to_integer/1)
{{weekday, hour}, to_int(views)}
end)
end

defp reach_sql, do: "[" <> Enum.map_join(@reach_percents, ", ", &Integer.to_string/1) <> "]"

defp totals([views, watch_ms, completion]) do
views = to_int(views)
watch_ms = to_int(watch_ms)
Expand Down Expand Up @@ -229,7 +290,11 @@ defmodule MaveCore.Analytics.Overview do
previous: totals([0, 0, 0.0]),
series: Enum.map(window.buckets, &%{start: &1, views: 0, watch_ms: 0}),
devices: [],
browsers: []
browsers: [],
operating_systems: [],
websites: [],
reach: Enum.map(@reach_percents, &%{percent: &1, share: 0.0}),
hours: %{}
}
end

Expand Down
106 changes: 65 additions & 41 deletions lib/mave_core_web/live/dashboard/data/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do
|> assign(:settings_open, false)
|> assign(:route_scope, :unscoped)
|> assign(:period, Overview.default_period())
|> assign(:show_all_devices, false)
|> assign(:show_all_browsers, false)
|> assign(:expanded_sources, MapSet.new())
|> assign(:data_access_notice, nil)
|> assign(:data_access_help, nil)
|> assign_async_defaults()}
Expand Down Expand Up @@ -84,8 +83,7 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do
{:noreply,
socket
|> assign(:overview, AsyncResult.ok(socket.assigns.overview, overview))
|> assign(:show_all_devices, false)
|> assign(:show_all_browsers, false)}
|> assign(:expanded_sources, MapSet.new())}
end

def handle_async(:fetch_overview, _result, socket) do
Expand All @@ -110,12 +108,16 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do
)}
end

def handle_event("toggle_show_all_devices", _, socket) do
{:noreply, assign(socket, :show_all_devices, !socket.assigns.show_all_devices)}
end
def handle_event("toggle_source", %{"source" => source}, socket)
when source in ["device", "browser", "os", "website"] do
expanded = socket.assigns.expanded_sources

expanded =
if MapSet.member?(expanded, source),
do: MapSet.delete(expanded, source),
else: MapSet.put(expanded, source)

def handle_event("toggle_show_all_browsers", _, socket) do
{:noreply, assign(socket, :show_all_browsers, !socket.assigns.show_all_browsers)}
{:noreply, assign(socket, :expanded_sources, expanded)}
end

def render(assigns) do
Expand Down Expand Up @@ -189,6 +191,17 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do

<.divider />

<div class="mb-6">
<.subtitle label={gettext("Engagement")} icon="hero-play-circle" />
</div>

<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
<OverviewComponents.reach overview={@overview} />
<OverviewComponents.hours overview={@overview} />
</div>

<.divider />

<div class="mb-6">
<.subtitle label={gettext("Sources")} icon="hero-globe-alt" />
</div>
Expand All @@ -197,22 +210,36 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do
<.source_panel
title={gettext("Device")}
assign={overview_field(@overview, :devices)}
rows={visible_devices(overview_field(@overview, :devices), @show_all_devices)}
toggle="toggle_show_all_devices"
expanded={@show_all_devices}
expanded={MapSet.member?(@expanded_sources, "device")}
key_name={:device}
empty_icon="device"
/>

<.source_panel
title={gettext("Browser")}
assign={overview_field(@overview, :browsers)}
rows={visible_browsers(overview_field(@overview, :browsers), @show_all_browsers)}
toggle="toggle_show_all_browsers"
expanded={@show_all_browsers}
expanded={MapSet.member?(@expanded_sources, "browser")}
key_name={:browser}
empty_icon="browser"
/>

<.source_panel
title={gettext("Operating system")}
assign={overview_field(@overview, :operating_systems)}
expanded={MapSet.member?(@expanded_sources, "os")}
key_name={:os}
empty_icon="os"
capitalize={false}
/>

<.source_panel
title={gettext("Websites")}
assign={overview_field(@overview, :websites)}
expanded={MapSet.member?(@expanded_sources, "website")}
key_name={:website}
empty_icon="website"
capitalize={false}
/>
</div>
<% end %>
</div>
Expand Down Expand Up @@ -294,11 +321,10 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do

attr :title, :string, required: true
attr :assign, :any, required: true
attr :rows, :list, required: true
attr :toggle, :string, required: true
attr :expanded, :boolean, required: true
attr :key_name, :atom, required: true
attr :empty_icon, :string, required: true
attr :capitalize, :boolean, default: true

defp source_panel(assigns) do
~H"""
Expand Down Expand Up @@ -374,23 +400,25 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do
<div class="text-sm font-medium">{gettext("No data yet")}</div>
</div>
<% else %>
<%= for row <- @rows do %>
<div class="flex px-3 py-2.5 border-b border-stone-100 last:border-none hover:bg-stone-50 active:bg-blue-50">
<div class="flex-grow text-sm text-stone-400">
{row |> Map.fetch!(@key_name) |> to_string() |> String.capitalize()}
<%= for row <- visible_rows(rows, @key_name, @expanded) do %>
<div class="flex gap-3 px-3 py-2.5 border-b border-stone-100 last:border-none hover:bg-stone-50 active:bg-blue-50">
<div class="flex-grow min-w-0 truncate text-sm text-stone-400">
{source_label(Map.fetch!(row, @key_name), @capitalize)}
</div>
<div class="text-sm text-stone-400">{format_number(row.views)}</div>
<div class="text-sm text-stone-400 tabular-nums">{format_number(row.views)}</div>
</div>
<% end %>
<%= if length(rows) < length(@assign.result || []) do %>
<div
class="px-3 py-2.5 bg-stone-50 border-t border-stone-100 flex justify-center cursor-pointer hover:bg-stone-100"
phx-click={@toggle}
<%= if length(visible_rows(rows, @key_name, false)) < length(visible_rows(rows, @key_name, true)) do %>
<button
type="button"
class="w-full px-3 py-2.5 bg-stone-50 border-t border-stone-100 flex justify-center cursor-pointer hover:bg-stone-100"
phx-click="toggle_source"
phx-value-source={@empty_icon}
>
<span class="text-sm text-stone-500 text-opacity-60">
{if @expanded, do: gettext("Show less"), else: gettext("Show more")}
</span>
</div>
</button>
<% end %>
<% end %>
</.video_data_section>
Expand Down Expand Up @@ -428,20 +456,17 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do

defp format_number(number), do: Formatter.format_number(number)

defp visible_devices(%AsyncResult{ok?: true, result: devices}, true), do: devices
defp visible_devices(%AsyncResult{ok?: true, result: devices}, false), do: Enum.take(devices, 5)
defp visible_devices(_, _), do: []

defp visible_browsers(%AsyncResult{ok?: true, result: browsers}, expanded?) do
filtered =
Enum.filter(browsers, fn %{browser: type, views: count} ->
not (type in ["ie", "firefox", "brave", "opera"] and count == 0)
end)

if expanded?, do: filtered, else: Enum.take(filtered, 5)
defp visible_rows(rows, :browser, expanded?) do
rows
|> Enum.reject(&(&1.browser in ["ie", "firefox", "brave", "opera"] and &1.views == 0))
|> visible_rows(nil, expanded?)
end

defp visible_browsers(_, _), do: []
defp visible_rows(rows, _key, true), do: rows
defp visible_rows(rows, _key, false), do: Enum.take(rows, 5)

defp source_label(value, true), do: value |> to_string() |> String.capitalize()
defp source_label(value, false), do: to_string(value)

defp assign_async_defaults(socket) do
socket
Expand All @@ -456,8 +481,7 @@ defmodule MaveCoreWeb.Dashboard.Data.Index do
AsyncResult.ok(AsyncResult.loading(), Overview.empty(socket.assigns.period))
)
|> assign(:top_videos, AsyncResult.ok(AsyncResult.loading(), []))
|> assign(:show_all_devices, false)
|> assign(:show_all_browsers, false)
|> assign(:expanded_sources, MapSet.new())
end

defp data_access_notice(reason) do
Expand Down
Loading
Loading