File size: 3,017 Bytes
936b397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
defmodule Plausible.Stats.Sampling do
  @moduledoc """
  Sampling related functions
  """
  @default_sample_threshold 10_000_000

  import Ecto.Query

  alias Plausible.Stats.{Query, SamplingCache}

  def default_sample_threshold(), do: @default_sample_threshold

  @spec add_query_hint(Ecto.Query.t(), Plausible.Stats.Query.t()) :: Ecto.Query.t()
  def add_query_hint(%Ecto.Query{} = db_query, %Plausible.Stats.Query{} = query) do
    case query.sample_threshold do
      :no_sampling ->
        db_query

      nil ->
        db_query

      threshold ->
        add_query_hint(db_query, threshold)
    end
  end

  @spec add_query_hint(Ecto.Query.t(), pos_integer() | float()) :: Ecto.Query.t()
  def add_query_hint(%Ecto.Query{} = query, threshold) when is_number(threshold) do
    from(x in query, hints: unsafe_fragment(^"SAMPLE #{threshold}"))
  end

  @spec add_query_hint(Ecto.Query.t()) :: Ecto.Query.t()
  def add_query_hint(%Ecto.Query{} = query) do
    add_query_hint(query, @default_sample_threshold)
  end

  @spec put_threshold(Plausible.Stats.Query.t(), Plausible.Site.t(), map()) ::
          Plausible.Stats.Query.t()
  def put_threshold(query, site, params) do
    sample_threshold =
      case params["sample_threshold"] do
        nil ->
          decide_sample_rate(site, query)

        "infinite" ->
          :no_sampling

        value_string ->
          {value, _} = Float.parse(value_string)
          value
      end

    Map.put(query, :sample_threshold, sample_threshold)
  end

  defp decide_sample_rate(site, query) do
    if Plausible.Sites.consolidated?(site) and not Enum.empty?(query.consolidated_site_ids) do
      query.consolidated_site_ids
      |> SamplingCache.consolidated_get()
      |> fractional_sample_rate(query)
    else
      site.id
      |> SamplingCache.get()
      |> fractional_sample_rate(query)
    end
  end

  def fractional_sample_rate(nil = _traffic_30_day, _query),
    do: :no_sampling

  def fractional_sample_rate(traffic_30_day, query) do
    date_range = Query.date_range(query)
    duration = Date.diff(date_range.last, date_range.first)

    estimated_traffic = estimate_traffic(traffic_30_day, duration, query)

    fraction =
      if(estimated_traffic > 0,
        do: Float.round(@default_sample_threshold / estimated_traffic, 2),
        else: 1.0
      )

    cond do
      # Don't sample small time ranges
      duration < 1 -> :no_sampling
      # If sampling doesn't have a significant effect, don't sample
      fraction > 0.4 -> :no_sampling
      true -> max(fraction, min_sample_rate())
    end
  end

  defp min_sample_rate(), do: 0.013

  defp estimate_traffic(traffic_30_day, duration, query) do
    duration_adjusted_traffic = traffic_30_day / 30.0 * duration

    estimate_by_filters(duration_adjusted_traffic, query.filters)
  end

  @filter_traffic_multiplier 1 / 4.0
  @max_filters 2

  defp estimate_by_filters(estimation, filters),
    do: estimation * @filter_traffic_multiplier ** min(length(filters), @max_filters)
end