mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Add sessions
This commit is contained in:
parent
77087b7655
commit
f7d752988a
@ -45,6 +45,9 @@ config :plausible,
|
||||
paddle_api: Plausible.Billing.PaddleApi,
|
||||
google_api: Plausible.Google.Api
|
||||
|
||||
config :plausible,
|
||||
session_timeout: 1000 * 60 * 30 # 30 minutes
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
@ -26,3 +26,6 @@ config :plausible, Plausible.Mailer,
|
||||
config :plausible,
|
||||
paddle_api: Plausible.PaddleApi.Mock,
|
||||
google_api: Plausible.Google.Api.Mock
|
||||
|
||||
config :plausible,
|
||||
session_timeout: 0
|
||||
|
@ -11,6 +11,7 @@ defmodule Plausible.Application do
|
||||
|
||||
opts = [strategy: :one_for_one, name: Plausible.Supervisor]
|
||||
{:ok, _} = Logger.add_backend(Sentry.LoggerBackend)
|
||||
Application.put_env(:plausible, :server_start, Timex.now())
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
|
71
lib/plausible/ingest/session.ex
Normal file
71
lib/plausible/ingest/session.ex
Normal file
@ -0,0 +1,71 @@
|
||||
defmodule Plausible.Ingest.Session do
|
||||
use GenServer
|
||||
use Plausible.Repo
|
||||
|
||||
@session_timeout Application.get_env(:plausible, :session_timeout)
|
||||
|
||||
def on_event(event) do
|
||||
user_session = :global.whereis_name(event.user_id)
|
||||
|
||||
if is_pid(user_session) do
|
||||
GenServer.cast(user_session, {:on_event, event})
|
||||
else
|
||||
GenServer.start_link(__MODULE__, event, name: {:global, event.user_id})
|
||||
end
|
||||
end
|
||||
|
||||
def on_unload(user_id, timestamp) do
|
||||
user_session = :global.whereis_name(user_id)
|
||||
|
||||
if is_pid(user_session) do
|
||||
GenServer.cast(user_session, {:on_unload, timestamp})
|
||||
end
|
||||
end
|
||||
|
||||
def init(event) do
|
||||
timer = Process.send_after(self(), :finalize, @session_timeout)
|
||||
{:ok, %{first_event: event, timer: timer, is_bounce: true, last_unload: nil}}
|
||||
end
|
||||
|
||||
def handle_cast({:on_event, _event}, state) do
|
||||
Process.cancel_timer(state[:timer])
|
||||
new_timer = Process.send_after(self(), :finalize, @session_timeout)
|
||||
{:noreply, %{state | timer: new_timer, is_bounce: false, last_unload: nil}}
|
||||
end
|
||||
|
||||
def handle_cast({:on_unload, timestamp}, state) do
|
||||
{:noreply, %{state | last_unload: timestamp}}
|
||||
end
|
||||
|
||||
def handle_info(:finalize, state) do
|
||||
event = state[:first_event]
|
||||
|
||||
if !is_potential_leftover?(event) do
|
||||
length = if state[:last_unload] do
|
||||
Timex.diff(state[:last_unload], event.timestamp, :seconds)
|
||||
end
|
||||
|
||||
Plausible.Session.changeset(%Plausible.Session{}, %{
|
||||
hostname: event.hostname,
|
||||
user_id: event.user_id,
|
||||
new_visitor: event.new_visitor,
|
||||
is_bounce: state[:is_bounce],
|
||||
length: length,
|
||||
referrer: event.referrer,
|
||||
referrer_source: event.referrer_source,
|
||||
country_code: event.country_code,
|
||||
operating_system: event.operating_system,
|
||||
browser: event.browser
|
||||
}) |> Repo.insert!
|
||||
end
|
||||
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
defp is_potential_leftover?(%{new_visitor: true}), do: false
|
||||
defp is_potential_leftover?(%{timestamp: timestamp}) do
|
||||
server_start_time = Application.get_env(:plausible, :server_start)
|
||||
Timex.diff(timestamp, server_start_time, :milliseconds) < @session_timeout
|
||||
end
|
||||
|
||||
end
|
28
lib/plausible/session/schema.ex
Normal file
28
lib/plausible/session/schema.ex
Normal file
@ -0,0 +1,28 @@
|
||||
defmodule Plausible.Session do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "sessions" do
|
||||
field :hostname, :string
|
||||
field :new_visitor, :boolean
|
||||
field :user_id, :binary_id
|
||||
|
||||
field :length, :integer
|
||||
field :is_bounce, :boolean
|
||||
|
||||
field :referrer, :string
|
||||
field :referrer_source, :string
|
||||
field :country_code, :string
|
||||
field :screen_size, :string
|
||||
field :operating_system, :string
|
||||
field :browser, :string
|
||||
|
||||
timestamps(inserted_at: :timestamp, updated_at: false)
|
||||
end
|
||||
|
||||
def changeset(session, attrs) do
|
||||
session
|
||||
|> cast(attrs, [:hostname, :referrer, :new_visitor, :user_id, :length, :is_bounce, :operating_system, :browser, :referrer_source, :country_code, :screen_size])
|
||||
|> validate_required([:hostname, :new_visitor, :user_id, :is_bounce])
|
||||
end
|
||||
end
|
@ -6,7 +6,10 @@ defmodule PlausibleWeb.Api.ExternalController do
|
||||
params = parse_body(conn)
|
||||
|
||||
case create_event(conn, params) do
|
||||
{:ok, _event} ->
|
||||
{:ok, nil} ->
|
||||
conn |> send_resp(202, "")
|
||||
{:ok, event} ->
|
||||
Plausible.Ingest.Session.on_event(event)
|
||||
conn |> send_resp(202, "")
|
||||
{:error, changeset} ->
|
||||
request = Sentry.Plug.build_request_interface_data(conn, [])
|
||||
|
@ -17,7 +17,7 @@ defmodule PlausibleWeb.Router do
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["application/json"]
|
||||
plug :accepts, ["json"]
|
||||
plug :fetch_session
|
||||
plug PlausibleWeb.AuthPlug
|
||||
end
|
||||
|
23
priv/repo/migrations/20191128082207_add_sessions.exs
Normal file
23
priv/repo/migrations/20191128082207_add_sessions.exs
Normal file
@ -0,0 +1,23 @@
|
||||
defmodule Plausible.Repo.Migrations.AddSessions do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:sessions) do
|
||||
add :hostname, :text, null: false
|
||||
add :new_visitor, :boolean, null: false
|
||||
add :user_id, :binary_id, null: false
|
||||
|
||||
add :is_bounce, :boolean, null: false
|
||||
add :length, :integer
|
||||
|
||||
add :referrer, :string
|
||||
add :referrer_source, :string
|
||||
add :country_code, :string
|
||||
add :screen_size, :string
|
||||
add :operating_system, :string
|
||||
add :browser, :string
|
||||
|
||||
timestamps(inserted_at: :timestamp, updated_at: false)
|
||||
end
|
||||
end
|
||||
end
|
69
test/plausible/ingest/session_test.exs
Normal file
69
test/plausible/ingest/session_test.exs
Normal file
@ -0,0 +1,69 @@
|
||||
defmodule Plausible.Ingest.SessionTest do
|
||||
use Plausible.DataCase
|
||||
alias Plausible.Ingest
|
||||
|
||||
defp capture_session(user_id) do
|
||||
session_pid = :global.whereis_name(user_id)
|
||||
Process.monitor(session_pid)
|
||||
|
||||
assert_receive({:DOWN, session_pid, :process, _, :normal})
|
||||
|
||||
Repo.one(Plausible.Session)
|
||||
end
|
||||
|
||||
describe "on_event/1" do
|
||||
test "starts a new session if there is no session for user id" do
|
||||
pageview = insert(:pageview)
|
||||
|
||||
refute is_pid(:global.whereis_name(pageview.user_id))
|
||||
|
||||
Ingest.Session.on_event(pageview)
|
||||
|
||||
assert is_pid(:global.whereis_name(pageview.user_id))
|
||||
end
|
||||
|
||||
test "copies event data to session" do
|
||||
pageview = insert(:pageview)
|
||||
|
||||
Ingest.Session.on_event(pageview)
|
||||
|
||||
session = capture_session(pageview.user_id)
|
||||
|
||||
assert session.user_id == pageview.user_id
|
||||
assert session.new_visitor == pageview.new_visitor
|
||||
end
|
||||
|
||||
test "inserts bounced session when timeout fires after one pageview" do
|
||||
pageview = insert(:pageview)
|
||||
|
||||
Ingest.Session.on_event(pageview)
|
||||
|
||||
session = capture_session(pageview.user_id)
|
||||
assert session.is_bounce
|
||||
end
|
||||
|
||||
test "session with two events is not a bounce" do
|
||||
pageview = insert(:pageview)
|
||||
pageview2 = insert(:pageview, user_id: pageview.user_id)
|
||||
|
||||
Ingest.Session.on_event(pageview)
|
||||
Ingest.Session.on_event(pageview2)
|
||||
|
||||
session = capture_session(pageview.user_id)
|
||||
refute session.is_bounce
|
||||
end
|
||||
end
|
||||
|
||||
describe "on_unload/1" do
|
||||
test "uses the unload timestamp to calculate session length" do
|
||||
pageview = insert(:pageview)
|
||||
unload_timestamp = Timex.shift(pageview.timestamp, seconds: 30)
|
||||
|
||||
Ingest.Session.on_event(pageview)
|
||||
Ingest.Session.on_unload(pageview.user_id, unload_timestamp)
|
||||
|
||||
session = capture_session(pageview.user_id)
|
||||
assert session.length == 30
|
||||
end
|
||||
end
|
||||
end
|
@ -2,6 +2,13 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
use PlausibleWeb.ConnCase
|
||||
use Plausible.Repo
|
||||
|
||||
defp finalize_session(user_id) do
|
||||
session_pid = :global.whereis_name(user_id)
|
||||
Process.monitor(session_pid)
|
||||
|
||||
assert_receive({:DOWN, session_pid, _, _, _})
|
||||
end
|
||||
|
||||
@user_agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
|
||||
@country_code "EE"
|
||||
|
||||
@ -23,6 +30,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.hostname == "gigride.live"
|
||||
@ -44,6 +52,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert pageview.hostname == "example.com"
|
||||
end
|
||||
@ -79,6 +88,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.operating_system == "Mac"
|
||||
@ -100,6 +110,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.referrer_source == "Facebook"
|
||||
@ -120,6 +131,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.referrer_source == nil
|
||||
@ -140,6 +152,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.referrer_source == nil
|
||||
@ -160,6 +173,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.referrer_source == "blog.gigride.live"
|
||||
@ -179,6 +193,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert pageview.referrer == "indiehackers.com/page"
|
||||
end
|
||||
@ -197,6 +212,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert pageview.referrer_source == "traffic-source"
|
||||
end
|
||||
@ -215,6 +231,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert pageview.referrer_source == "traffic-source"
|
||||
end
|
||||
@ -234,6 +251,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.referrer_source == "indiehackers.com"
|
||||
@ -254,6 +272,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert is_nil(pageview.referrer_source)
|
||||
@ -276,6 +295,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.screen_size == "Mobile"
|
||||
@ -295,6 +315,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
pageview = Repo.one(Plausible.Event)
|
||||
finalize_session(pageview.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert pageview.screen_size == nil
|
||||
@ -314,6 +335,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||
|> post("/api/event", Jason.encode!(params))
|
||||
|
||||
event = Repo.one(Plausible.Event)
|
||||
finalize_session(event.user_id)
|
||||
|
||||
assert response(conn, 202) == ""
|
||||
assert event.name == "custom event"
|
||||
|
Loading…
Reference in New Issue
Block a user