| defmodule Plausible.Stats.QueryBuilderTest do |
| use Plausible.DataCase |
| alias Plausible.Stats.{DateTimeRange, ParsedQueryParams, QueryBuilder, Query, QueryError} |
|
|
| @now DateTime.new!(~D[2021-05-05], ~T[12:30:00], "Etc/UTC") |
| @date_range_realtime %DateTimeRange{ |
| first: DateTime.new!(~D[2021-05-05], ~T[12:25:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-05], ~T[12:30:05], "Etc/UTC") |
| } |
| @date_range_30m %DateTimeRange{ |
| first: DateTime.new!(~D[2021-05-05], ~T[12:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-05], ~T[12:30:05], "Etc/UTC") |
| } |
| @date_range_24h %DateTimeRange{ |
| first: DateTime.new!(~D[2021-05-04], ~T[12:30:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-05], ~T[12:30:00], "Etc/UTC") |
| } |
| @date_range_day %DateTimeRange{ |
| first: DateTime.new!(~D[2021-05-05], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-05], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_7d %DateTimeRange{ |
| first: DateTime.new!(~D[2021-04-28], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-04], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_10d %DateTimeRange{ |
| first: DateTime.new!(~D[2021-04-25], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-04], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_30d %DateTimeRange{ |
| first: DateTime.new!(~D[2021-04-05], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-04], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_month %DateTimeRange{ |
| first: DateTime.new!(~D[2021-05-01], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-05-31], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_3mo %DateTimeRange{ |
| first: DateTime.new!(~D[2021-02-01], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-04-30], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_6mo %DateTimeRange{ |
| first: DateTime.new!(~D[2020-11-01], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-04-30], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_year %DateTimeRange{ |
| first: DateTime.new!(~D[2021-01-01], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-12-31], ~T[23:59:59], "Etc/UTC") |
| } |
| @date_range_12mo %DateTimeRange{ |
| first: DateTime.new!(~D[2020-05-01], ~T[00:00:00], "Etc/UTC"), |
| last: DateTime.new!(~D[2021-04-30], ~T[23:59:59], "Etc/UTC") |
| } |
|
|
| setup [:create_user, :create_site] |
|
|
| describe "filter validation" do |
| for operation <- [ |
| :is, |
| :is_not, |
| :matches_wildcard, |
| :matches_wildcard_not, |
| :matches, |
| :matches_not, |
| :contains, |
| :contains_not |
| ] do |
| test "valid simple #{operation} filter passes validation", %{site: site} do |
| params = %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all, |
| filters: [[unquote(operation), "event:name", ["foo"]]] |
| } |
|
|
| assert {:ok, query} = QueryBuilder.build(site, params) |
|
|
| assert_matches %Query{filters: [[^unquote(operation), "event:name", ["foo"]]]} = query |
| end |
|
|
| test "valid complex nested #{operation} filter passes validation", %{site: site} do |
| params = %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all, |
| filters: [ |
| [ |
| :not, |
| [ |
| :and, |
| [ |
| [unquote(operation), "event:name", ["foo"]], |
| [unquote(operation), "event:name", ["bar"]] |
| ] |
| ] |
| ] |
| ] |
| } |
|
|
| assert {:ok, query} = QueryBuilder.build(site, params) |
|
|
| assert_matches %Query{ |
| filters: [ |
| [ |
| :not, |
| [ |
| :and, |
| [ |
| [unquote(operation), "event:name", ["foo"]], |
| [unquote(operation), "event:name", ["bar"]] |
| ] |
| ] |
| ] |
| ] |
| } = query |
| end |
| end |
|
|
| test "event goal name is checked within behavioral filters", %{site: site} do |
| insert(:goal, %{site: site, event_name: "Signup"}) |
|
|
| params = %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all, |
| filters: [[:has_done, [:is, "event:goal", ["Unknown"]]]] |
| } |
|
|
| assert {:error, %QueryError{message: error}} = QueryBuilder.build(site, params) |
|
|
| assert error == |
| "Invalid filters. The goal `Unknown` is not configured for this site. Find out how to configure goals here: https://plausible.io/docs/stats-api#filtering-by-goals" |
| end |
|
|
| for operation <- [:matches, :matches_not, :matches_wildcard, :matches_wildcard_not] do |
| test "case_sensitive modifier is not valid for #{operation}", %{site: site} do |
| assert {:error, %QueryError{message: error}} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all, |
| filters: [ |
| [unquote(operation), "event:page", ["a"], %{case_insensitive: true}] |
| ] |
| }) |
|
|
| assert error =~ "Invalid filters." |
| assert error =~ "case_sensitive modifier is not allowed with pattern operators" |
| assert error =~ Atom.to_string(unquote(operation)) |
| end |
| end |
|
|
| test "prohibits case_sensitive modifier with pattern operator in a complex filter tree", %{ |
| site: site |
| } do |
| assert {:error, %QueryError{message: error}} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all, |
| filters: [ |
| [ |
| :or, |
| [ |
| [ |
| :and, |
| [ |
| [:is, "visit:city_name", ["London"]], |
| [:not, [:is, "visit:country_name", ["Canada"]]] |
| ] |
| ], |
| [:matches, "event:page", ["a"], %{case_insensitive: true}] |
| ] |
| ] |
| ] |
| }) |
|
|
| assert error =~ "Invalid filters." |
| assert error =~ "case_sensitive modifier is not allowed with pattern operators" |
| assert error =~ ":matches" |
| end |
| end |
|
|
| describe "date range" do |
| for {input_date_range, expected_utc_time_range} <- [ |
| {:realtime, @date_range_realtime}, |
| {:realtime_30m, @date_range_30m}, |
| {:"24h", @date_range_24h} |
| ] do |
| test "builds utc_time_range for #{input_date_range} input_date_range", %{site: site} do |
| assert {:ok, query} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| now: @now, |
| metrics: [:visitors], |
| input_date_range: unquote(input_date_range) |
| }) |
|
|
| assert query.utc_time_range == unquote(Macro.escape(expected_utc_time_range)) |
| end |
| end |
|
|
| test "utc_time_range construction respects relative_date", %{site: site} do |
| relative_date = @now |> DateTime.to_date() |
|
|
| |
| now = DateTime.utc_now(:second) |
|
|
| for {date_range_shortcut, expected_utc_time_range} <- [ |
| {:day, @date_range_day}, |
| {{:last_n_days, 7}, @date_range_7d}, |
| {{:last_n_days, 10}, @date_range_10d}, |
| {{:last_n_days, 30}, @date_range_30d}, |
| {:month, @date_range_month}, |
| {{:last_n_months, 3}, @date_range_3mo}, |
| {{:last_n_months, 6}, @date_range_6mo}, |
| {{:last_n_months, 12}, @date_range_12mo}, |
| {:year, @date_range_year} |
| ] do |
| assert {:ok, query} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| now: now, |
| metrics: [:visitors], |
| relative_date: relative_date, |
| input_date_range: date_range_shortcut |
| }) |
|
|
| assert query.utc_time_range == expected_utc_time_range |
| end |
| end |
| end |
|
|
| describe "exit_rate metric" do |
| test "fails validation without visit:exit_page dimension", %{site: site} do |
| assert {:error, %QueryError{message: error}} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:exit_rate], |
| input_date_range: :all |
| }) |
|
|
| assert error == |
| "Metric `exit_rate` requires a `\"visit:exit_page\"` dimension. No other dimensions are allowed." |
| end |
|
|
| test "fails validation with event only filters", %{site: site} do |
| assert {:error, %QueryError{message: error}} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:exit_rate], |
| dimensions: ["visit:exit_page"], |
| filters: [[:is, "event:page", ["/"]]], |
| input_date_range: :all |
| }) |
|
|
| assert error == "Metric `exit_rate` cannot be queried when filtering on event dimensions." |
| end |
|
|
| test "fails validation with event metrics", %{site: site} do |
| assert {:error, %QueryError{message: error}} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:exit_rate, :pageviews], |
| dimensions: ["visit:exit_page"], |
| input_date_range: :all |
| }) |
|
|
| assert error == |
| "Event metric(s) `pageviews` cannot be queried along with session dimension(s) `visit:exit_page`" |
| end |
|
|
| test "passes validation", %{site: site} do |
| assert {:ok, query} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:exit_rate], |
| dimensions: ["visit:exit_page"], |
| input_date_range: :all |
| }) |
|
|
| assert_matches %Query{ |
| metrics: [:exit_rate], |
| dimensions: ["visit:exit_page"] |
| } = query |
| end |
| end |
|
|
| on_ee do |
| describe "query.consolidated_site_ids" do |
| test "is set to nil when site is regular", %{site: site} do |
| assert {:ok, %Query{consolidated_site_ids: nil}} = |
| QueryBuilder.build(site, %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all |
| }) |
| end |
|
|
| test "is set to a list of site_ids when site is consolidated", %{site: site} do |
| new_site(team: site.team) |
| cv = new_consolidated_view(site.team) |
|
|
| assert {:ok, %Query{consolidated_site_ids: consolidated_site_ids}} = |
| QueryBuilder.build(cv, %ParsedQueryParams{ |
| metrics: [:visitors], |
| input_date_range: :all |
| }) |
|
|
| assert length(consolidated_site_ids) == 2 |
| assert site.id in consolidated_site_ids |
| end |
| end |
| end |
| end |
|
|