Upgrade Erlang/Elixir stack (#3454)

* Bump deps

* Bump stack

* Fix deprecation warnings

* Fix VCR cassettes mismatch due to OTP-18414

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Format & fix flaky tests

* Handle raw IPv4 hostnames; test public suffix TLD

* Configure locus db cache_dir

So that maxmind unavailability doesn't affect
application startup. PERSISTENT_CACHE_DIR env var is used
to point locus at the GeoIP DB file.

* WIP: Remove ExVCR

* Fix test env config

* Fixup exvcr

* Remove exvcr from deps

* Add convert script

* Remove exvcr cassettes

* Remove convert script

* Rename test

* Update moduledoc

* Update dockerfile

* Bump CI cache

* Tag more slow tests, why not?

* Use charlist for locus cache option

* Pin nodejs

* Merge google tests, make them async

---------

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
This commit is contained in:
hq1 2023-10-24 10:33:48 +02:00 committed by GitHub
parent 2ada3d700f
commit 117eef000d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 843049 additions and 696 deletions

View File

@ -62,8 +62,8 @@ jobs:
deps
_build
priv/plts
key: ${{ runner.os }}-mix-v5-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-v5-
key: ${{ runner.os }}-mix-v6-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-v6-
- name: Install dependencies
run: mix deps.get && npm install --prefix ./tracker
- name: Check Formatting

View File

@ -4,7 +4,7 @@ alias Plausible.{Site, Sites, Goal, Goals, Stats}
import_if_available(Ecto.Query)
import_if_available(Plausible.Factory)
Logger.configure(level: :warn)
Logger.configure(level: :warning)
IO.puts(
IO.ANSI.cyan() <>

View File

@ -1,4 +1,3 @@
erlang 25.2.3
elixir 1.14.3-otp-25
nodejs 18.17.1
python 3.9.12
erlang 26.1.2
elixir 1.15.7-otp-26
nodejs 21.0.0

View File

@ -2,7 +2,7 @@
# platform specific, it makes sense to build it in the docker
#### Builder
FROM hexpm/elixir:1.14.3-erlang-25.2.3-alpine-3.18.0 as buildcontainer
FROM hexpm/elixir:1.15.7-erlang-26.1.2-alpine-3.18.4 as buildcontainer
# preparation
ENV MIX_ENV=prod
@ -53,7 +53,7 @@ COPY rel rel
RUN mix release plausible
# Main Docker Image
FROM alpine:3.18.0
FROM alpine:3.18.4
LABEL maintainer="plausible.io <hello@plausible.io>"
ARG BUILD_METADATA={}

View File

@ -3,7 +3,7 @@ CLICKHOUSE_DATABASE_URL=http://127.0.0.1:8123/plausible_test
SECRET_KEY_BASE=/njrhntbycvastyvtk1zycwfm981vpo/0xrvwjjvemdakc/vsvbrevlwsc6u8rcg
BASE_URL=http://localhost:8000
CRON_ENABLED=false
LOG_LEVEL=warn
LOG_LEVEL=warning
ENVIRONMENT=test
MAILER_ADAPTER=Bamboo.TestAdapter
ENABLE_EMAIL_VERIFICATION=true
@ -12,3 +12,5 @@ HCAPTCHA_SITEKEY=test
HCAPTCHA_SECRET=scottiger
IP_GEOLOCATION_DB=test/priv/GeoLite2-City-Test.mmdb
SITE_DEFAULT_INGEST_THRESHOLD=1000000
GOOGLE_CLIENT_ID=fake_client_id
GOOGLE_CLIENT_SECRET=fake_client_secret

View File

@ -119,13 +119,13 @@ build_metadata =
{:error, error} ->
error = Exception.format(:error, error)
Logger.warn("""
Logger.warning("""
failed to parse $BUILD_METADATA: #{error}
$BUILD_METADATA is set to #{build_metadata_raw}\
""")
Logger.warn("falling back to empty build metadata, as if $BUILD_METADATA was set to {}")
Logger.warning("falling back to empty build metadata, as if $BUILD_METADATA was set to {}")
_fallback = %{}
end
@ -164,10 +164,11 @@ ip_geolocation_db = get_var_from_path_or_env(config_dir, "IP_GEOLOCATION_DB", ge
geonames_source_file = get_var_from_path_or_env(config_dir, "GEONAMES_SOURCE_FILE")
maxmind_license_key = get_var_from_path_or_env(config_dir, "MAXMIND_LICENSE_KEY")
maxmind_edition = get_var_from_path_or_env(config_dir, "MAXMIND_EDITION", "GeoLite2-City")
maxmind_cache_dir = get_var_from_path_or_env(config_dir, "PERSISTENT_CACHE_DIR")
if System.get_env("DISABLE_AUTH") do
require Logger
Logger.warn("DISABLE_AUTH env var is no longer supported")
Logger.warning("DISABLE_AUTH env var is no longer supported")
end
enable_email_verification =
@ -560,6 +561,7 @@ geo_opts =
[
license_key: maxmind_license_key,
edition: maxmind_edition,
cache_dir: maxmind_cache_dir,
async: true
]

View File

@ -18,10 +18,6 @@ config :plausible,
paddle_api: Plausible.PaddleApi.Mock,
google_api: Plausible.Google.Api.Mock
config :plausible, :google,
client_id: "fake_client_id",
client_secret: "fake_client_secret"
config :bamboo, :refute_timeout, 10
config :plausible,

View File

@ -0,0 +1,18 @@
[
{
"status": 400,
"url": "https://www.googleapis.com/oauth2/v4/token",
"method": "post",
"request_body": {
"client_id": "fake_client_id",
"client_secret": "fake_client_secret",
"grant_type": "refresh_token",
"redirect_uri": "http://localhost:8000/auth/google/callback",
"refresh_token": "*****"
},
"response_body": {
"error": "invalid_grant",
"error_description": "Bad Request"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
[
{
"status": 200,
"url": "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
"method": "post",
"request_body": {
"dimensionFilterGroups": [
{
"filters": [
{
"dimension": "page",
"expression": "https://sc-domain%3Adummy.test5"
}
]
}
],
"dimensions": [
"query"
],
"endDate": "2022-01-05",
"rowLimit": 5,
"startDate": "2022-01-01"
},
"response_body": {
"responseAggregationType": "auto",
"rows": [
{
"clicks": 25.0,
"ctr": 0.3,
"impressions": 50.0,
"keys": [
"keyword1",
"keyword2"
],
"position": 2.0
},
{
"clicks": 15.0,
"ctr": 0.5,
"impressions": 25.0,
"keys": [
"keyword3",
"keyword4"
],
"position": 4.0
}
]
}
}
]

View File

@ -0,0 +1,41 @@
[
{
"status": 200,
"url": "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
"method": "post",
"request_body": {
"dimensionFilterGroups": {},
"dimensions": [
"query"
],
"endDate": "2022-01-05",
"rowLimit": 5,
"startDate": "2022-01-01"
},
"response_body": {
"responseAggregationType": "auto",
"rows": [
{
"clicks": 25.0,
"ctr": 0.3,
"impressions": 50.0,
"keys": [
"keyword1",
"keyword2"
],
"position": 2.0
},
{
"clicks": 15.0,
"ctr": 0.5,
"impressions": 25.0,
"keys": [
"keyword3",
"keyword4"
],
"position": 4.0
}
]
}
}
]

View File

@ -0,0 +1,41 @@
[
{
"status": 200,
"url": "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query",
"method": "post",
"request_body": {
"dimensionFilterGroups": {},
"dimensions": [
"query"
],
"endDate": "2022-01-05",
"rowLimit": 5,
"startDate": "2022-01-01"
},
"response_body": {
"responseAggregationType": "auto",
"rows": [
{
"clicks": 25.0,
"ctr": 0.3,
"impressions": 50.0,
"keys": [
"keyword1",
"keyword2"
],
"position": 2.0
},
{
"clicks": 15.0,
"ctr": 0.5,
"impressions": 25.0,
"keys": [
"keyword3",
"keyword4"
],
"position": 4.0
}
]
}
}
]

View File

@ -1,35 +0,0 @@
[
{
"request": {
"body": "client_id=fake_client_id&client_secret=fake_client_secret&refresh_token=*****&grant_type=refresh_token&redirect_uri=http://localhost:8000/auth/google/callback",
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"method": "post",
"options": [],
"request_body": "",
"url": "https://www.googleapis.com/oauth2/v4/token"
},
"response": {
"binary": false,
"body": "{\n \"error\": \"invalid_grant\",\n \"error_description\": \"Bad Request\"\n}",
"headers": {
"date": "Fri, 12 Aug 2022 16:43:57 GMT",
"pragma": "no-cache",
"cache-control": "no-cache, no-store, max-age=0, must-revalidate",
"expires": "Mon, 01 Jan 1990 00:00:00 GMT",
"content-type": "application/json; charset=utf-8",
"vary": "X-Origin",
"server": "scaffolding on HTTPServer2",
"x-xss-protection": "0",
"x-frame-options": "SAMEORIGIN",
"x-content-type-options": "nosniff",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
"accept-ranges": "none",
"transfer-encoding": "chunked"
},
"status_code": 400,
"type": "ok"
}
}
]

File diff suppressed because one or more lines are too long

View File

@ -1,33 +0,0 @@
[
{
"request": {
"body": "{\"dimensionFilterGroups\":[{\"filters\":[{\"dimension\":\"page\",\"expression\":\"https://sc-domain%3Adummy.test5\"}]}],\"dimensions\":[\"query\"],\"endDate\":\"2022-01-05\",\"rowLimit\":5,\"startDate\":\"2022-01-01\"}",
"headers": {
"Authorization": "Bearer 123"
},
"method": "post",
"options": [],
"request_body": "",
"url": "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query"
},
"response": {
"binary": false,
"body": "{\"rows\":[{\"keys\":[\"keyword1\",\"keyword2\"],\"clicks\":25.0,\"impressions\":50.0,\"ctr\":0.3,\"position\":2.0},{\"keys\":[\"keyword3\",\"keyword4\"],\"clicks\":15.0,\"impressions\":25.0,\"ctr\":0.5,\"position\":4.0}],\"responseAggregationType\":\"auto\"}",
"headers": {
"vary": "X-Origin",
"content-type": "application/json; charset=UTF-8",
"date": "Wed, 10 Aug 2022 14:55:07 GMT",
"server": "ESF",
"cache-control": "private",
"x-xss-protection": "0",
"x-frame-options": "SAMEORIGIN",
"x-content-type-options": "nosniff",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
"accept-ranges": "none",
"transfer-encoding": "chunked"
},
"status_code": 200,
"type": "ok"
}
}
]

View File

@ -1,33 +0,0 @@
[
{
"request": {
"body": "{\"dimensionFilterGroups\":{},\"dimensions\":[\"query\"],\"endDate\":\"2022-01-05\",\"rowLimit\":5,\"startDate\":\"2022-01-01\"}",
"headers": {
"Authorization": "Bearer 123"
},
"method": "post",
"options": [],
"request_body": "",
"url": "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query"
},
"response": {
"binary": false,
"body": "{\"rows\":[{\"keys\":[\"keyword1\",\"keyword2\"],\"clicks\":25.0,\"impressions\":50.0,\"ctr\":0.3,\"position\":2.0},{\"keys\":[\"keyword3\",\"keyword4\"],\"clicks\":15.0,\"impressions\":25.0,\"ctr\":0.5,\"position\":4.0}],\"responseAggregationType\":\"auto\"}",
"headers": {
"vary": "X-Origin",
"content-type": "application/json; charset=UTF-8",
"date": "Wed, 10 Aug 2022 14:55:07 GMT",
"server": "ESF",
"cache-control": "private",
"x-xss-protection": "0",
"x-frame-options": "SAMEORIGIN",
"x-content-type-options": "nosniff",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
"accept-ranges": "none",
"transfer-encoding": "chunked"
},
"status_code": 200,
"type": "ok"
}
}
]

View File

@ -1,33 +0,0 @@
[
{
"request": {
"body": "{\"dimensionFilterGroups\":{},\"dimensions\":[\"query\"],\"endDate\":\"2022-01-05\",\"rowLimit\":5,\"startDate\":\"2022-01-01\"}",
"headers": {
"Authorization": "Bearer 123"
},
"method": "post",
"options": [],
"request_body": "",
"url": "https://www.googleapis.com/webmasters/v3/sites/sc-domain%3Adummy.test/searchAnalytics/query"
},
"response": {
"binary": false,
"body": "{\"rows\":[{\"keys\":[\"keyword1\",\"keyword2\"],\"clicks\":25.0,\"impressions\":50.0,\"ctr\":0.3,\"position\":2.0},{\"keys\":[\"keyword3\",\"keyword4\"],\"clicks\":15.0,\"impressions\":25.0,\"ctr\":0.5,\"position\":4.0}],\"responseAggregationType\":\"auto\"}",
"headers": {
"vary": "X-Origin",
"content-type": "application/json; charset=UTF-8",
"date": "Wed, 10 Aug 2022 14:55:07 GMT",
"server": "ESF",
"cache-control": "private",
"x-xss-protection": "0",
"x-frame-options": "SAMEORIGIN",
"x-content-type-options": "nosniff",
"alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"",
"accept-ranges": "none",
"transfer-encoding": "chunked"
},
"status_code": 200,
"type": "ok"
}
}
]

View File

@ -16,7 +16,7 @@ defmodule Mix.Tasks.GenerateReferrerFavicons do
domains =
Enum.reduce(entries, %{}, fn {key, val}, domains ->
domain =
Enum.into(val, %{})['domains']
Enum.into(val, %{})[~c"domains"]
|> List.first()
Map.put_new(domains, List.to_string(key), List.to_string(domain))

View File

@ -94,7 +94,9 @@ defmodule Plausible.Application do
google_conf[:client_id] && google_conf[:client_secret] ->
pool_config
|> Map.put(google_conf[:api_url], conn_opts: [transport_opts: [timeout: 15_000]])
|> Map.put(google_conf[:reporting_api_url], conn_opts: [transport_opts: [timeout: 15_000]])
|> Map.put(google_conf[:reporting_api_url],
conn_opts: [transport_opts: [timeout: 15_000]]
)
true ->
pool_config

View File

@ -42,7 +42,17 @@ defmodule Plausible.Geo do
cond do
license_key = opts[:license_key] ->
edition = opts[:edition] || "GeoLite2-City"
if is_binary(opts[:cache_dir]) do
:ok =
:locus.start_loader(@db, {:maxmind, edition},
license_key: license_key,
database_cache_file:
String.to_charlist(Path.join(opts[:cache_dir], edition <> ".mmdb.gz"))
)
else
:ok = :locus.start_loader(@db, {:maxmind, edition}, license_key: license_key)
end
path = opts[:path] ->
:ok = :locus.start_loader(@db, path)

View File

@ -44,7 +44,7 @@ defmodule Plausible.Google.HTTP do
Sentry.Context.set_extra_context(%{ga_response: %{body: body, status: status}})
{:error, :request_failed}
{:error, _} ->
{:error, _reason} ->
{:error, :request_failed}
end
end

View File

@ -441,9 +441,12 @@ defmodule Plausible.Ingestion.Event do
defp get_root_domain(nil), do: "(none)"
defp get_root_domain(hostname) do
case PublicSuffix.registrable_domain(hostname) do
domain when is_binary(domain) -> domain
_any -> hostname
case :inet.parse_ipv4_address(String.to_charlist(hostname)) do
{:ok, _} ->
hostname
{:error, :einval} ->
PublicSuffix.registrable_domain(hostname) || hostname
end
end

View File

@ -29,14 +29,14 @@ defmodule Plausible.Sentry.Client do
case resp do
{:ok, %{status: status, headers: _}} when is_redirect(status) ->
# Just playing safe here. hackney client didn't support those; redirects are opt-in in hackney
Logger.warn("Sentry returned a redirect that is not handled yet.")
Logger.warning("Sentry returned a redirect that is not handled yet.")
{:error, :stop}
{:ok, %{status: status, body: body, headers: headers}} ->
{:ok, status, headers, body}
{:error, error} = e ->
Logger.warn("Sentry call failed with: #{inspect(error)}")
Logger.warning("Sentry call failed with: #{inspect(error)}")
e
end
end

View File

@ -146,7 +146,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
Multi.put(multi, :previous_owner_membership, previous_owner)
nil ->
Logger.warn(
Logger.warning(
"Transferring ownership from a site with no owner: #{site.domain} " <>
", new owner ID: #{new_owner_id}"
)

View File

@ -308,7 +308,10 @@ defmodule PlausibleWeb.AuthController do
settings_changeset = Auth.User.settings_changeset(conn.assigns[:current_user])
email_changeset = Auth.User.settings_changeset(conn.assigns[:current_user])
render_settings(conn, settings_changeset: settings_changeset, email_changeset: email_changeset)
render_settings(conn,
settings_changeset: settings_changeset,
email_changeset: email_changeset
)
end
def save_settings(conn, %{"user" => user_params}) do

View File

@ -123,34 +123,34 @@ defmodule PlausibleWeb.StatsController do
|> Enum.join()
filename =
'Plausible export #{params["domain"]} #{Timex.format!(query.date_range.first, "{ISOdate} ")} to #{Timex.format!(query.date_range.last, "{ISOdate} ")}.zip'
~c"Plausible export #{params["domain"]} #{Timex.format!(query.date_range.first, "{ISOdate} ")} to #{Timex.format!(query.date_range.last, "{ISOdate} ")}.zip"
params = Map.merge(params, %{"limit" => "300", "csv" => "True", "detailed" => "True"})
limited_params = Map.merge(params, %{"limit" => "100"})
csvs = %{
'sources.csv' => fn -> Api.StatsController.sources(conn, params) end,
'utm_mediums.csv' => fn -> Api.StatsController.utm_mediums(conn, params) end,
'utm_sources.csv' => fn -> Api.StatsController.utm_sources(conn, params) end,
'utm_campaigns.csv' => fn -> Api.StatsController.utm_campaigns(conn, params) end,
'utm_contents.csv' => fn -> Api.StatsController.utm_contents(conn, params) end,
'utm_terms.csv' => fn -> Api.StatsController.utm_terms(conn, params) end,
'pages.csv' => fn -> Api.StatsController.pages(conn, limited_params) end,
'entry_pages.csv' => fn -> Api.StatsController.entry_pages(conn, params) end,
'exit_pages.csv' => fn -> Api.StatsController.exit_pages(conn, limited_params) end,
'countries.csv' => fn -> Api.StatsController.countries(conn, params) end,
'regions.csv' => fn -> Api.StatsController.regions(conn, params) end,
'cities.csv' => fn -> Api.StatsController.cities(conn, params) end,
'browsers.csv' => fn -> Api.StatsController.browsers(conn, params) end,
'operating_systems.csv' => fn -> Api.StatsController.operating_systems(conn, params) end,
'devices.csv' => fn -> Api.StatsController.screen_sizes(conn, params) end,
'conversions.csv' => fn -> Api.StatsController.conversions(conn, params) end,
'referrers.csv' => fn -> Api.StatsController.referrers(conn, params) end
~c"sources.csv" => fn -> Api.StatsController.sources(conn, params) end,
~c"utm_mediums.csv" => fn -> Api.StatsController.utm_mediums(conn, params) end,
~c"utm_sources.csv" => fn -> Api.StatsController.utm_sources(conn, params) end,
~c"utm_campaigns.csv" => fn -> Api.StatsController.utm_campaigns(conn, params) end,
~c"utm_contents.csv" => fn -> Api.StatsController.utm_contents(conn, params) end,
~c"utm_terms.csv" => fn -> Api.StatsController.utm_terms(conn, params) end,
~c"pages.csv" => fn -> Api.StatsController.pages(conn, limited_params) end,
~c"entry_pages.csv" => fn -> Api.StatsController.entry_pages(conn, params) end,
~c"exit_pages.csv" => fn -> Api.StatsController.exit_pages(conn, limited_params) end,
~c"countries.csv" => fn -> Api.StatsController.countries(conn, params) end,
~c"regions.csv" => fn -> Api.StatsController.regions(conn, params) end,
~c"cities.csv" => fn -> Api.StatsController.cities(conn, params) end,
~c"browsers.csv" => fn -> Api.StatsController.browsers(conn, params) end,
~c"operating_systems.csv" => fn -> Api.StatsController.operating_systems(conn, params) end,
~c"devices.csv" => fn -> Api.StatsController.screen_sizes(conn, params) end,
~c"conversions.csv" => fn -> Api.StatsController.conversions(conn, params) end,
~c"referrers.csv" => fn -> Api.StatsController.referrers(conn, params) end
}
csvs =
if Plausible.Billing.Feature.Props.enabled?(site) do
Map.put(csvs, 'custom_props.csv', fn ->
Map.put(csvs, ~c"custom_props.csv", fn ->
Api.StatsController.all_custom_prop_values(conn, params)
end)
else
@ -165,7 +165,7 @@ defmodule PlausibleWeb.StatsController do
Map.keys(csvs)
|> Enum.zip(csv_values)
csvs = [{'visitors.csv', visitors} | csvs]
csvs = [{~c"visitors.csv", visitors} | csvs]
{:ok, {_, zip_content}} = :zip.create(filename, csvs, [:memory])

View File

@ -74,8 +74,7 @@ defmodule Plausible.MixProject do
{:envy, "~> 1.1.1"},
{:ex_machina, "~> 2.3", only: [:dev, :test]},
{:excoveralls, "~> 0.10", only: :test},
{:exvcr, "~> 0.11", only: :test},
{:finch, "~> 0.14.0", override: true},
{:finch, "~> 0.16.0"},
{:floki, "~> 0.34.3", only: [:dev, :test]},
{:fun_with_flags, "~> 1.9.0"},
{:fun_with_flags_ui, "~> 0.8"},

View File

@ -8,7 +8,7 @@
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"cachex": {:hex, :cachex, "3.4.0", "868b2959ea4aeb328c6b60ff66c8d5123c083466ad3c33d3d8b5f142e13101fb", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "370123b1ab4fba4d2965fb18f87fd758325709787c8c5fce35b3fe80645ccbe5"},
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"ch": {:hex, :ch, "0.1.14", "c53489b66eeb83dca63e63155c3e0de74f99ba30a15e90d0cd6b38db86be5891", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "306357bd9a92662713b6b9b4244eb94e1ef93ececa3906dbbef7392c4001c8ef"},
"chatterbox": {:hex, :ts_chatterbox, "0.13.0", "6f059d97bcaa758b8ea6fffe2b3b81362bd06b639d3ea2bb088335511d691ebf", [:rebar3], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "b93d19104d86af0b3f2566c4cba2a57d2e06d103728246ba1ac6c3c0ff010aa7"},
@ -47,9 +47,8 @@
"excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"},
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"},
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
"exvcr": {:hex, :exvcr, "0.13.3", "fcd5f54ea0ebd41db7fe16701f3c67871d1b51c3c104ab88f11135a173d47134", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "db61057447388b7adc4443a55047d11d09acc75eeb5548507c775a8402e02689"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
"floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
"fun_with_flags": {:hex, :fun_with_flags, "1.9.0", "0be8692727623af0c00e353f7bdcc9934dd6e1f87798ca6bfec9e739d42b63e6", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}], "hexpm", "8145dc009d549389f1b1915a715fb7e357fcd8fd7b91c74f04aae74912eef27a"},
"fun_with_flags_ui": {:hex, :fun_with_flags_ui, "0.8.1", "43fb6ff46eb47bdd2eeb3eccb0f15ef25e23dcbf136cc1ec3132abab53c20c3d", [:mix], [{:cowboy, ">= 2.0.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:fun_with_flags, "~> 1.8", [hex: :fun_with_flags, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 2.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "709db0e00ec803a54d7592f04a2bea0676e2394bd30bdd43a62c33af3bdc228e"},
@ -58,7 +57,7 @@
"gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
"gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"},
"grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~>1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~>0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~>0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~>0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"},
"hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~> 2.12.0", [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.4.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", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [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.4.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", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"},
"heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"},
"hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"},
@ -87,9 +86,9 @@
"mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"},
"nanoid": {:hex, :nanoid, "2.0.5", "1d2948d8967ef2d948a58c3fef02385040bd9823fc6394bd604b8d98e5516b22", [:mix], [], "hexpm", "956e8876321104da72aa48770539ff26b36b744cd26753ec8e7a8a37e53d5f58"},
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"oauther": {:hex, :oauther, "1.3.0", "82b399607f0ca9d01c640438b34d74ebd9e4acd716508f868e864537ecdb1f76", [:mix], [], "hexpm", "78eb888ea875c72ca27b0864a6f550bc6ee84f2eeca37b093d3d833fbcaec04e"},
"oban": {:hex, :oban, "2.12.1", "f604d7e6a8be9fda4a9b0f6cebbd633deba569f85dbff70c4d25d99a6f023177", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b1844c2b74e0d788b73e5144b0c9d5674cb775eae29d88a36f3c3b48d42d058"},
"observer_cli": {:hex, :observer_cli, "1.7.3", "25d094d485f47239f218b53df0691a102fef13071dfd0d04922b5142297cfc93", [:mix, :rebar3], [{:recon, "~>2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "a41b6d3e11a3444e063e09cc225f7f3e631ce14019e5fbcaebfda89b1bd788ea"},
@ -122,7 +121,7 @@
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"},
"prom_ex": {:hex, :prom_ex, "1.8.0", "662615e1d2f2ab3e0dc13a51c92ad0ccfcab24336a90cb9b114ee1bce9ef88aa", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "3eea763dfa941e25de50decbf17a6a94dbd2270e7b32f88279aa6e9bbb8e23e7"},
"public_suffix": {:git, "https://github.com/axelson/publicsuffix-elixir", "89372422ab8b433de508519ef474e39699fd11ca", []},
"public_suffix": {:git, "https://github.com/axelson/publicsuffix-elixir", "fa40c243d4b5d8598b90cff268bc4e33f3bb63f1", []},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"recon": {:hex, :recon, "2.5.2", "cba53fa8db83ad968c9a652e09c3ed7ddcc4da434f27c3eaa9ca47ffb2b1ff03", [:mix, :rebar3], [], "hexpm", "2c7523c8dee91dff41f6b3d63cba2bd49eb6d2fe5bf1eec0df7f87eb5e230e1c"},
"ref_inspector": {:hex, :ref_inspector, "1.3.1", "bb0489a4c4299dcd633f2b7a60c41a01f5590789d0b28225a60be484e1fbe777", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "3172eb1b08e5c69966f796e3fe0e691257546fa143a5eb0ecc18a6e39b233854"},
@ -131,14 +130,14 @@
"sentry": {:hex, :sentry, "8.1.0", "8d235b62fce5f8e067ea1644e30939405b71a5e1599d9529ff82899d11d03f2b", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f9fc7641ef61e885510f5e5963c2948b9de1de597c63f781e9d3d6c9c8681ab4"},
"siphash": {:hex, :siphash, "3.2.0", "ec03fd4066259218c85e2a4b8eec4bb9663bc02b127ea8a0836db376ba73f2ed", [:make, :mix], [], "hexpm", "ba3810701c6e95637a745e186e8a4899087c3b079ba88fb8f33df054c3b0b7c3"},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"telemetry_registry": {:hex, :telemetry_registry, "0.3.0", "6768f151ea53fc0fbca70dbff5b20a8d663ee4e0c0b2ae589590e08658e76f1e", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "492e2adbc609f3e79ece7f29fec363a97a2c484ac78a83098535d6564781e917"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.15.0", "1c0377617a1111000bca3f4cd530b62690c9bd2dc9b868b4459203cd4d7f16ab", [:rebar3], [{:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "87fd2e865078fdf8913a8c27bd8fe2be986383e31011f21d7f92cc5f7bc90731"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.20.0", "1ac0c53f95e201feb8d398ef9d764ae74175231289d89f166ba88a7f50cd8e73", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "ab57b74b1a63dc5775650699a3ec032ec0065005eff1f020818742b7312a8426"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"ua_inspector": {:hex, :ua_inspector, "3.4.0", "9410b51f9aeda5074da3f4f32553f3bc20b6463869a3822db1ee08aa6d0afbb9", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4fb3d9283621935a6f5158c12e30ce7ac18e004f6f11e05f5e3ae9ef8beb7022"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},

View File

@ -179,7 +179,11 @@ defmodule Plausible.BillingTest do
test "is true for a deleted subscription if no next_bill_date specified" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
insert(:subscription, user: user, status: Subscription.Status.deleted(), next_bill_date: nil)
insert(:subscription,
user: user,
status: Subscription.Status.deleted(),
next_bill_date: nil
)
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_active_subscription}
end

View File

@ -11,6 +11,7 @@ defmodule Plausible.DebugReplayInfoTest do
end
end
@tag :slow
test "adds replayable sentry context" do
site = insert(:site)
query = Plausible.Stats.Query.from(site, %{"period" => "day"})

View File

@ -1,6 +1,7 @@
defmodule Plausible.Google.ApiTest do
use Plausible.DataCase, async: false
use ExVCR.Mock, adapter: ExVCR.Adapter.Finch
use Plausible.DataCase, async: true
use Plausible.Test.Support.HTTPMocker
alias Plausible.Google.Api
import ExUnit.CaptureLog
@ -25,6 +26,20 @@ defmodule Plausible.Google.ApiTest do
|> Enum.map(&File.read!/1)
|> Enum.map(&Jason.decode!/1)
@tag :slow
test "imports page views from Google Analytics", %{site: site} do
mock_http_with("google_analytics_import#1.json")
view_id = "54297898"
date_range = Date.range(~D[2011-01-01], ~D[2022-07-19])
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
auth = {"***", "refresh_token", future}
assert :ok == Plausible.Google.Api.import_analytics(site, date_range, view_id, auth)
assert 1_495_150 == Plausible.Stats.Clickhouse.imported_pageview_count(site)
end
@tag :slow
test "import_analytics/4 refreshes OAuth token when needed", %{site: site} do
past = DateTime.add(DateTime.utc_now(), -3600, :second)
@ -64,6 +79,7 @@ defmodule Plausible.Google.ApiTest do
{:ok, buffer: pid}
end
@tag :slow
test "will fetch and persist import data from Google Analytics", %{site: site, buffer: buffer} do
request = %Plausible.Google.ReportRequest{
dataset: "imported_exit_pages",
@ -265,11 +281,9 @@ defmodule Plausible.Google.ApiTest do
end
describe "fetch_stats/3 with VCR cassetes" do
# We need real HTTP Client for VCR tests
setup_patch_env(:http_impl, Plausible.HTTPClient)
test "returns name and visitor count", %{user: user, site: site} do
use_cassette "google_analytics_stats", match_requests_on: [:request_body] do
mock_http_with("google_analytics_stats.json")
insert(:google_auth,
user: user,
site: site,
@ -285,10 +299,10 @@ defmodule Plausible.Google.ApiTest do
%{name: ["keyword3", "keyword4"], visitors: 15}
]} = Plausible.Google.Api.fetch_stats(site, query, 5)
end
end
test "returns next page when page argument is set", %{user: user, site: site} do
use_cassette "google_analytics_stats#with_page", match_requests_on: [:request_body] do
mock_http_with("google_analytics_stats#with_page.json")
insert(:google_auth,
user: user,
site: site,
@ -307,10 +321,10 @@ defmodule Plausible.Google.ApiTest do
%{name: ["keyword3", "keyword4"], visitors: 15}
]} = Plausible.Google.Api.fetch_stats(site, query, 5)
end
end
test "defaults first page when page argument is not set", %{user: user, site: site} do
use_cassette "google_analytics_stats#without_page", match_requests_on: [:request_body] do
mock_http_with("google_analytics_stats#without_page.json")
insert(:google_auth,
user: user,
site: site,
@ -326,10 +340,10 @@ defmodule Plausible.Google.ApiTest do
%{name: ["keyword3", "keyword4"], visitors: 15}
]} = Plausible.Google.Api.fetch_stats(site, query, 5)
end
end
test "returns error when token refresh fails", %{user: user, site: site} do
use_cassette "google_analytics_auth#invalid_grant" do
mock_http_with("google_analytics_auth#invalid_grant.json")
insert(:google_auth,
user: user,
site: site,
@ -344,7 +358,6 @@ defmodule Plausible.Google.ApiTest do
assert {:error, "invalid_grant"} = Plausible.Google.Api.fetch_stats(site, query, 5)
end
end
end
test "list_views/1 returns view IDs grouped by hostname" do
expect(

View File

@ -25,6 +25,7 @@ defmodule Plausible.Google.BufferTest do
|> Enum.map(&Map.drop(&1, [:table]))
end
@tag :slow
test "insert_many/3 flushes when buffer reaches limit", %{site: site} do
{:ok, pid} = Buffer.start_link()

View File

@ -1,23 +0,0 @@
defmodule Plausible.Google.Api.VCRTest do
use Plausible.DataCase, async: false
use ExVCR.Mock, adapter: ExVCR.Adapter.Finch
require Ecto.Query
setup [:create_user, :create_site]
# We need real HTTP Client for VCR tests
setup_patch_env(:http_impl, Plausible.HTTPClient)
@tag :slow
test "imports page views from Google Analytics", %{site: site} do
use_cassette "google_analytics_import#1", match_requests_on: [:request_body] do
view_id = "54297898"
date_range = Date.range(~D[2011-01-01], ~D[2022-07-19])
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
auth = {"***", "refresh_token", future}
assert :ok == Plausible.Google.Api.import_analytics(site, date_range, view_id, auth)
assert 1_495_150 == Plausible.Stats.Clickhouse.imported_pageview_count(site)
end
end
end

View File

@ -102,6 +102,7 @@ defmodule Plausible.HTTPClientTest do
HTTPClient.post(bypass_url(bypass, path: "/any"), headers_no_content_type, params)
end
@tag :slow
test "post/4 accepts finch request opts", %{bypass: bypass} do
Bypass.expect_once(bypass, "POST", "/timeout", fn conn ->
Process.sleep(500)

View File

@ -132,4 +132,34 @@ defmodule Plausible.Ingestion.EventTest do
assert {:ok, %{buffered: [event], dropped: []}} = Event.build_and_buffer(request)
assert event.clickhouse_event.revenue_source_amount == nil
end
test "IPv4 hostname is stored without public suffix processing" do
_site = insert(:site, domain: "192.168.0.1")
payload = %{
name: "checkout",
url: "http://192.168.0.1"
}
conn = build_conn(:post, "/api/events", payload)
assert {:ok, request} = Request.build(conn)
assert {:ok, %{buffered: [event]}} = Event.build_and_buffer(request)
assert event.clickhouse_event_attrs.hostname == "192.168.0.1"
end
test "Hostname is stored with public suffix processing" do
_site = insert(:site, domain: "foo.netlify.app")
payload = %{
name: "checkout",
url: "http://foo.netlify.app"
}
conn = build_conn(:post, "/api/events", payload)
assert {:ok, request} = Request.build(conn)
assert {:ok, %{buffered: [event]}} = Event.build_and_buffer(request)
assert event.clickhouse_event_attrs.hostname == "foo.netlify.app"
end
end

View File

@ -43,6 +43,7 @@ defmodule Plausible.Ingestion.RequestTest do
assert request.props == %{}
end
@tag :slow
test "requests include moving timestamp" do
payload = %{
name: "pageview",

View File

@ -383,6 +383,7 @@ defmodule Plausible.Site.CacheTest do
@items1 for i <- 1..200_000, do: {i, nil, :batch1}
@items2 for _ <- 1..200_000, do: {Enum.random(1..400_000), nil, :batch2}
@max_seconds 2
@tag :slow
test "merging large sets is expected to be under #{@max_seconds} seconds", %{test: test} do
{:ok, _} = start_test_cache(test)

View File

@ -42,6 +42,7 @@ defmodule Plausible.Site.GateKeeperTest do
assert {:deny, :throttle} = GateKeeper.check(domain, opts)
end
@tag :slow
test "rate limiting works with scale window", %{test: test, opts: opts} do
domain = "site1.example.com"

View File

@ -91,6 +91,7 @@ defmodule PlausibleWeb.Api.ExternalControllerTest do
assert NaiveDateTime.compare(e1.timestamp, e2.timestamp) == :eq
end
@tag :slow
test "timestamps differ when two events sent in a row", %{conn: conn, site: site} do
params = %{
domain: site.domain,

View File

@ -131,7 +131,11 @@ defmodule PlausibleWeb.Api.StatsController.SuggestionsTest do
test "returns suggestions for screen sizes", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview, timestamp: ~N[2019-01-01 23:00:00], pathname: "/", screen_size: "Desktop")
build(:pageview,
timestamp: ~N[2019-01-01 23:00:00],
pathname: "/",
screen_size: "Desktop"
)
])
conn =

View File

@ -232,7 +232,7 @@ defmodule PlausibleWeb.AuthControllerTest do
# but at the same time it's good to have a confirmation
# that it indeed generates a new code
if verification.code == new_verification.code do
Logger.warn(
Logger.warning(
"Congratulations! You you have hit 1 in 8999 chance of the same " <>
"email verification code repeating twice in a row!"
)
@ -491,7 +491,10 @@ defmodule PlausibleWeb.AuthControllerTest do
conn: conn,
user: user
} do
insert(:subscription, paddle_plan_id: @configured_enterprise_plan_paddle_plan_id, user: user)
insert(:subscription,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
user: user
)
insert(:enterprise_plan,
paddle_plan_id: "1234",

View File

@ -506,6 +506,12 @@ defmodule PlausibleWeb.SiteControllerTest do
describe "GET /:website/settings/integrations for self-hosting" do
setup [:create_user, :log_in, :create_site]
setup_patch_env(:google,
client_id: nil,
client_secret: nil,
api_url: "https://www.googleapis.com"
)
test "display search console settings", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200)

View File

@ -134,24 +134,24 @@ defmodule PlausibleWeb.StatsControllerTest do
zip = Enum.map(zip, fn {filename, _} -> filename end)
assert 'visitors.csv' in zip
assert 'browsers.csv' in zip
assert 'cities.csv' in zip
assert 'conversions.csv' in zip
assert 'countries.csv' in zip
assert 'custom_props.csv' in zip
assert 'devices.csv' in zip
assert 'entry_pages.csv' in zip
assert 'exit_pages.csv' in zip
assert 'operating_systems.csv' in zip
assert 'pages.csv' in zip
assert 'regions.csv' in zip
assert 'sources.csv' in zip
assert 'utm_campaigns.csv' in zip
assert 'utm_contents.csv' in zip
assert 'utm_mediums.csv' in zip
assert 'utm_sources.csv' in zip
assert 'utm_terms.csv' in zip
assert ~c"visitors.csv" in zip
assert ~c"browsers.csv" in zip
assert ~c"cities.csv" in zip
assert ~c"conversions.csv" in zip
assert ~c"countries.csv" in zip
assert ~c"custom_props.csv" in zip
assert ~c"devices.csv" in zip
assert ~c"entry_pages.csv" in zip
assert ~c"exit_pages.csv" in zip
assert ~c"operating_systems.csv" in zip
assert ~c"pages.csv" in zip
assert ~c"regions.csv" in zip
assert ~c"sources.csv" in zip
assert ~c"utm_campaigns.csv" in zip
assert ~c"utm_contents.csv" in zip
assert ~c"utm_mediums.csv" in zip
assert ~c"utm_sources.csv" in zip
assert ~c"utm_terms.csv" in zip
end
test "does not export custom properties when site owner is on a growth plan", %{
@ -165,7 +165,7 @@ defmodule PlausibleWeb.StatsControllerTest do
{:ok, zip} = :zip.unzip(response, [:memory])
files = Map.new(zip)
refute Map.has_key?(files, 'custom_props.csv')
refute Map.has_key?(files, ~c"custom_props.csv")
end
test "exports data in zipped csvs", %{conn: conn, site: site} do
@ -201,7 +201,7 @@ defmodule PlausibleWeb.StatsControllerTest do
{:ok, zip} = :zip.unzip(response, [:memory])
{_filename, result} =
Enum.find(zip, fn {filename, _data} -> filename == 'custom_props.csv' end)
Enum.find(zip, fn {filename, _data} -> filename == ~c"custom_props.csv" end)
assert parse_csv(result) == [
["property", "value", "visitors", "events", "percentage"],
@ -222,7 +222,7 @@ defmodule PlausibleWeb.StatsControllerTest do
{:ok, zip} = :zip.unzip(response, [:memory])
{_filename, visitors} =
Enum.find(zip, fn {filename, _data} -> filename == 'visitors.csv' end)
Enum.find(zip, fn {filename, _data} -> filename == ~c"visitors.csv" end)
assert parse_csv(visitors) == [
[
@ -304,7 +304,7 @@ defmodule PlausibleWeb.StatsControllerTest do
{:ok, zip} = :zip.unzip(response(conn, 200), [:memory])
{_filename, result} =
Enum.find(zip, fn {filename, _data} -> filename == 'custom_props.csv' end)
Enum.find(zip, fn {filename, _data} -> filename == ~c"custom_props.csv" end)
assert parse_csv(result) == [
["property", "value", "visitors", "events", "percentage"],
@ -423,7 +423,7 @@ defmodule PlausibleWeb.StatsControllerTest do
{:ok, zip} = :zip.unzip(response(conn, 200), [:memory])
{_filename, result} =
Enum.find(zip, fn {filename, _data} -> filename == 'custom_props.csv' end)
Enum.find(zip, fn {filename, _data} -> filename == ~c"custom_props.csv" end)
assert parse_csv(result) == [
["property", "value", "visitors", "events", "conversion_rate"],
@ -545,7 +545,10 @@ defmodule PlausibleWeb.StatsControllerTest do
insert(:shared_link, site: site, password_hash: Plausible.Auth.Password.hash("password"))
link2 =
insert(:shared_link, site: site2, password_hash: Plausible.Auth.Password.hash("password1"))
insert(:shared_link,
site: site2,
password_hash: Plausible.Auth.Password.hash("password1")
)
conn = post(conn, "/share/#{link.slug}/authenticate", %{password: "password"})
assert redirected_to(conn, 302) == "/share/#{site.domain}?auth=#{link.slug}"

View File

@ -319,6 +319,7 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
refute element_exists?(doc, "#dropdown-test-component-option-1")
end
@tag :slow
test "pre-fills the suggestions asynchronously", %{conn: conn} do
{:ok, lv, doc} = live_isolated(conn, SampleViewAsync, session: %{})
refute element_exists?(doc, "#dropdown-test-component-option-1")
@ -329,6 +330,7 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
assert text_of_element(doc, "#dropdown-test-component-option-3") == "Three"
end
@tag :slow
test "uses the suggestions function asynchronously", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, SampleViewAsync, session: %{})
doc = type_into_combo(lv, "test-component", "Echo me")

View File

@ -155,6 +155,7 @@ defmodule PlausibleWeb.Live.Components.FormTest do
refute text(p_hint) =~ "Test suggestion 2."
end
@tag :slow
test "favors hint warning over suggestion when both present" do
doc =
render_component(&Form.strength_meter/1,

View File

@ -255,6 +255,7 @@ defmodule PlausibleWeb.Live.FunnelSettingsTest do
refute element_exists?(doc, save_inactive)
end
@tag :slow
test "funnel gets evaluated on every select, assuming a second has passed between selections",
%{
conn: conn,

View File

@ -63,25 +63,29 @@ defmodule PlausibleWeb.Live.GoalSettings.FormTest do
test "creates a custom event", %{conn: conn, site: site} do
{parent, lv} = get_liveview(conn, site, with_parent?: true)
refute render(parent) =~ "Foo"
lv |> element("form") |> render_submit(%{goal: %{event_name: "Foo"}})
refute render(parent) =~ "SampleCustomEvent"
lv |> element("form") |> render_submit(%{goal: %{event_name: "SampleCustomEvent"}})
parent_html = render(parent)
assert parent_html =~ "Foo"
assert parent_html =~ "SampleCustomEvent"
assert parent_html =~ "Custom Event"
end
test "creates a revenue goal", %{conn: conn, site: site} do
{parent, lv} = get_liveview(conn, site, with_parent?: true)
refute render(parent) =~ "Foo"
lv |> element("form") |> render_submit(%{goal: %{event_name: "Foo", currency: "EUR"}})
refute render(parent) =~ "SampleRevenueGoal"
lv
|> element("form")
|> render_submit(%{goal: %{event_name: "SampleRevenueGoal", currency: "EUR"}})
parent_html = render(parent)
assert parent_html =~ "Foo (EUR)"
assert parent_html =~ "SampleRevenueGoal (EUR)"
assert parent_html =~ "Revenue Goal"
end
test "creates a pageview goal", %{conn: conn, site: site} do
{parent, lv} = get_liveview(conn, site, with_parent?: true)
refute render(parent) =~ "Foo"
refute render(parent) =~ "Visit /page/**"
lv |> element("form") |> render_submit(%{goal: %{page_path: "/page/**"}})
parent_html = render(parent)
assert parent_html =~ "Visit /page/**"

View File

@ -60,14 +60,16 @@ defmodule PlausibleWeb.Plugins.API.ErrorsTest do
test "formats changeset errors" do
changeset = Example.changeset(%Example{}, %{email: "foo", age: 101})
assert Plug.Test.conn(:get, "/")
errors =
Plug.Test.conn(:get, "/")
|> Errors.error(:bad_request, changeset)
|> json_response(400)
|> Map.fetch!("errors") == [
%{"detail" => "age: is invalid"},
%{"detail" => "email: has invalid format"},
%{"detail" => "name: can't be blank"}
]
|> Map.fetch!("errors")
assert Enum.count(errors) == 3
assert %{"detail" => "age: is invalid"} in errors
assert %{"detail" => "email: has invalid format"} in errors
assert %{"detail" => "name: can't be blank"} in errors
end
end
end

View File

@ -6,7 +6,10 @@ defmodule PlausibleWeb.ErrorViewTest do
layout = Application.get_env(:plausible, PlausibleWeb.Endpoint)[:render_errors][:layout]
error_html =
Phoenix.View.render_to_string(PlausibleWeb.ErrorView, "500.html", conn: conn, layout: layout)
Phoenix.View.render_to_string(PlausibleWeb.ErrorView, "500.html",
conn: conn,
layout: layout
)
refute error_html =~ "data-domain="
end

View File

@ -0,0 +1,54 @@
defmodule Plausible.Test.Support.HTTPMocker do
@moduledoc """
Currently only supports post request, it's a drop-in replacement
for our exvcr usage that wasn't ever needed (e.g. we had no way to
re-record the cassettes anyway).
"""
defmacro __using__(_) do
quote do
import Mox
def mock_http_with(http_mock_fixture) do
mocks =
"fixture/http_mocks/#{http_mock_fixture}"
|> File.read!()
|> Jason.decode!()
|> Enum.into(%{}, &{{&1["url"], &1["request_body"]}, &1})
stub(
Plausible.HTTPClient.Mock,
:post,
fn url, _, params, _ -> http_mocker_stub(mocks, url, params) end
)
stub(
Plausible.HTTPClient.Mock,
:post,
fn url, _, params -> http_mocker_stub(mocks, url, params) end
)
end
defp http_mocker_stub(mocks, url, params) do
params =
case Jason.encode(params) do
{:ok, p} -> Jason.decode!(p)
{:error, _} -> params
end
mock = Map.fetch!(mocks, {url, params})
response = %Finch.Response{
status: mock["status"],
headers: [{"content-type", "application/json"}],
body: mock["response_body"]
}
if mock["status"] >= 200 and mock["status"] < 300 do
{:ok, response}
else
{:error, Plausible.HTTPClient.Non200Error.new(response)}
end
end
end
end
end