Skip to content
Merged
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
42 changes: 36 additions & 6 deletions assets/js/dashboard/extra/exploration.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { ChevronUpDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import { FlagIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { popover } from '../components/popover'

const JOURNEY_END_EVENT = '__journey_end__'
Comment thread
zoldar marked this conversation as resolved.

const DIRECTION = { FORWARD: 'forward', BACKWARD: 'backward' }

const DIRECTION_OPTIONS = [
Expand Down Expand Up @@ -102,6 +104,19 @@ function stepsToJourneyParam(steps) {
)
}

function maybeEmptyResults(results, activeFilter) {
if (
results.length === 0 ||
(!activeFilter &&
results.length === 1 &&
results[0].step.name === JOURNEY_END_EVENT)
) {
return []
} else {
return results
}
}

// Keep only entries with index < fromIndex, discarding everything at or after.
// Used to truncate frozen candidate snapshots when the journey is shortened.
function truncateFrozenAt(frozen, fromIndex) {
Expand Down Expand Up @@ -354,7 +369,9 @@ function CandidateCard({
colIndex,
onSelect
}) {
const isCustomEvent = step.name !== 'pageview'
const isJourneyEnd = step.name === JOURNEY_END_EVENT
const isCustomEvent =
step.name !== 'pageview' && step.name !== JOURNEY_END_EVENT
Comment thread
zoldar marked this conversation as resolved.
const isGoal = step.is_goal

const visitorsToShow =
Expand Down Expand Up @@ -382,12 +399,16 @@ function CandidateCard({
? 'bg-gray-100/60 dark:bg-gray-850'
: 'hover:bg-gray-100/60 dark:hover:bg-gray-850'

const pointer = isJourneyEnd ? 'pointer-events-none' : ''

const onSelectHandler = isJourneyEnd ? () => {} : onSelect

return (
<li>
<button
data-exploration-step={isSelected ? colIndex : undefined}
className={`group relative w-full text-left text-sm rounded-sm overflow-hidden focus:outline-none ${rowBg}`}
onClick={() => onSelect(isSelected ? null : step)}
className={`group relative w-full text-left text-sm rounded-sm overflow-hidden focus:outline-none ${rowBg} ${pointer}`}
onClick={() => onSelectHandler(isSelected ? null : step)}
>
<div
className={`absolute top-0 left-0 h-full rounded-sm transition-[width] ease-in-out ${barBg}`}
Expand Down Expand Up @@ -801,7 +822,10 @@ function useExplorationData(site, dashboardState, inViewport) {
if (!isStale())
setState((prev) => ({
...prev,
activeResults: r?.next ?? [],
activeResults: maybeEmptyResults(
r?.next ?? [],
prev.activeFilter
),
rateLimited: false
}))
})
Expand Down Expand Up @@ -846,7 +870,10 @@ function useExplorationData(site, dashboardState, inViewport) {
if (!isStale())
setState((prev) => ({
...prev,
activeResults: r?.next ?? [],
activeResults: maybeEmptyResults(
r?.next ?? [],
prev.activeFilter
),
rateLimited: false
}))
})
Expand Down Expand Up @@ -897,7 +924,10 @@ function useExplorationData(site, dashboardState, inViewport) {
setState((prev) => {
const next = {
...prev,
activeResults: response?.next ?? [],
activeResults: maybeEmptyResults(
response?.next ?? [],
prev.activeFilter
),
rateLimited: false
}
if (includeFunnel) {
Expand Down
13 changes: 8 additions & 5 deletions extra/lib/plausible/stats/exploration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ defmodule Plausible.Stats.Exploration do
import Plausible.Stats.SQL.Fragments
import Plausible.Stats.Util, only: [percentage: 2]

alias Plausible.Stats.Exploration.Journey
alias Plausible.ClickhouseRepo
alias Plausible.Stats.Base
alias Plausible.Stats.Exploration.Journey
alias Plausible.Stats.Filters
alias Plausible.Stats.Query

Expand Down Expand Up @@ -143,7 +143,7 @@ defmodule Plausible.Stats.Exploration do
query,
[],
[],
MapSet.new(),
MapSet.new([{Journey.Step.journey_end_event(), ""}]),
max_steps,
max_candidates,
include_wildcard?
Expand Down Expand Up @@ -241,7 +241,6 @@ defmodule Plausible.Stats.Exploration do

q_matches =
from(s in subquery(q_steps),
where: selected_as(:name) != "",
select: %{
user_id: s.user_id,
name: selected_as(field(s, ^next_name), :name),
Expand Down Expand Up @@ -286,8 +285,12 @@ defmodule Plausible.Stats.Exploration do
from(m in subquery(q_all_combined_matches),
select: %{
step: %Journey.Step{
label: selected_as(m.label, :label),
name: m.name,
label:
selected_as(
fragment("if(? != '', ?, ?)", m.name, m.label, ^Journey.Step.journey_end_label()),
:label
),
name: fragment("if(? != '', ?, ?)", m.name, m.name, ^Journey.Step.journey_end_event()),
pathname: m.pathname,
includes_subpaths: m.includes_subpaths,
subpaths_count: m.subpaths_count,
Expand Down
22 changes: 18 additions & 4 deletions extra/lib/plausible/stats/journey/step.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ defmodule Plausible.Stats.Exploration.Journey.Step do
subpaths_count: 0,
is_goal: false

@journey_end_event "__journey_end__"
@journey_end_label "no further action"
Comment thread
zoldar marked this conversation as resolved.

@spec journey_end_event() :: String.t()
def journey_end_event, do: @journey_end_event

@spec journey_end_label() :: String.t()
def journey_end_label, do: @journey_end_label

@spec from(map()) :: t()
def from(step) do
new(step.name, step.pathname, step.includes_subpaths, step.subpaths_count, step.is_goal)
Expand All @@ -21,10 +30,15 @@ defmodule Plausible.Stats.Exploration.Journey.Step do
def new(name, pathname, includes_subpaths \\ false, subpaths_count \\ 0, is_goal \\ false)
when is_boolean(includes_subpaths) and is_integer(subpaths_count) do
label =
if name != "pageview" do
name
else
pathname
cond do
name == @journey_end_event ->
@journey_end_label

name != "pageview" ->
name

true ->
pathname
end

%__MODULE__{
Expand Down
23 changes: 23 additions & 0 deletions lib/plausible/goal.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
defmodule Plausible.Goal do
@moduledoc """
Goal schema.
"""

use Plausible
use Ecto.Schema

import Ecto.Changeset

@type t() :: %__MODULE__{}

on_ee do
@journey_end_event Plausible.Stats.Exploration.Journey.Step.journey_end_event()
else
@journey_end_event nil
end

schema "goals" do
field :event_name, :string
field :page_path, :string
Expand Down Expand Up @@ -153,6 +164,10 @@ defmodule Plausible.Goal do
value == "engagement" ->
{:error, "The event name 'engagement' is reserved and cannot be used as a goal"}

journey_end_event?(value) ->
{:error,
"The event name '#{@journey_end_event}' is reserved and cannot be used as a goal"}

value && String.match?(value, ~r/^.+/) ->
:ok

Expand Down Expand Up @@ -223,6 +238,14 @@ defmodule Plausible.Goal do
[]
end
end

on_ee do
defp journey_end_event?(name) do
name == @journey_end_event
end
else
defp journey_end_event?(_name), do: always(false)
end
end

defimpl Jason.Encoder, for: Plausible.Goal do
Expand Down
11 changes: 11 additions & 0 deletions test/plausible/goals_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,17 @@ defmodule Plausible.GoalsTest do
end

on_ee do
@journey_end_event Plausible.Stats.Exploration.Journey.Step.journey_end_event()

test "create/2 fails to create a goal with '#{@journey_end_event}' as event_name (reserved)" do
site = new_site()
assert {:error, changeset} = Goals.create(site, %{"event_name" => @journey_end_event})

assert {"The event name '#{@journey_end_event}' is reserved and cannot be used as a goal",
_} =
changeset.errors[:event_name]
end

test "create/2 sets site.updated_at for revenue goal" do
site_1 = new_site(updated_at: DateTime.add(DateTime.utc_now(), -3600))

Expand Down
113 changes: 109 additions & 4 deletions test/plausible/stats/exploration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ defmodule Plausible.Stats.ExplorationTest do
alias Plausible.Stats.Exploration
alias Plausible.Stats.QueryBuilder

@journey_end_event Exploration.Journey.Step.journey_end_event()
@journey_end_label Exploration.Journey.Step.journey_end_label()

setup do
site = new_site()

Expand Down Expand Up @@ -572,6 +575,67 @@ defmodule Plausible.Stats.ExplorationTest do
assert next_step.visitors == 1
end

test "allows to filter by journey end event label" do
site = new_site()
now = DateTime.utc_now()

populate_stats(site, [
build(:pageview,
user_id: 123,
pathname: "/home",
timestamp: DateTime.shift(now, minute: -50)
),
build(:pageview,
user_id: 123,
pathname: "/login",
timestamp: DateTime.shift(now, minute: -40)
),
build(:pageview,
user_id: 124,
pathname: "/home",
timestamp: DateTime.shift(now, minute: -30)
),
build(:pageview,
user_id: 124,
pathname: "/login",
timestamp: DateTime.shift(now, minute: -20)
),
build(:pageview,
user_id: 124,
pathname: "/dashboard",
timestamp: DateTime.shift(now, minute: -10)
),
build(:pageview,
user_id: 125,
pathname: "/home",
timestamp: DateTime.shift(now, minute: -50)
),
build(:pageview,
user_id: 125,
pathname: "/login",
timestamp: DateTime.shift(now, minute: -40)
),
build(:pageview,
user_id: 125,
pathname: "/dashboard",
timestamp: DateTime.shift(now, minute: -30)
)
])

query = QueryBuilder.build!(site, input_date_range: :all)

journey = [
%Exploration.Journey.Step{name: "pageview", pathname: "/home"},
%Exploration.Journey.Step{name: "pageview", pathname: "/login"}
]

assert {:ok, [next_step]} =
Exploration.next_steps(site, query, journey, search_term: "no further")

assert next_step.step.label == "no further action"
assert next_step.visitors == 1
end

test "includes root path (/) in suggestions" do
site = new_site()

Expand Down Expand Up @@ -661,6 +725,40 @@ defmodule Plausible.Stats.ExplorationTest do
assert next_step2.visitors == 1
end

test "there can be multiple journey suggestions for a single user/session" do
site = new_site()

now = DateTime.utc_now()

ago = fn ms -> DateTime.shift(now, minute: -1 * ms) end

populate_stats(site, [
build(:pageview, user_id: 123, pathname: "/home", timestamp: ago.(100)),
build(:pageview, user_id: 123, pathname: "/login", timestamp: ago.(99)),
build(:pageview, user_id: 123, pathname: "/dashboard", timestamp: ago.(98)),
build(:pageview, user_id: 123, pathname: "/home", timestamp: ago.(97)),
build(:pageview, user_id: 123, pathname: "/login", timestamp: ago.(96)),
build(:pageview, user_id: 123, pathname: "/sites", timestamp: ago.(95))
])

journey = [
%Exploration.Journey.Step{name: "pageview", pathname: "/home"},
%Exploration.Journey.Step{name: "pageview", pathname: "/login"}
]

query = QueryBuilder.build!(site, input_date_range: :all)

assert {:ok,
[
%{step: %{pathname: "/dashboard"}, visitors: 1},
%{step: %{pathname: "/sites"}, visitors: 1}
]} =
Exploration.next_steps(site, query, journey,
search_term: "",
direction: :forward
)
end

test "does not suggest the same path/pathname as in previous step (regression test)" do
site = new_site()

Expand Down Expand Up @@ -720,6 +818,7 @@ defmodule Plausible.Stats.ExplorationTest do

assert {:ok,
[
%{step: %{name: @journey_end_event}},
%{step: %{pathname: "/:dashboard"}}
]} =
Exploration.next_steps(site, query, journey,
Expand Down Expand Up @@ -1166,9 +1265,12 @@ defmodule Plausible.Stats.ExplorationTest do
}
]

assert {:ok, [next_step]} = Exploration.next_steps(site, query, journey)
assert {:ok, [next_step1, next_step2]} = Exploration.next_steps(site, query, journey)

assert next_step.step.label == "/a-blog"
assert next_step1.step.label == @journey_end_label
assert next_step1.visitors == 2
assert next_step2.step.label == "/a-blog"
assert next_step2.visitors == 1
end

test "suggestions matching goal pattern from previous step are excluded" do
Expand Down Expand Up @@ -1231,9 +1333,12 @@ defmodule Plausible.Stats.ExplorationTest do
}
]

assert {:ok, [next_step]} = Exploration.next_steps(site, query, journey)
assert {:ok, [next_step1, next_step2]} = Exploration.next_steps(site, query, journey)

assert next_step.step.label == "/blog"
assert next_step1.step.label == @journey_end_label
assert next_step1.visitors == 3
assert next_step2.step.label == "/blog"
assert next_step2.visitors == 1
end
end

Expand Down
Loading