@@ -8,16 +8,22 @@ defmodule Plausible.Stats.Exploration do
88
99 @ type t ( ) :: % __MODULE__ { }
1010
11- @ derive { Jason.Encoder , only: [ :name , :pathname , :label , :includes_subpaths , :subpaths_count ] }
12- defstruct name: nil , pathname: "" , label: nil , includes_subpaths: false , subpaths_count: 0
11+ @ derive { Jason.Encoder ,
12+ only: [ :name , :pathname , :label , :includes_subpaths , :subpaths_count , :is_goal ] }
13+ defstruct name: nil ,
14+ pathname: "" ,
15+ label: nil ,
16+ includes_subpaths: false ,
17+ subpaths_count: 0 ,
18+ is_goal: false
1319
1420 @ spec from ( map ( ) ) :: t ( )
1521 def from ( step ) do
16- new ( step . name , step . pathname , step . includes_subpaths , step . subpaths_count )
22+ new ( step . name , step . pathname , step . includes_subpaths , step . subpaths_count , step . is_goal )
1723 end
1824
1925 @ spec new ( String . t ( ) , String . t ( ) , boolean ( ) , non_neg_integer ( ) ) :: t ( )
20- def new ( name , pathname , includes_subpaths \\ false , subpaths_count \\ 0 )
26+ def new ( name , pathname , includes_subpaths \\ false , subpaths_count \\ 0 , is_goal \\ false )
2127 when is_boolean ( includes_subpaths ) and is_integer ( subpaths_count ) do
2228 label =
2329 if name != "pageview" do
@@ -31,7 +37,8 @@ defmodule Plausible.Stats.Exploration do
3137 name: name ,
3238 pathname: pathname ,
3339 includes_subpaths: includes_subpaths ,
34- subpaths_count: subpaths_count
40+ subpaths_count: subpaths_count ,
41+ is_goal: is_goal
3542 }
3643 end
3744 end
@@ -76,24 +83,29 @@ defmodule Plausible.Stats.Exploration do
7683 @ spec max_steps ( ) :: pos_integer ( )
7784 def max_steps , do: @ max_steps
7885
79- @ spec next_steps ( Query . t ( ) , journey ( ) , keyword ( ) ) ::
86+ @ spec next_steps ( Plausible.Site . t ( ) , Query . t ( ) , journey ( ) , keyword ( ) ) ::
8087 { :ok , [ next_step ( ) ] } | { :error , :journey_too_long }
81- def next_steps ( query , journey , opts \\ [ ] )
88+ def next_steps ( site , query , journey , opts \\ [ ] )
8289
83- def next_steps ( _query , journey , _opts ) when length ( journey ) >= @ max_steps do
90+ def next_steps ( _site , _query , journey , _opts ) when length ( journey ) >= @ max_steps do
8491 { :error , :journey_too_long }
8592 end
8693
87- def next_steps ( query , journey , opts ) do
94+ def next_steps ( site , query , journey , opts ) do
8895 opts = Keyword . merge ( @ next_steps_defaults , opts )
8996 direction = Keyword . fetch! ( opts , :direction )
9097 search_term = Keyword . fetch! ( opts , :search_term )
9198 max_candidates = min ( Keyword . fetch! ( opts , :max_candidates ) , @ max_candidates )
9299 include_wilcard? = Keyword . fetch! ( opts , :include_wildcard? )
93100
101+ goals =
102+ site
103+ |> Plausible.Goals . for_site ( include_goals_with_custom_props?: false )
104+ |> filter_eligible_goals ( )
105+
94106 query
95107 |> Base . base_event_query ( )
96- |> next_steps_query ( journey , search_term , direction , max_candidates , include_wilcard? )
108+ |> next_steps_query ( journey , search_term , direction , max_candidates , include_wilcard? , goals )
97109 # We pass the query struct to record query metadata for
98110 # the CH debug console.
99111 |> ClickhouseRepo . all ( query: query )
@@ -143,9 +155,9 @@ defmodule Plausible.Stats.Exploration do
143155 to include implicit wildcard pathnames in suggestions or not
144156 (default: true)
145157 """
146- @ spec interesting_funnel ( Query . t ( ) , keyword ( ) ) ::
158+ @ spec interesting_funnel ( Plausible.Site . t ( ) , Query . t ( ) , keyword ( ) ) ::
147159 { :ok , % { funnel: [ funnel_step ( ) ] , candidates: [ next_step ( ) ] } } | { :error , :not_found }
148- def interesting_funnel ( query , opts \\ [ ] ) do
160+ def interesting_funnel ( site , query , opts \\ [ ] ) do
149161 max_steps = min ( Keyword . get ( opts , :max_steps , 6 ) , @ max_steps )
150162 max_candidates = min ( Keyword . get ( opts , :max_candidates , 10 ) , @ max_candidates )
151163
@@ -157,14 +169,15 @@ defmodule Plausible.Stats.Exploration do
157169 )
158170
159171 with { :ok , result } <-
160- build_interesting_journey ( query , max_steps , max_candidates , include_wildcard? ) ,
172+ build_interesting_journey ( site , query , max_steps , max_candidates , include_wildcard? ) ,
161173 { :ok , funnel } <- journey_funnel ( query , result . journey ) do
162174 { :ok , % { funnel: funnel , candidates: result . candidates } }
163175 end
164176 end
165177
166- defp build_interesting_journey ( query , max_steps , max_candidates , include_wildcard? ) do
178+ defp build_interesting_journey ( site , query , max_steps , max_candidates , include_wildcard? ) do
167179 case do_build_journey (
180+ site ,
168181 query ,
169182 [ ] ,
170183 [ ] ,
@@ -179,6 +192,7 @@ defmodule Plausible.Stats.Exploration do
179192 end
180193
181194 defp do_build_journey (
195+ _site ,
182196 _query ,
183197 journey ,
184198 step_candidates ,
@@ -192,6 +206,7 @@ defmodule Plausible.Stats.Exploration do
192206 end
193207
194208 defp do_build_journey (
209+ site ,
195210 query ,
196211 journey ,
197212 step_candidates ,
@@ -201,7 +216,7 @@ defmodule Plausible.Stats.Exploration do
201216 include_wildcard?
202217 ) do
203218 { :ok , candidates } =
204- next_steps ( query , journey ,
219+ next_steps ( site , query , journey ,
205220 max_candidates: max_candidates ,
206221 include_wildcard?: include_wildcard?
207222 )
@@ -214,6 +229,7 @@ defmodule Plausible.Stats.Exploration do
214229 new_seen = MapSet . put ( seen , normalize_step_key ( step ) )
215230
216231 do_build_journey (
232+ site ,
217233 query ,
218234 journey ++ [ step ] ,
219235 step_candidates ++ [ candidates ] ,
@@ -238,7 +254,22 @@ defmodule Plausible.Stats.Exploration do
238254 defp normalize_pathname ( "/" ) , do: "/"
239255 defp normalize_pathname ( pathname ) , do: String . trim_trailing ( pathname , "/" )
240256
241- defp next_steps_query ( query , steps , search_term , direction , max_candidates , include_wildcard? )
257+ defp filter_eligible_goals ( goals ) do
258+ Enum . filter ( goals , fn g ->
259+ is_nil ( g . currency ) and g . scroll_threshold == - 1 and g . custom_props == % { } and
260+ is_nil ( g . event_name )
261+ end )
262+ end
263+
264+ defp next_steps_query (
265+ query ,
266+ steps ,
267+ search_term ,
268+ direction ,
269+ max_candidates ,
270+ include_wildcard? ,
271+ goals
272+ )
242273 when is_direction ( direction ) do
243274 next_step_idx = length ( steps ) + 1
244275 q_steps = steps_query ( query , next_step_idx , direction )
@@ -251,7 +282,8 @@ defmodule Plausible.Stats.Exploration do
251282 where: selected_as ( :name ) != "" ,
252283 select: % {
253284 name: selected_as ( field ( s , ^ next_name ) , :name ) ,
254- pathname: selected_as ( field ( s , ^ next_pathname ) , :pathname )
285+ pathname: selected_as ( field ( s , ^ next_pathname ) , :pathname ) ,
286+ _sample_factor: fragment ( "any(?)" , s . _sample_factor )
255287 }
256288 )
257289
@@ -266,12 +298,10 @@ defmodule Plausible.Stats.Exploration do
266298
267299 q_per_user_matches =
268300 from ( m in q_matches ,
269- select_merge: % { user_id: m . user_id , _sample_factor: fragment ( "any(?)" , m . _sample_factor ) } ,
301+ select_merge: % { user_id: m . user_id } ,
270302 group_by: [ selected_as ( :name ) , selected_as ( :pathname ) , m . user_id ]
271303 )
272304
273- q_combined = combined_query ( q_per_user_matches , include_wildcard? )
274-
275305 # Fan out each q_combined row into up to two output rows (exact + wildcard)
276306 # using ARRAY JOIN over a small boolean array.
277307 #
@@ -280,8 +310,10 @@ defmodule Plausible.Stats.Exploration do
280310 # subpath, or same visitor count as exact). ARRAY JOIN then emits one or more
281311 # rows per group. The joined boolean `is_wildcard` selects which values to
282312 # use for visitors / includes_subpaths / subpaths_count.
283- q_all_matches =
284- from ( m in subquery ( q_combined ) ,
313+ q_wildcard_combined = combined_wildcard_query ( q_per_user_matches , include_wildcard? )
314+
315+ q_wildcard_combined_matches =
316+ from ( m in subquery ( q_wildcard_combined ) ,
285317 join:
286318 is_wildcard in fragment (
287319 """
@@ -300,6 +332,16 @@ defmodule Plausible.Stats.Exploration do
300332 hints: "ARRAY" ,
301333 where: selected_as ( :visitors ) > 0 ,
302334 select: % {
335+ label:
336+ selected_as (
337+ fragment (
338+ "if(? != 'pageview', ?, ?)" ,
339+ m . name ,
340+ m . name ,
341+ m . pathname
342+ ) ,
343+ :label
344+ ) ,
303345 name: m . name ,
304346 pathname: m . pathname ,
305347 visitors:
@@ -308,27 +350,28 @@ defmodule Plausible.Stats.Exploration do
308350 :visitors
309351 ) ,
310352 includes_subpaths: fragment ( "CAST(?, 'Bool')" , is_wildcard ) ,
311- subpaths_count: fragment ( "if(?, ?, 0)" , is_wildcard , m . subpaths_count )
353+ subpaths_count: fragment ( "if(?, ?, 0)" , is_wildcard , m . subpaths_count ) ,
354+ is_goal: fragment ( "CAST(?, 'Bool')" , false )
312355 }
313356 )
314357
315- from ( m in subquery ( q_all_matches ) ,
358+ q_all_combined_matches =
359+ if q_goal_matches = goals_query ( q_per_user_matches , goals ) do
360+ q_wildcard_combined_matches
361+ |> union_all ( ^ q_goal_matches )
362+ else
363+ q_wildcard_combined_matches
364+ end
365+
366+ from ( m in subquery ( q_all_combined_matches ) ,
316367 select: % {
317368 step: % Journey.Step {
318- label:
319- selected_as (
320- fragment (
321- "if(? != 'pageview', ?, ?)" ,
322- m . name ,
323- m . name ,
324- m . pathname
325- ) ,
326- :label
327- ) ,
369+ label: selected_as ( m . label , :label ) ,
328370 name: m . name ,
329371 pathname: m . pathname ,
330372 includes_subpaths: m . includes_subpaths ,
331- subpaths_count: m . subpaths_count
373+ subpaths_count: m . subpaths_count ,
374+ is_goal: m . is_goal
332375 } ,
333376 visitors: m . visitors
334377 } ,
@@ -342,6 +385,59 @@ defmodule Plausible.Stats.Exploration do
342385 |> maybe_search ( search_term )
343386 end
344387
388+ @ goal_pathname_condition """
389+ if(? LIKE '%*%',
390+ match(?, concat('^', replaceAll(?, '*', '.*'), '$')),
391+ ? = ?
392+ )
393+ """
394+
395+ defp goals_query ( _ , [ ] ) , do: nil
396+
397+ defp goals_query ( q_matches , goals ) do
398+ values =
399+ Enum . map ( goals , fn g ->
400+ % {
401+ label: g . display_name ,
402+ name: g . event_name || "pageview" ,
403+ pathname: Regex . escape ( g . page_path || "" )
404+ }
405+ end )
406+
407+ types = % { label: :string , name: :string , pathname: :string }
408+
409+ query =
410+ from ( g in values ( values , types ) ,
411+ inner_join: m in subquery ( q_matches ) ,
412+ on:
413+ g . name == m . name and
414+ ( g . name != "pageview" or
415+ ( g . name == "pageview" and
416+ fragment (
417+ @ goal_pathname_condition ,
418+ g . pathname ,
419+ m . pathname ,
420+ g . pathname ,
421+ m . pathname ,
422+ g . pathname
423+ ) ) ) ,
424+ select: % {
425+ label: selected_as ( g . label , :label ) ,
426+ name: selected_as ( g . name , :name ) ,
427+ pathname: selected_as ( g . pathname , :pathname ) ,
428+ visitors: scale_sample ( fragment ( "uniq(?)" , m . user_id ) ) ,
429+ includes_subpaths: fragment ( "CAST(?, 'Bool')" , false ) ,
430+ subpaths_count: 0 ,
431+ is_goal: fragment ( "CAST(?, 'Bool')" , true )
432+ } ,
433+ group_by: [ selected_as ( :label ) , selected_as ( :name ) , selected_as ( :pathname ) ]
434+ )
435+
436+ from ( m in subquery ( query ) ,
437+ where: m . visitors > 0
438+ )
439+ end
440+
345441 # Expand each (name, pathname, user_id) row into all prefix paths via
346442 # ARRAY JOIN, then aggregate once to get both exact and wildcard visitor
347443 # counts in a single scan of events_v2.
@@ -361,7 +457,7 @@ defmodule Plausible.Stats.Exploration do
361457 arraySlice(split_pathname, 1, 1)), [?])
362458 """
363459
364- defp combined_query ( q_matches , true = _include_wildcard? ) do
460+ defp combined_wildcard_query ( q_matches , true = _include_wildcard? ) do
365461 from ( em in subquery ( q_matches ) ,
366462 join: pname in fragment ( @ wildcard_array_join , em . name , em . pathname , em . pathname ) ,
367463 on: true ,
@@ -380,7 +476,7 @@ defmodule Plausible.Stats.Exploration do
380476 )
381477 end
382478
383- defp combined_query ( q_matches , false = _include_wildcard? ) do
479+ defp combined_wildcard_query ( q_matches , false = _include_wildcard? ) do
384480 from ( em in subquery ( q_matches ) ,
385481 where: em . name != "pageview" or selected_as ( :pathname ) != "" ,
386482 select: % {
0 commit comments