Experimental session count (#3786)

* WIP

* Allow `experimetnal_session_count` request serialization

* Extend `Plausible.Stats.Query` with `experimental_session_count` flag

* Add `FunWithFlags` actor implementation for `Site`

* Change the way sessions are retrieved

* Remove redundant test

* Format

* Update the test

---------

Co-authored-by: Uku Taht <uku.taht@gmail.com>
This commit is contained in:
hq1 2024-02-15 12:21:07 +01:00 committed by GitHub
parent 672d682e95
commit 926de4dd10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 60 additions and 25 deletions

View File

@ -1,4 +1,4 @@
import {formatISO} from './util/date'
import { formatISO } from './util/date'
let abortController = new AbortController()
let SHARED_LINK_AUTH = null
@ -36,14 +36,15 @@ function serializeFilters(filters) {
return JSON.stringify(cleaned)
}
export function serializeQuery(query, extraQuery=[]) {
export function serializeQuery(query, extraQuery = []) {
const queryObj = {}
if (query.period) { queryObj.period = query.period }
if (query.date) { queryObj.date = formatISO(query.date) }
if (query.from) { queryObj.from = formatISO(query.from) }
if (query.to) { queryObj.to = formatISO(query.to) }
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
if (query.with_imported) { queryObj.with_imported = query.with_imported }
if (query.period) { queryObj.period = query.period }
if (query.date) { queryObj.date = formatISO(query.date) }
if (query.from) { queryObj.from = formatISO(query.from) }
if (query.to) { queryObj.to = formatISO(query.to) }
if (query.filters) { queryObj.filters = serializeFilters(query.filters) }
if (query.experimental_session_count) { queryObj.experimental_session_count = query.experimental_session_count }
if (query.with_imported) { queryObj.with_imported = query.with_imported }
if (SHARED_LINK_AUTH) { queryObj.auth = SHARED_LINK_AUTH }
if (query.comparison) {
@ -58,11 +59,11 @@ export function serializeQuery(query, extraQuery=[]) {
return '?' + serialize(queryObj)
}
export function get(url, query={}, ...extraQuery) {
const headers = SHARED_LINK_AUTH ? {'X-Shared-Link-Auth': SHARED_LINK_AUTH} : {}
export function get(url, query = {}, ...extraQuery) {
const headers = SHARED_LINK_AUTH ? { 'X-Shared-Link-Auth': SHARED_LINK_AUTH } : {}
url = url + serializeQuery(query, extraQuery)
return fetch(url, {signal: abortController.signal, headers: headers})
.then( response => {
return fetch(url, { signal: abortController.signal, headers: headers })
.then(response => {
if (!response.ok) {
return response.json().then((msg) => {
throw new ApiError(msg.error, msg)

View File

@ -39,6 +39,7 @@ export function parseQuery(querystring, site) {
to: q.get('to') ? dayjs.utc(q.get('to')) : undefined,
match_day_of_week: matchDayOfWeek == 'true',
with_imported: q.get('with_imported') ? q.get('with_imported') === 'true' : true,
experimental_session_count: q.get('experimental_session_count'),
filters: {
'goal': q.get('goal'),
'props': JSON.parse(q.get('props')),

View File

@ -242,3 +242,9 @@ defmodule Plausible.Site do
end
end
end
defimpl FunWithFlags.Actor, for: Plausible.Site do
def id(%{domain: domain}) do
"site:#{domain}"
end
end

View File

@ -132,14 +132,18 @@ defmodule Plausible.Stats.Base do
}
def query_sessions(site, query) do
{first_datetime, last_datetime} = utc_boundaries(query, site)
{first_datetime, last_datetime} =
utc_boundaries(query, site)
q = from(s in "sessions_v2", where: s.site_id == ^site.id)
sessions_q =
from(
s in "sessions_v2",
where: s.site_id == ^site.id,
where: s.start >= ^first_datetime and s.start < ^last_datetime
)
if FunWithFlags.enabled?(:experimental_session_count, for: site) or
query.experimental_session_count? do
from s in q, where: s.timestamp >= ^first_datetime and s.start < ^last_datetime
else
from s in q, where: s.start >= ^first_datetime and s.start < ^last_datetime
end
on_full_build do
sessions_q = Plausible.Stats.Sampling.add_query_hint(sessions_q, query)

View File

@ -8,7 +8,8 @@ defmodule Plausible.Stats.Query do
sample_threshold: 20_000_000,
imported_data_requested: false,
include_imported: false,
now: nil
now: nil,
experimental_session_count?: false
require OpenTelemetry.Tracer, as: Tracer
alias Plausible.Stats.{Filters, Interval}
@ -21,6 +22,7 @@ defmodule Plausible.Stats.Query do
query =
__MODULE__
|> struct!(now: now)
|> put_experimental_session_count(params)
|> put_period(site, params)
|> put_interval(params)
|> put_parsed_filters(params)
@ -34,6 +36,14 @@ defmodule Plausible.Stats.Query do
query
end
defp put_experimental_session_count(query, params) do
if Map.get(params, "experimental_session_count") == "true" do
struct!(query, experimental_session_count?: true)
else
query
end
end
defp put_period(query, site, %{"period" => "realtime"}) do
date = today(site.timezone)

View File

@ -40,19 +40,32 @@ defmodule PlausibleWeb.Api.StatsController.TopStatsTest do
} in res["top_stats"]
end
test "counts total visits", %{conn: conn, site: site} do
test "experimental session count", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, user_id: @user_id, timestamp: ~N[2021-01-01 00:00:00]),
build(:pageview, user_id: 3421, timestamp: ~N[2020-12-31 23:30:00]),
build(:pageview, user_id: @user_id, timestamp: ~N[2020-12-31 23:59:00]),
build(:pageview, user_id: @user_id, timestamp: ~N[2021-01-01 00:01:00]),
build(:pageview, user_id: @user_id, timestamp: ~N[2021-01-01 10:00:00]),
build(:pageview, timestamp: ~N[2021-01-01 15:00:00])
build(:pageview, user_id: 2, timestamp: ~N[2020-12-31 23:59:00]),
build(:pageview, user_id: 2, timestamp: ~N[2021-01-01 00:01:00]),
build(:pageview, user_id: 617_235, timestamp: ~N[2021-01-03 00:00:00])
])
conn = get(conn, "/api/stats/#{site.domain}/top-stats?period=day&date=2021-01-01")
conn =
get(
conn,
"/api/stats/#{site.domain}/top-stats?period=day&date=2021-01-01&experimental_session_count=true"
)
res = json_response(conn, 200)
assert %{"name" => "Total visits", "value" => 3} in res["top_stats"]
assert res["top_stats"] == [
%{"name" => "Unique visitors", "value" => 2},
%{"name" => "Total visits", "value" => 2},
%{"name" => "Total pageviews", "value" => 2},
%{"name" => "Views per visit", "value" => 2.0},
%{"name" => "Bounce rate", "value" => 0},
%{"name" => "Visit duration", "value" => 120}
]
end
test "counts pages per visit", %{conn: conn, site: site} do