| defmodule Plausible.Stats.Sparkline do |
| @moduledoc """ |
| Module used to generate sparkline overview data for sites, including consolidated views. |
| The sparkline graphs are used in site index and in CRM. |
| """ |
|
|
| alias Plausible.{Site, Stats} |
| alias Plausible.Stats.QueryBuilder |
|
|
| @spec parallel_overview([Site.t()], NaiveDateTime.t()) :: map() |
| def parallel_overview(sites, now \\ NaiveDateTime.utc_now()) when is_list(sites) do |
| Task.async_stream( |
| sites, |
| fn site -> |
| {site.domain, safe_overview_24h(site, now)} |
| end, |
| timeout: 5000, |
| on_timeout: :kill_task, |
| max_concurrency: 4 |
| ) |
| |> Enum.reduce(%{}, fn |
| {:ok, {domain, {:ok, stats}}}, acc -> |
| Map.put(acc, domain, stats) |
|
|
| _, acc -> |
| acc |
| end) |
| end |
|
|
| @spec overview_24h(Site.t(), NaiveDateTime.t()) :: map() |
| def overview_24h(view_or_site, now \\ NaiveDateTime.utc_now()) do |
| stats = query_24h_stats(view_or_site, now) |
| intervals = query_24h_intervals(view_or_site, now) |
|
|
| Map.merge(stats, intervals) |
| end |
|
|
| @spec safe_overview_24h(Site.t(), NaiveDateTime.t()) :: |
| {:ok, map()} | {:error, :inaccessible} | {:error, :not_found} |
|
|
| def safe_overview_24h(site, now \\ NaiveDateTime.utc_now()) |
| def safe_overview_24h(nil, _now), do: {:error, :not_found} |
|
|
| def safe_overview_24h(%Site{} = site, now) do |
| try do |
| {:ok, overview_24h(site, now)} |
| catch |
| kind, reason -> |
| message = Exception.format(kind, reason, __STACKTRACE__) |
|
|
| Sentry.capture_message( |
| "Could not render overview 24h - site will be kept in loading indefinitely", |
| extra: %{ |
| error: message, |
| site: site.domain, |
| comment: "Probing error volume" |
| } |
| ) |
|
|
| {:error, :inaccessible} |
| end |
| end |
|
|
| def empty_24h_intervals(now \\ NaiveDateTime.utc_now()) do |
| first = NaiveDateTime.add(now, -24, :hour) |
| {:ok, time} = Time.new(first.hour, 0, 0) |
| first = NaiveDateTime.new!(NaiveDateTime.to_date(first), time) |
|
|
| for offset <- 0..24 do |
| %{interval: to_string(NaiveDateTime.add(first, offset, :hour)), visitors: 0} |
| end |
| end |
|
|
| defp query_24h_stats(view_or_site, now) do |
| if Plausible.Sites.regular?(view_or_site) do |
| query_24h_stats_regular(view_or_site, now) |
| else |
| query_24h_stats_consolidated(view_or_site, now) |
| end |
| end |
|
|
| defp query_24h_stats_regular(site, now) do |
| stats_query = |
| QueryBuilder.build!(site, |
| now: DateTime.from_naive!(now, "Etc/UTC"), |
| input_date_range: :"24h", |
| metrics: [:visitors], |
| include: [compare: :previous_period] |
| ) |
|
|
| %Stats.QueryResult{ |
| results: [ |
| %{ |
| metrics: [visitors], |
| comparison: %{ |
| change: [visitors_change] |
| } |
| } |
| ] |
| } = Stats.query(site, stats_query) |
|
|
| %{ |
| visitors: visitors, |
| visitors_change: visitors_change |
| } |
| end |
|
|
| defp query_24h_stats_consolidated(view, now) do |
| stats_query = |
| QueryBuilder.build!(view, |
| now: DateTime.from_naive!(now, "Etc/UTC"), |
| input_date_range: :"24h", |
| metrics: [:visitors, :visits, :pageviews, :views_per_visit], |
| include: [compare: :previous_period] |
| ) |
|
|
| %Stats.QueryResult{ |
| results: [ |
| %{ |
| metrics: [visitors, visits, pageviews, views_per_visit], |
| comparison: %{ |
| change: [visitors_change, visits_change, pageviews_change, views_per_visit_change] |
| } |
| } |
| ] |
| } = Stats.query(view, stats_query) |
|
|
| %{ |
| visitors: visitors, |
| visits: visits, |
| pageviews: pageviews, |
| views_per_visit: views_per_visit, |
| visitors_change: visitors_change, |
| visits_change: visits_change, |
| pageviews_change: pageviews_change, |
| views_per_visit_change: views_per_visit_change |
| } |
| end |
|
|
| defp query_24h_intervals(view_or_site, now) do |
| graph_query = |
| QueryBuilder.build!(view_or_site, |
| now: DateTime.from_naive!(now, "Etc/UTC"), |
| metrics: [:visitors], |
| input_date_range: :"24h", |
| dimensions: ["time:hour"], |
| order_by: [{"time:hour", :asc}], |
| include: [time_labels: true] |
| ) |
|
|
| %Stats.QueryResult{results: results, meta: %{values: [time_labels: time_labels]}} = |
| Stats.query(view_or_site, graph_query) |
|
|
| visitors_by_timestamp = |
| Map.new(results, fn %{dimensions: [timestamp], metrics: [visitors]} -> |
| {timestamp, visitors} |
| end) |
|
|
| %{ |
| intervals: |
| Enum.map(time_labels, fn timestamp -> |
| %{interval: timestamp, visitors: Map.get(visitors_by_timestamp, timestamp, 0)} |
| end) |
| } |
| end |
| end |
|
|