Merge branch 'manual-filters' of github.com:Vigasaurus/plausible-analytics into manual-filters
This commit is contained in:
commit
e00929b1c5
|
|
@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
|
||||||
- Glob (wildcard) based page, entry page and exit page filters plausible/analytics#1067
|
- Glob (wildcard) based page, entry page and exit page filters plausible/analytics#1067
|
||||||
- Exclusion filters for page, entry page and exit page filters plausible/analytics#1067
|
- Exclusion filters for page, entry page and exit page filters plausible/analytics#1067
|
||||||
- Menu to add new and edit existing filters directly plausible/analytics#1067
|
- Menu to add new and edit existing filters directly plausible/analytics#1067
|
||||||
|
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters plausible/analytics#1073
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix weekly report time range plausible/analytics#951
|
- Fix weekly report time range plausible/analytics#951
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,10 @@ Make sure Docker, Elixir, Erlang and Node.js are all installed on your developme
|
||||||
3. Fill in the rest of the forms and for the domain use `dummy.site`
|
3. Fill in the rest of the forms and for the domain use `dummy.site`
|
||||||
4. Run `make dummy_event` from the terminal to generate a fake pageview event for the dummy site.
|
4. Run `make dummy_event` from the terminal to generate a fake pageview event for the dummy site.
|
||||||
5. You should now be all set!
|
5. You should now be all set!
|
||||||
|
|
||||||
|
### Stopping Docker containers
|
||||||
|
|
||||||
|
1. Stop and remove the Postgres container with `make postgres-stop`.
|
||||||
|
2. Stop and remove the Clickhouse container with `make clickhouse-stop`.
|
||||||
|
|
||||||
|
Volumes are preserved. You'll find that the Postgres and Clickhouse state are retained when you bring them up again the next time: no need to re-register and so on.
|
||||||
10
Makefile
10
Makefile
|
|
@ -1,8 +1,14 @@
|
||||||
clickhouse:
|
clickhouse:
|
||||||
docker run --detach -p 8123:8123 --ulimit nofile=262144:262144 --volume=$$PWD/.clickhouse_db_vol:/var/lib/clickhouse yandex/clickhouse-server:21.3.2.5
|
docker run --detach -p 8123:8123 --ulimit nofile=262144:262144 --volume=$$PWD/.clickhouse_db_vol:/var/lib/clickhouse --name plausible_clickhouse yandex/clickhouse-server:21.3.2.5
|
||||||
|
|
||||||
|
clickhouse-stop:
|
||||||
|
docker stop plausible_clickhouse && docker rm plausible_clickhouse
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
docker run --detach -e POSTGRES_PASSWORD="postgres" -p 5432:5432 postgres:12
|
docker run --detach -e POSTGRES_PASSWORD="postgres" -p 5432:5432 --volume=plausible_db:/var/lib/postgresql/data --name plausible_db postgres:12
|
||||||
|
|
||||||
|
postgres-stop:
|
||||||
|
docker stop plausible_db && docker rm plausible_db
|
||||||
|
|
||||||
dummy_event:
|
dummy_event:
|
||||||
curl 'http://localhost:8000/api/event' \
|
curl 'http://localhost:8000/api/event' \
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ iframe[hidden] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-link[disabled] {
|
.pagination-link[disabled] {
|
||||||
@apply cursor-default bg-gray-100 pointer-events-none;
|
@apply cursor-default bg-gray-100 dark:bg-gray-300 pointer-events-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-list-text:hover ~ .filter-list-edit {
|
.filter-list-text:hover ~ .filter-list-edit {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ app_version = System.get_env("APP_VERSION", "0.0.1")
|
||||||
ch_db_url =
|
ch_db_url =
|
||||||
System.get_env("CLICKHOUSE_DATABASE_URL", "http://plausible_events_db:8123/plausible_events_db")
|
System.get_env("CLICKHOUSE_DATABASE_URL", "http://plausible_events_db:8123/plausible_events_db")
|
||||||
|
|
||||||
|
{ch_flush_interval_ms, ""} = Integer.parse(System.get_env("CLICKHOUSE_FLUSH_INTERVAL_MS", "5000"))
|
||||||
|
{ch_max_buffer_size, ""} = Integer.parse(System.get_env("CLICKHOUSE_MAX_BUFFER_SIZE", "10000"))
|
||||||
|
|
||||||
### Mandatory params End
|
### Mandatory params End
|
||||||
|
|
||||||
sentry_dsn = System.get_env("SENTRY_DSN")
|
sentry_dsn = System.get_env("SENTRY_DSN")
|
||||||
|
|
@ -78,7 +81,6 @@ disable_registration = String.to_existing_atom(System.get_env("DISABLE_REGISTRAT
|
||||||
hcaptcha_sitekey = System.get_env("HCAPTCHA_SITEKEY")
|
hcaptcha_sitekey = System.get_env("HCAPTCHA_SITEKEY")
|
||||||
hcaptcha_secret = System.get_env("HCAPTCHA_SECRET")
|
hcaptcha_secret = System.get_env("HCAPTCHA_SECRET")
|
||||||
log_level = String.to_existing_atom(System.get_env("LOG_LEVEL", "warn"))
|
log_level = String.to_existing_atom(System.get_env("LOG_LEVEL", "warn"))
|
||||||
log_format = System.get_env("LOG_FORMAT", "elixir")
|
|
||||||
is_selfhost = String.to_existing_atom(System.get_env("SELFHOST", "true"))
|
is_selfhost = String.to_existing_atom(System.get_env("SELFHOST", "true"))
|
||||||
{site_limit, ""} = Integer.parse(System.get_env("SITE_LIMIT", "20"))
|
{site_limit, ""} = Integer.parse(System.get_env("SITE_LIMIT", "20"))
|
||||||
disable_cron = String.to_existing_atom(System.get_env("DISABLE_CRON", "false"))
|
disable_cron = String.to_existing_atom(System.get_env("DISABLE_CRON", "false"))
|
||||||
|
|
@ -130,7 +132,9 @@ config :plausible, Plausible.ClickhouseRepo,
|
||||||
loggers: [Ecto.LogEntry],
|
loggers: [Ecto.LogEntry],
|
||||||
queue_target: 500,
|
queue_target: 500,
|
||||||
queue_interval: 2000,
|
queue_interval: 2000,
|
||||||
url: ch_db_url
|
url: ch_db_url,
|
||||||
|
flush_interval_ms: ch_flush_interval_ms,
|
||||||
|
max_buffer_size: ch_max_buffer_size
|
||||||
|
|
||||||
case mailer_adapter do
|
case mailer_adapter do
|
||||||
"Bamboo.PostmarkAdapter" ->
|
"Bamboo.PostmarkAdapter" ->
|
||||||
|
|
@ -251,6 +255,9 @@ config :plausible, :user_agent_cache,
|
||||||
limit: user_agent_cache_limit,
|
limit: user_agent_cache_limit,
|
||||||
stats: user_agent_cache_stats
|
stats: user_agent_cache_stats
|
||||||
|
|
||||||
|
config :hammer,
|
||||||
|
backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
|
||||||
|
|
||||||
config :kaffy,
|
config :kaffy,
|
||||||
otp_app: :plausible,
|
otp_app: :plausible,
|
||||||
ecto_repo: Plausible.Repo,
|
ecto_repo: Plausible.Repo,
|
||||||
|
|
@ -280,22 +287,11 @@ if config_env() != :test && geolite2_country_db do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
logger_backends = %{
|
|
||||||
"elixir" => [:console],
|
|
||||||
"json" => [Ink]
|
|
||||||
}
|
|
||||||
|
|
||||||
config :logger,
|
config :logger,
|
||||||
level: log_level,
|
level: log_level,
|
||||||
backends: logger_backends[log_format]
|
backends: [:console]
|
||||||
|
|
||||||
config :logger, Sentry.LoggerBackend,
|
config :logger, Sentry.LoggerBackend,
|
||||||
capture_log_messages: true,
|
capture_log_messages: true,
|
||||||
level: :error,
|
level: :error,
|
||||||
excluded_domains: []
|
excluded_domains: []
|
||||||
|
|
||||||
if log_format == "json" do
|
|
||||||
config :logger, Ink,
|
|
||||||
name: "plausible",
|
|
||||||
level: log_level
|
|
||||||
end
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ config :geolix,
|
||||||
%{
|
%{
|
||||||
id: :country,
|
id: :country,
|
||||||
adapter: Geolix.Adapter.Fake,
|
adapter: Geolix.Adapter.Fake,
|
||||||
data: %{{1, 1, 1, 1} => %{country: %{iso_code: "US"}}}
|
data: %{
|
||||||
|
{1, 1, 1, 1} => %{country: %{iso_code: "US"}},
|
||||||
|
{1, 1, 1, 1, 1, 1, 1, 1} => %{country: %{iso_code: "US"}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ defmodule Plausible.Auth.ApiKey do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
@required [:user_id, :key, :name]
|
@required [:user_id, :key, :name]
|
||||||
@optional [:scopes]
|
@optional [:scopes, :hourly_request_limit]
|
||||||
schema "api_keys" do
|
schema "api_keys" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :scopes, {:array, :string}, default: ["stats:read:*"]
|
field :scopes, {:array, :string}, default: ["stats:read:*"]
|
||||||
|
field :hourly_request_limit, :integer
|
||||||
|
|
||||||
field :key, :string, virtual: true
|
field :key, :string, virtual: true
|
||||||
field :key_hash, :string
|
field :key_hash, :string
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ defmodule Plausible.Billing.Plans do
|
||||||
%{limit: 150_000_000, yearly_product_id: "648089", yearly_cost: "$4800"}
|
%{limit: 150_000_000, yearly_product_id: "648089", yearly_cost: "$4800"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@unlisted_plans_v2 [
|
||||||
|
%{limit: 10_000_000, monthly_product_id: "655350", yearly_cost: "$250"}
|
||||||
|
]
|
||||||
|
|
||||||
@v2_pricing_date ~D[2021-05-13]
|
@v2_pricing_date ~D[2021-05-13]
|
||||||
|
|
||||||
def plans_for(user) do
|
def plans_for(user) do
|
||||||
|
|
@ -70,7 +74,7 @@ defmodule Plausible.Billing.Plans do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp all_plans() do
|
defp all_plans() do
|
||||||
plans_v1() ++ @unlisted_plans_v1 ++ plans_v2()
|
plans_v1() ++ @unlisted_plans_v1 ++ plans_v2() ++ @unlisted_plans_v2
|
||||||
end
|
end
|
||||||
|
|
||||||
defp plans_v1() do
|
defp plans_v1() do
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
defmodule Plausible.Event.WriteBuffer do
|
defmodule Plausible.Event.WriteBuffer do
|
||||||
use GenServer
|
use GenServer
|
||||||
require Logger
|
require Logger
|
||||||
@flush_interval_ms 5_000
|
|
||||||
@max_buffer_size 10_000
|
|
||||||
|
|
||||||
def start_link(_opts) do
|
def start_link(_opts) do
|
||||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||||
|
|
@ -10,7 +8,7 @@ defmodule Plausible.Event.WriteBuffer do
|
||||||
|
|
||||||
def init(buffer) do
|
def init(buffer) do
|
||||||
Process.flag(:trap_exit, true)
|
Process.flag(:trap_exit, true)
|
||||||
timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:ok, %{buffer: buffer, timer: timer}}
|
{:ok, %{buffer: buffer, timer: timer}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -27,11 +25,11 @@ defmodule Plausible.Event.WriteBuffer do
|
||||||
def handle_cast({:insert, event}, %{buffer: buffer} = state) do
|
def handle_cast({:insert, event}, %{buffer: buffer} = state) do
|
||||||
new_buffer = [event | buffer]
|
new_buffer = [event | buffer]
|
||||||
|
|
||||||
if length(new_buffer) >= @max_buffer_size do
|
if length(new_buffer) >= max_buffer_size() do
|
||||||
Logger.info("Buffer full, flushing to disk")
|
Logger.info("Buffer full, flushing to disk")
|
||||||
Process.cancel_timer(state[:timer])
|
Process.cancel_timer(state[:timer])
|
||||||
do_flush(new_buffer)
|
do_flush(new_buffer)
|
||||||
new_timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
new_timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:noreply, %{buffer: [], timer: new_timer}}
|
{:noreply, %{buffer: [], timer: new_timer}}
|
||||||
else
|
else
|
||||||
{:noreply, %{state | buffer: new_buffer}}
|
{:noreply, %{state | buffer: new_buffer}}
|
||||||
|
|
@ -40,14 +38,14 @@ defmodule Plausible.Event.WriteBuffer do
|
||||||
|
|
||||||
def handle_info(:tick, %{buffer: buffer}) do
|
def handle_info(:tick, %{buffer: buffer}) do
|
||||||
do_flush(buffer)
|
do_flush(buffer)
|
||||||
timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:noreply, %{buffer: [], timer: timer}}
|
{:noreply, %{buffer: [], timer: timer}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call(:flush, _from, %{buffer: buffer} = state) do
|
def handle_call(:flush, _from, %{buffer: buffer} = state) do
|
||||||
Process.cancel_timer(state[:timer])
|
Process.cancel_timer(state[:timer])
|
||||||
do_flush(buffer)
|
do_flush(buffer)
|
||||||
new_timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
new_timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:reply, nil, %{buffer: [], timer: new_timer}}
|
{:reply, nil, %{buffer: [], timer: new_timer}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -67,4 +65,12 @@ defmodule Plausible.Event.WriteBuffer do
|
||||||
Plausible.ClickhouseRepo.insert_all(Plausible.ClickhouseEvent, events)
|
Plausible.ClickhouseRepo.insert_all(Plausible.ClickhouseEvent, events)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp flush_interval_ms() do
|
||||||
|
Keyword.fetch!(Application.get_env(:plausible, Plausible.ClickhouseRepo), :flush_interval_ms)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp max_buffer_size() do
|
||||||
|
Keyword.fetch!(Application.get_env(:plausible, Plausible.ClickhouseRepo), :max_buffer_size)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
defmodule Plausible.Session.WriteBuffer do
|
defmodule Plausible.Session.WriteBuffer do
|
||||||
use GenServer
|
use GenServer
|
||||||
require Logger
|
require Logger
|
||||||
@flush_interval_ms 1000
|
|
||||||
@max_buffer_size 10_000
|
|
||||||
|
|
||||||
def start_link(_opts) do
|
def start_link(_opts) do
|
||||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||||
|
|
@ -10,7 +8,7 @@ defmodule Plausible.Session.WriteBuffer do
|
||||||
|
|
||||||
def init(buffer) do
|
def init(buffer) do
|
||||||
Process.flag(:trap_exit, true)
|
Process.flag(:trap_exit, true)
|
||||||
timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:ok, %{buffer: buffer, timer: timer}}
|
{:ok, %{buffer: buffer, timer: timer}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -22,11 +20,11 @@ defmodule Plausible.Session.WriteBuffer do
|
||||||
def handle_cast({:insert, sessions}, %{buffer: buffer} = state) do
|
def handle_cast({:insert, sessions}, %{buffer: buffer} = state) do
|
||||||
new_buffer = sessions ++ buffer
|
new_buffer = sessions ++ buffer
|
||||||
|
|
||||||
if length(new_buffer) >= @max_buffer_size do
|
if length(new_buffer) >= max_buffer_size() do
|
||||||
Logger.info("Buffer full, flushing to disk")
|
Logger.info("Buffer full, flushing to disk")
|
||||||
Process.cancel_timer(state[:timer])
|
Process.cancel_timer(state[:timer])
|
||||||
flush(new_buffer)
|
flush(new_buffer)
|
||||||
new_timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
new_timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:noreply, %{buffer: [], timer: new_timer}}
|
{:noreply, %{buffer: [], timer: new_timer}}
|
||||||
else
|
else
|
||||||
{:noreply, %{state | buffer: new_buffer}}
|
{:noreply, %{state | buffer: new_buffer}}
|
||||||
|
|
@ -35,7 +33,7 @@ defmodule Plausible.Session.WriteBuffer do
|
||||||
|
|
||||||
def handle_info(:tick, %{buffer: buffer}) do
|
def handle_info(:tick, %{buffer: buffer}) do
|
||||||
flush(buffer)
|
flush(buffer)
|
||||||
timer = Process.send_after(self(), :tick, @flush_interval_ms)
|
timer = Process.send_after(self(), :tick, flush_interval_ms())
|
||||||
{:noreply, %{buffer: [], timer: timer}}
|
{:noreply, %{buffer: [], timer: timer}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -60,4 +58,12 @@ defmodule Plausible.Session.WriteBuffer do
|
||||||
Plausible.ClickhouseRepo.insert_all(Plausible.ClickhouseSession, sessions)
|
Plausible.ClickhouseRepo.insert_all(Plausible.ClickhouseSession, sessions)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp flush_interval_ms() do
|
||||||
|
Keyword.fetch!(Application.get_env(:plausible, Plausible.ClickhouseRepo), :flush_interval_ms)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp max_buffer_size() do
|
||||||
|
Keyword.fetch!(Application.get_env(:plausible, Plausible.ClickhouseRepo), :max_buffer_size)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -679,7 +679,7 @@ defmodule Plausible.Stats.Clickhouse do
|
||||||
|> Enum.into(%{})
|
|> Enum.into(%{})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp page_times_by_page_url(site, query, page_list) do
|
def page_times_by_page_url(site, query, page_list) do
|
||||||
q =
|
q =
|
||||||
from(
|
from(
|
||||||
e in base_query_w_sessions(site, %Query{
|
e in base_query_w_sessions(site, %Query{
|
||||||
|
|
@ -713,7 +713,10 @@ defmodule Plausible.Stats.Clickhouse do
|
||||||
FROM (#{base_query_raw}))
|
FROM (#{base_query_raw}))
|
||||||
WHERE s=s2 AND p IN tuple(?)
|
WHERE s=s2 AND p IN tuple(?)
|
||||||
GROUP BY p,p2,s)
|
GROUP BY p,p2,s)
|
||||||
GROUP BY p" |> ClickhouseRepo.query(base_query_raw_params ++ [page_list ++ ["/"]])
|
GROUP BY p"
|
||||||
|
|> ClickhouseRepo.query(
|
||||||
|
base_query_raw_params ++ [(Enum.count(page_list) > 0 && page_list) || ["/"]]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_percentages(stat_list) do
|
defp add_percentages(stat_list) do
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,11 @@ defmodule PlausibleWeb.Api.Helpers do
|
||||||
|> Phoenix.Controller.json(%{error: msg})
|
|> Phoenix.Controller.json(%{error: msg})
|
||||||
|> halt()
|
|> halt()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def too_many_requests(conn, msg) do
|
||||||
|
conn
|
||||||
|
|> put_status(429)
|
||||||
|
|> Phoenix.Controller.json(%{error: msg})
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,51 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
time_on_page =
|
||||||
|
if query.filters["page"] do
|
||||||
|
[{success, duration}, {prev_success, prev_duration}] =
|
||||||
|
Task.yield_many(
|
||||||
|
[
|
||||||
|
Task.async(fn ->
|
||||||
|
{:ok, page_times} =
|
||||||
|
Stats.page_times_by_page_url(site, query, [query.filters["page"]])
|
||||||
|
|
||||||
|
page_times
|
||||||
|
end),
|
||||||
|
Task.async(fn ->
|
||||||
|
{:ok, page_times} =
|
||||||
|
Stats.page_times_by_page_url(site, prev_query, [query.filters["page"]])
|
||||||
|
|
||||||
|
page_times
|
||||||
|
end)
|
||||||
|
],
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
|> Enum.map(fn {task, response} ->
|
||||||
|
case response do
|
||||||
|
nil ->
|
||||||
|
Task.shutdown(task, :brutal_kill)
|
||||||
|
{nil, nil}
|
||||||
|
|
||||||
|
{:ok, page_times} ->
|
||||||
|
result = Enum.at(page_times.rows, 0)
|
||||||
|
result = if result, do: Enum.at(result, 1), else: nil
|
||||||
|
if result, do: {:ok, round(result)}, else: {:ok, 0}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success == :ok && prev_success == :ok do
|
||||||
|
%{
|
||||||
|
name: "Time on Page",
|
||||||
|
duration: duration,
|
||||||
|
change: percent_change(prev_duration, duration)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
stats =
|
stats =
|
||||||
[
|
[
|
||||||
%{
|
%{
|
||||||
|
|
@ -117,7 +162,8 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||||
change: percent_change(prev_pageviews, pageviews)
|
change: percent_change(prev_pageviews, pageviews)
|
||||||
},
|
},
|
||||||
%{name: "Bounce rate", percentage: bounce_rate, change: change_bounce_rate},
|
%{name: "Bounce rate", percentage: bounce_rate, change: change_bounce_rate},
|
||||||
visit_duration
|
visit_duration,
|
||||||
|
time_on_page
|
||||||
]
|
]
|
||||||
|> Enum.filter(& &1)
|
|> Enum.filter(& &1)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,41 +227,77 @@ defmodule PlausibleWeb.AuthController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def login(conn, %{"email" => email, "password" => password}) do
|
def login(conn, %{"email" => email, "password" => password}) do
|
||||||
alias Plausible.Auth.Password
|
with :ok <- check_ip_rate_limit(conn),
|
||||||
|
{:ok, user} <- find_user(email),
|
||||||
|
:ok <- check_user_rate_limit(user),
|
||||||
|
:ok <- check_password(user, password) do
|
||||||
|
login_dest = get_session(conn, :login_dest) || "/sites"
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_session(:current_user_id, user.id)
|
||||||
|
|> put_resp_cookie("logged_in", "true",
|
||||||
|
http_only: false,
|
||||||
|
max_age: 60 * 60 * 24 * 365 * 5000
|
||||||
|
)
|
||||||
|
|> put_session(:login_dest, nil)
|
||||||
|
|> redirect(to: login_dest)
|
||||||
|
else
|
||||||
|
:wrong_password ->
|
||||||
|
render(conn, "login_form.html",
|
||||||
|
error: "Wrong email or password. Please try again.",
|
||||||
|
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||||
|
)
|
||||||
|
|
||||||
|
:user_not_found ->
|
||||||
|
Plausible.Auth.Password.dummy_calculation()
|
||||||
|
|
||||||
|
render(conn, "login_form.html",
|
||||||
|
error: "Wrong email or password. Please try again.",
|
||||||
|
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:rate_limit, _} ->
|
||||||
|
render_error(
|
||||||
|
conn,
|
||||||
|
429,
|
||||||
|
"Too many login attempts. Wait a minute before trying again."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@login_interval 60_000
|
||||||
|
@login_limit 5
|
||||||
|
defp check_ip_rate_limit(conn) do
|
||||||
|
ip_address = PlausibleWeb.RemoteIp.get(conn)
|
||||||
|
|
||||||
|
case Hammer.check_rate("login:ip:#{ip_address}", @login_interval, @login_limit) do
|
||||||
|
{:allow, _} -> :ok
|
||||||
|
{:deny, _} -> {:rate_limit, :ip_address}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_user(email) do
|
||||||
user =
|
user =
|
||||||
Repo.one(
|
Repo.one(
|
||||||
from u in Plausible.Auth.User,
|
from u in Plausible.Auth.User,
|
||||||
where: u.email == ^email
|
where: u.email == ^email
|
||||||
)
|
)
|
||||||
|
|
||||||
if user do
|
if user, do: {:ok, user}, else: :user_not_found
|
||||||
if Password.match?(password, user.password_hash || "") do
|
end
|
||||||
login_dest = get_session(conn, :login_dest) || "/sites"
|
|
||||||
|
|
||||||
conn
|
defp check_user_rate_limit(user) do
|
||||||
|> put_session(:current_user_id, user.id)
|
case Hammer.check_rate("login:user:#{user.id}", @login_interval, @login_limit) do
|
||||||
|> put_resp_cookie("logged_in", "true",
|
{:allow, _} -> :ok
|
||||||
http_only: false,
|
{:deny, _} -> {:rate_limit, :user}
|
||||||
max_age: 60 * 60 * 24 * 365 * 5000
|
end
|
||||||
)
|
end
|
||||||
|> put_session(:login_dest, nil)
|
|
||||||
|> redirect(to: login_dest)
|
defp check_password(user, password) do
|
||||||
else
|
if Plausible.Auth.Password.match?(password, user.password_hash || "") do
|
||||||
conn
|
:ok
|
||||||
|> render("login_form.html",
|
|
||||||
error: "Wrong email or password. Please try again.",
|
|
||||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
Password.dummy_calculation()
|
:wrong_password
|
||||||
|
|
||||||
conn
|
|
||||||
|> render("login_form.html",
|
|
||||||
error: "Wrong email or password. Please try again.",
|
|
||||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(conn, _opts) do
|
def call(conn, _opts) do
|
||||||
with {:ok, api_key} <- get_bearer_token(conn),
|
with {:ok, token} <- get_bearer_token(conn),
|
||||||
|
{:ok, api_key} <- find_api_key(token),
|
||||||
|
:ok <- check_api_key_rate_limit(api_key),
|
||||||
{:ok, site} <- verify_access(api_key, conn.params["site_id"]) do
|
{:ok, site} <- verify_access(api_key, conn.params["site_id"]) do
|
||||||
assign(conn, :site, site)
|
assign(conn, :site, site)
|
||||||
else
|
else
|
||||||
|
|
@ -25,6 +27,12 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||||
"Missing site ID. Please provide the required site_id parameter with your request."
|
"Missing site ID. Please provide the required site_id parameter with your request."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
{:error, :rate_limit, limit} ->
|
||||||
|
H.too_many_requests(
|
||||||
|
conn,
|
||||||
|
"Too many API requests. Your API key is limited to #{limit} requests per hour."
|
||||||
|
)
|
||||||
|
|
||||||
{:error, :invalid_api_key} ->
|
{:error, :invalid_api_key} ->
|
||||||
H.unauthorized(
|
H.unauthorized(
|
||||||
conn,
|
conn,
|
||||||
|
|
@ -36,13 +44,11 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||||
defp verify_access(_api_key, nil), do: {:error, :missing_site_id}
|
defp verify_access(_api_key, nil), do: {:error, :missing_site_id}
|
||||||
|
|
||||||
defp verify_access(api_key, site_id) do
|
defp verify_access(api_key, site_id) do
|
||||||
hashed_key = ApiKey.do_hash(api_key)
|
|
||||||
found_key = Repo.get_by(ApiKey, key_hash: hashed_key)
|
|
||||||
site = Repo.get_by(Plausible.Site, domain: site_id)
|
site = Repo.get_by(Plausible.Site, domain: site_id)
|
||||||
is_owner = site && found_key && Plausible.Sites.is_owner?(found_key.user_id, site)
|
is_owner = site && Plausible.Sites.is_owner?(api_key.user_id, site)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
found_key && site && is_owner -> {:ok, site}
|
site && is_owner -> {:ok, site}
|
||||||
true -> {:error, :invalid_api_key}
|
true -> {:error, :invalid_api_key}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -57,4 +63,18 @@ defmodule PlausibleWeb.AuthorizeStatsApiPlug do
|
||||||
_ -> {:error, :missing_api_key}
|
_ -> {:error, :missing_api_key}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp find_api_key(token) do
|
||||||
|
hashed_key = ApiKey.do_hash(token)
|
||||||
|
found_key = Repo.get_by(ApiKey, key_hash: hashed_key)
|
||||||
|
if found_key, do: {:ok, found_key}, else: {:error, :invalid_api_key}
|
||||||
|
end
|
||||||
|
|
||||||
|
@one_hour 60 * 60 * 1000
|
||||||
|
defp check_api_key_rate_limit(api_key) do
|
||||||
|
case Hammer.check_rate("api_request:#{api_key.id}", @one_hour, api_key.hourly_request_limit) do
|
||||||
|
{:allow, _} -> :ok
|
||||||
|
{:deny, _} -> {:error, :rate_limit, api_key.hourly_request_limit}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ defmodule PlausibleWeb.RemoteIp do
|
||||||
def get(conn) do
|
def get(conn) do
|
||||||
cf_connecting_ip = List.first(Plug.Conn.get_req_header(conn, "cf-connecting-ip"))
|
cf_connecting_ip = List.first(Plug.Conn.get_req_header(conn, "cf-connecting-ip"))
|
||||||
forwarded_for = List.first(Plug.Conn.get_req_header(conn, "x-forwarded-for"))
|
forwarded_for = List.first(Plug.Conn.get_req_header(conn, "x-forwarded-for"))
|
||||||
|
forwarded = List.first(Plug.Conn.get_req_header(conn, "forwarded"))
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
cf_connecting_ip ->
|
cf_connecting_ip ->
|
||||||
|
|
@ -12,6 +13,14 @@ defmodule PlausibleWeb.RemoteIp do
|
||||||
|> Enum.map(&String.trim/1)
|
|> Enum.map(&String.trim/1)
|
||||||
|> List.first()
|
|> List.first()
|
||||||
|
|
||||||
|
forwarded ->
|
||||||
|
Regex.named_captures(~r/for=(?<for>[^;,]+).*$/, forwarded)
|
||||||
|
|> Map.get("for")
|
||||||
|
# IPv6 addresses are enclosed in quote marks and square brackets: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
|
||||||
|
|> String.trim("\"")
|
||||||
|
|> String.trim_leading("[")
|
||||||
|
|> String.trim_trailing("]")
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
to_string(:inet_parse.ntoa(conn.remote_ip))
|
to_string(:inet_parse.ntoa(conn.remote_ip))
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ We're super excited to have you on board!
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Here are five steps to get the most out of your Plausible experience:
|
Here are five steps to get the most out of your Plausible experience:
|
||||||
<br /><br />
|
<br /><br />
|
||||||
* Serve the script as a <%= link("first party connection from your custom domain", to: "https://docs.plausible.io/custom-domain") %><br />
|
* <%= link("Enable email reports", to: "https://docs.plausible.io/email-reports/") %> and notifications for <%= link("traffic spikes", to: "https://plausible.io/docs/traffic-spikes") %><br />
|
||||||
* <%= link("Enable email reports", to: "https://docs.plausible.io/email-reports/") %> and notifications for <%= link("traffic spikes", to: "https://docs.plausible.io/traffic-spikes/") %><br />
|
* <%= link("Integrate with Search Console", to: "https://plausible.io/docs/google-search-console-integration") %> to get keyword phrases people find your site with<br />
|
||||||
* <%= link("Integrate with Search Console", to: "https://docs.plausible.io/google-search-console-integration/") %> to get keyword phrases people find your site with<br />
|
* Set up some easy goals including <%= link("404 error pages", to: "https://plausible.io/docs/404-error-pages-tracking") %> and <%= link("outbound link clicks", to: "https://plausible.io/docs/outbound-link-click-tracking/") %><br />
|
||||||
* Set up some easy goals including <%= link("404 error pages", to: "https://docs.plausible.io/404-error-pages-tracking") %> and <%= link("outbound link clicks", to: "https://docs.plausible.io/outbound-link-click-tracking/") %><br />
|
* <%= link("Opt out from counting your own visits", to: "https://plausible.io/docs/excluding") %><br />
|
||||||
* <%= link("Opt out from counting your own visits", to: "https://docs.plausible.io/excluding/") %><br />
|
* If you're concerned about adblockers, <%= link("set up a proxy to bypass them", to: "https://plausible.io/docs/proxy/introduction") %><br />
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Then you're ready to start exploring your fast loading, ethical and actionable <%= link("Plausible dashboard", to: "https://plausible.io/sites") %>.
|
Then you're ready to start exploring your fast loading, ethical and actionable <%= link("Plausible dashboard", to: "https://plausible.io/sites") %>.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<%= if !Application.get_env(:plausible, :is_selfhost) && !@conn.assigns[:skip_plausible_tracking] do %>
|
<%= if !Application.get_env(:plausible, :is_selfhost) && !@conn.assigns[:skip_plausible_tracking] do %>
|
||||||
<script async defer src="<%="#{plausible_url()}/js/script.js"%>"></script>
|
<script defer src="https://testing-plausible-io-proxy.uku-taht.workers.dev/js/script.js"></script>
|
||||||
|
<script defer src="<%="#{plausible_url()}/js/script.js"%>"></script>
|
||||||
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@
|
||||||
|
|
||||||
<%= if @pagination.total_pages > 1 do %>
|
<%= if @pagination.total_pages > 1 do %>
|
||||||
<%= pagination @conn, @pagination, [current_class: "is-current"], fn p -> %>
|
<%= pagination @conn, @pagination, [current_class: "is-current"], fn p -> %>
|
||||||
<nav class="px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6" aria-label="Pagination">
|
<nav class="px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-gray-500 sm:px-6" aria-label="Pagination">
|
||||||
<div class="hidden sm:block">
|
<div class="hidden sm:block">
|
||||||
<p class="text-sm text-gray-700">
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
Showing page
|
Showing page
|
||||||
<span class="font-medium"><%= @pagination.page %></span>
|
<span class="font-medium"><%= @pagination.page %></span>
|
||||||
of
|
of
|
||||||
|
|
@ -51,8 +51,8 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex justify-between sm:justify-end">
|
<div class="flex-1 flex justify-between sm:justify-end">
|
||||||
<%= pagination_link(p, :previous, label: "← Previous", class: "pagination-link relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50", force_show: true) %>
|
<%= pagination_link(p, :previous, label: "← Previous", class: "pagination-link relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-100 hover:bg-gray-50", force_show: true) %>
|
||||||
<%= pagination_link(p, :next, label: "Next →", class: "pagination-link ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50", force_show: true) %>
|
<%= pagination_link(p, :next, label: "Next →", class: "pagination-link ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-100 hover:bg-gray-50", force_show: true) %>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,24 @@
|
||||||
|
<div class="rounded-md bg-yellow-50 dark:bg-transparent dark:border border-yellow-200 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-yellow-400 dark:text-yellow-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-400">
|
||||||
|
Deprecated feature
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
|
||||||
|
<p>
|
||||||
|
We are moving away from CNAME-based custom domains. If you're concerned about adblockers, we recommend
|
||||||
|
<%= link("setting up a proxy", class: "underline text-yellow-800 dark:text-yellow-400", to: "https://plausible.io/docs/proxy/introduction", target: "_blank") %> for your analytics script instead.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
<div class="shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6">
|
||||||
<header class="relative">
|
<header class="relative">
|
||||||
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Custom domain</h2>
|
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Custom domain</h2>
|
||||||
|
|
|
||||||
4
mix.exs
4
mix.exs
|
|
@ -95,8 +95,8 @@ defmodule Plausible.MixProject do
|
||||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||||
{:kaffy, "~> 0.9.0"},
|
{:kaffy, "~> 0.9.0"},
|
||||||
{:envy, "~> 1.1.1"},
|
{:envy, "~> 1.1.1"},
|
||||||
{:ink, "~> 1.0"},
|
{:phoenix_pagination, "~> 0.7.0"},
|
||||||
{:phoenix_pagination, "~> 0.7.0"}
|
{:hammer, "~> 6.0"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
1
mix.lock
1
mix.lock
|
|
@ -44,6 +44,7 @@
|
||||||
"geolix_adapter_mmdb2": {:hex, :geolix_adapter_mmdb2, "0.5.0", "5912723d9538ecddc6b29b1d8041b917b735a78fd3c122bfea8c44aa782e3369", [:mix], [{:geolix, "~> 1.1", [hex: :geolix, repo: "hexpm", optional: false]}, {:mmdb2_decoder, "~> 3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}], "hexpm", "cb1485b6a0a2d3e541949207428a245718dbf1258453a0df0e5fdd925bcecd3e"},
|
"geolix_adapter_mmdb2": {:hex, :geolix_adapter_mmdb2, "0.5.0", "5912723d9538ecddc6b29b1d8041b917b735a78fd3c122bfea8c44aa782e3369", [:mix], [{:geolix, "~> 1.1", [hex: :geolix, repo: "hexpm", optional: false]}, {:mmdb2_decoder, "~> 3.0", [hex: :mmdb2_decoder, repo: "hexpm", optional: false]}], "hexpm", "cb1485b6a0a2d3e541949207428a245718dbf1258453a0df0e5fdd925bcecd3e"},
|
||||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
||||||
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
|
"hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"},
|
||||||
|
"hammer": {:hex, :hammer, "6.0.0", "72ec6fff10e9d63856968988a22ee04c4d6d5248071ddccfbda50aa6c455c1d7", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "d8e1ec2e534c4aae508b906759e077c3c1eb3e2b9425235d4b7bbab0b016210a"},
|
||||||
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
|
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
|
||||||
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
|
"hut": {:hex, :hut, "1.3.0", "71f2f054e657c03f959cf1acc43f436ea87580696528ca2a55c8afb1b06c85e7", [:"erlang.mk", :rebar, :rebar3], [], "hexpm", "7e15d28555d8a1f2b5a3a931ec120af0753e4853a4c66053db354f35bf9ab563"},
|
||||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Plausible.Repo.Migrations.AddRateLimitToApiKeys do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:api_keys) do
|
||||||
|
add :hourly_request_limit, :integer, null: false, default: 1000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
defmodule Plausible.Billing.PlansTest do
|
|
||||||
use Plausible.DataCase
|
|
||||||
use Bamboo.Test, shared: true
|
|
||||||
alias Plausible.Billing.Plans
|
|
||||||
end
|
|
||||||
|
|
@ -510,6 +510,45 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
|
||||||
assert pageview.country_code == "US"
|
assert pageview.country_code == "US"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "Uses the Forwarded header when cf-connecting-ip and x-forwarded-for are missing", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
params = %{
|
||||||
|
name: "pageview",
|
||||||
|
domain: "external-controller-test-forwarded.com",
|
||||||
|
url: "http://gigride.live/"
|
||||||
|
}
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "text/plain")
|
||||||
|
|> put_req_header("forwarded", "by=0.0.0.0;for=1.1.1.1;host=somehost.com;proto=https")
|
||||||
|
|> post("/api/event", Jason.encode!(params))
|
||||||
|
|
||||||
|
pageview = get_event("external-controller-test-forwarded.com")
|
||||||
|
|
||||||
|
assert pageview.country_code == "US"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "Forwarded header can parse ipv6", %{conn: conn} do
|
||||||
|
params = %{
|
||||||
|
name: "pageview",
|
||||||
|
domain: "external-controller-test-forwarded-ipv6.com",
|
||||||
|
url: "http://gigride.live/"
|
||||||
|
}
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "text/plain")
|
||||||
|
|> put_req_header(
|
||||||
|
"forwarded",
|
||||||
|
"by=0.0.0.0;for=\"[1:1:1:1:1:1:1:1]\",for=0.0.0.0;host=somehost.com;proto=https"
|
||||||
|
)
|
||||||
|
|> post("/api/event", Jason.encode!(params))
|
||||||
|
|
||||||
|
pageview = get_event("external-controller-test-forwarded-ipv6.com")
|
||||||
|
|
||||||
|
assert pageview.country_code == "US"
|
||||||
|
end
|
||||||
|
|
||||||
test "URL is decoded", %{conn: conn} do
|
test "URL is decoded", %{conn: conn} do
|
||||||
params = %{
|
params = %{
|
||||||
name: "pageview",
|
name: "pageview",
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,29 @@ defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do
|
||||||
"Missing site ID. Please provide the required site_id parameter with your request."
|
"Missing site ID. Please provide the required site_id parameter with your request."
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "limits the rate of API requests", %{user: user} do
|
||||||
|
api_key = insert(:api_key, user_id: user.id, hourly_request_limit: 3)
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||||
|
|> get("/api/v1/stats/aggregate")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||||
|
|> get("/api/v1/stats/aggregate")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||||
|
|> get("/api/v1/stats/aggregate")
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}")
|
||||||
|
|> get("/api/v1/stats/aggregate")
|
||||||
|
|
||||||
|
assert json_response(conn, 429) == %{
|
||||||
|
"error" => "Too many API requests. Your API key is limited to 3 requests per hour."
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,38 @@ defmodule PlausibleWeb.AuthControllerTest do
|
||||||
assert get_session(conn, :current_user_id) == nil
|
assert get_session(conn, :current_user_id) == nil
|
||||||
assert html_response(conn, 200) =~ "Enter your email and password"
|
assert html_response(conn, 200) =~ "Enter your email and password"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "limits login attempts to 5 per minute" do
|
||||||
|
user = insert(:user, password: "password")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("x-forwarded-for", "1.1.1.1")
|
||||||
|
|> post("/login", email: user.email, password: "wrong")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("x-forwarded-for", "1.1.1.1")
|
||||||
|
|> post("/login", email: user.email, password: "wrong")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("x-forwarded-for", "1.1.1.1")
|
||||||
|
|> post("/login", email: user.email, password: "wrong")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("x-forwarded-for", "1.1.1.1")
|
||||||
|
|> post("/login", email: user.email, password: "wrong")
|
||||||
|
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("x-forwarded-for", "1.1.1.1")
|
||||||
|
|> post("/login", email: user.email, password: "wrong")
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> put_req_header("x-forwarded-for", "1.1.1.1")
|
||||||
|
|> post("/login", email: user.email, password: "wrong")
|
||||||
|
|
||||||
|
assert get_session(conn, :current_user_id) == nil
|
||||||
|
assert html_response(conn, 429) =~ "Too many login attempts"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /password/request-reset" do
|
describe "GET /password/request-reset" do
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
// with some early customers. This script uses a cookie but this was an old version of Plausible.
|
// with some early customers. This script uses a cookie but this was an old version of Plausible.
|
||||||
// Current script can be found in the tracker/src/plausible.js file
|
// Current script can be found in the tracker/src/plausible.js file
|
||||||
|
|
||||||
(function(window, plausibleHost){
|
(function(){
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var plausibleHost = new URL(scriptEl.src).origin
|
||||||
|
|
||||||
function setCookie(name,value) {
|
function setCookie(name,value) {
|
||||||
var date = new Date();
|
var date = new Date();
|
||||||
date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS
|
date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS
|
||||||
|
|
@ -117,4 +119,4 @@
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Image().src = plausibleHost + '/api/error?message=' + encodeURIComponent(e.message);
|
new Image().src = plausibleHost + '/api/error?message=' + encodeURIComponent(e.message);
|
||||||
}
|
}
|
||||||
})(window, '<%= base_url %>');
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue