From 1a4f2f7f4c9d394c4c1c91b3ebe31003fd0e5916 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 11 May 2024 08:45:55 -0400 Subject: [PATCH 1/2] Expose Sentry.get_dsn() --- lib/mix/tasks/sentry.send_test_event.ex | 9 ++- lib/sentry.ex | 14 ++++ lib/sentry/config.ex | 67 +---------------- lib/sentry/dsn.ex | 95 +++++++++++++++++++++++++ lib/sentry/transport.ex | 8 +-- test/sentry/config_test.exs | 24 +++++-- test/sentry_test.exs | 17 +++++ 7 files changed, 153 insertions(+), 81 deletions(-) create mode 100644 lib/sentry/dsn.ex diff --git a/lib/mix/tasks/sentry.send_test_event.ex b/lib/mix/tasks/sentry.send_test_event.ex index 046a5ef4..7a740733 100644 --- a/lib/mix/tasks/sentry.send_test_event.ex +++ b/lib/mix/tasks/sentry.send_test_event.ex @@ -60,11 +60,10 @@ defmodule Mix.Tasks.Sentry.SendTestEvent do defp print_environment_info do Mix.shell().info("Client configuration:") - if Config.dsn() do - {endpoint, public_key, secret_key} = Config.dsn() - Mix.shell().info("server: #{endpoint}") - Mix.shell().info("public_key: #{public_key}") - Mix.shell().info("secret_key: #{secret_key}") + if dsn = Config.dsn() do + Mix.shell().info("server: #{dsn.endpoint_uri}") + Mix.shell().info("public_key: #{dsn.public_key}") + Mix.shell().info("secret_key: #{dsn.secret_key}") end Mix.shell().info("current environment_name: #{inspect(to_string(Config.environment_name()))}") diff --git a/lib/sentry.ex b/lib/sentry.ex index c9091e23..c5a54de7 100644 --- a/lib/sentry.ex +++ b/lib/sentry.ex @@ -438,4 +438,18 @@ defmodule Sentry do @doc since: "10.0.0" @spec put_config(atom(), term()) :: :ok defdelegate put_config(key, value), to: Config + + @doc """ + Returns the currently-set Sentry DSN, *if set* (or `nil` otherwise). + + This is useful in situations like capturing user feedback. + """ + @doc since: "10.6.0" + @spec get_dsn() :: String.t() | nil + def get_dsn do + case Config.dsn() do + %Sentry.DSN{original_dsn: original_dsn} -> original_dsn + nil -> nil + end + end end diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index 8489a137..ac887a0c 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -68,7 +68,7 @@ defmodule Sentry.Config do basic_opts_schema = [ dsn: [ - type: {:or, [nil, {:custom, __MODULE__, :__validate_string_dsn__, []}]}, + type: {:or, [nil, {:custom, Sentry.DSN, :parse, []}]}, default: nil, type_doc: "`t:String.t/0` or `nil`", doc: """ @@ -671,69 +671,4 @@ defmodule Sentry.Config do {:error, "expected #{inspect(key)} to be a #{inspect(mod)} struct, got: #{inspect(term)}"} end end - - def __validate_string_dsn__(dsn) when is_binary(dsn) do - uri = URI.parse(dsn) - - if uri.query do - raise ArgumentError, """ - using a Sentry DSN with query parameters is not supported since v9.0.0 of this library. - The configured DSN was: - - #{inspect(dsn)} - - The query string in that DSN is: - - #{inspect(uri.query)} - - Please remove the query parameters from your DSN and pass them in as regular - configuration. Check out the guide to upgrade to 9.0.0 at: - - https://hexdocs.pm/sentry/upgrade-9.x.html - - See the documentation for the Sentry module for more information on configuration - in general. - """ - end - - unless is_binary(uri.path) do - throw("missing project ID at the end of the DSN URI: #{inspect(dsn)}") - end - - unless is_binary(uri.userinfo) do - throw("missing user info in the DSN URI: #{inspect(dsn)}") - end - - {public_key, secret_key} = - case String.split(uri.userinfo, ":", parts: 2) do - [public, secret] -> {public, secret} - [public] -> {public, nil} - end - - with {:ok, {base_path, project_id}} <- pop_project_id(uri.path) do - new_path = Enum.join([base_path, "api", project_id, "envelope"], "/") <> "/" - endpoint_uri = URI.merge(%URI{uri | userinfo: nil}, new_path) - - {:ok, {URI.to_string(endpoint_uri), public_key, secret_key}} - end - catch - message -> {:error, message} - end - - def __validate_string_dsn__(other) do - {:error, "expected :dsn to be a string or nil, got: #{inspect(other)}"} - end - - defp pop_project_id(uri_path) do - path = String.split(uri_path, "/") - {project_id, path} = List.pop_at(path, -1) - - case Integer.parse(project_id) do - {_project_id, ""} -> - {:ok, {Enum.join(path, "/"), project_id}} - - _other -> - {:error, "expected the DSN path to end with an integer project ID, got: #{inspect(path)}"} - end - end end diff --git a/lib/sentry/dsn.ex b/lib/sentry/dsn.ex new file mode 100644 index 00000000..93704244 --- /dev/null +++ b/lib/sentry/dsn.ex @@ -0,0 +1,95 @@ +defmodule Sentry.DSN do + @moduledoc false + + @type t() :: %__MODULE__{ + original_dsn: String.t(), + endpoint_uri: String.t(), + public_key: String.t(), + secret_key: String.t() | nil + } + + defstruct [ + :original_dsn, + :endpoint_uri, + :public_key, + :secret_key + ] + + # {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}{PATH}/{PROJECT_ID} + @spec parse(String.t()) :: {:ok, t()} | {:error, String.t()} + def parse(term) + + def parse(dsn) when is_binary(dsn) do + uri = URI.parse(dsn) + + if uri.query do + raise ArgumentError, """ + using a Sentry DSN with query parameters is not supported since v9.0.0 of this library. + The configured DSN was: + + #{inspect(dsn)} + + The query string in that DSN is: + + #{inspect(uri.query)} + + Please remove the query parameters from your DSN and pass them in as regular + configuration. Check out the guide to upgrade to 9.0.0 at: + + https://hexdocs.pm/sentry/upgrade-9.x.html + + See the documentation for the Sentry module for more information on configuration + in general. + """ + end + + unless is_binary(uri.path) do + throw("missing project ID at the end of the DSN URI: #{inspect(dsn)}") + end + + unless is_binary(uri.userinfo) do + throw("missing user info in the DSN URI: #{inspect(dsn)}") + end + + {public_key, secret_key} = + case String.split(uri.userinfo, ":", parts: 2) do + [public, secret] -> {public, secret} + [public] -> {public, nil} + end + + with {:ok, {base_path, project_id}} <- pop_project_id(uri.path) do + new_path = Enum.join([base_path, "api", project_id, "envelope"], "/") <> "/" + endpoint_uri = URI.merge(%URI{uri | userinfo: nil}, new_path) + + parsed_dsn = %__MODULE__{ + endpoint_uri: URI.to_string(endpoint_uri), + public_key: public_key, + secret_key: secret_key, + original_dsn: dsn + } + + {:ok, parsed_dsn} + end + catch + message -> {:error, message} + end + + def parse(other) do + {:error, "expected :dsn to be a string or nil, got: #{inspect(other)}"} + end + + ## Helpers + + defp pop_project_id(uri_path) do + path = String.split(uri_path, "/") + {project_id, path} = List.pop_at(path, -1) + + case Integer.parse(project_id) do + {_project_id, ""} -> + {:ok, {Enum.join(path, "/"), project_id}} + + _other -> + {:error, "expected the DSN path to end with an integer project ID, got: #{inspect(path)}"} + end + end +end diff --git a/lib/sentry/transport.ex b/lib/sentry/transport.ex index 76ff2d04..bc963a0c 100644 --- a/lib/sentry/transport.ex +++ b/lib/sentry/transport.ex @@ -88,15 +88,15 @@ defmodule Sentry.Transport do end defp get_endpoint_and_headers do - {endpoint, public_key, secret_key} = Config.dsn() + %Sentry.DSN{} = dsn = Config.dsn() auth_query = [ sentry_version: @sentry_version, sentry_client: @sentry_client, sentry_timestamp: System.system_time(:second), - sentry_key: public_key, - sentry_secret: secret_key + sentry_key: dsn.public_key, + sentry_secret: dsn.secret_key ] |> Enum.reject(fn {_, value} -> is_nil(value) end) |> Enum.map_join(", ", fn {name, value} -> "#{name}=#{value}" end) @@ -106,6 +106,6 @@ defmodule Sentry.Transport do {"X-Sentry-Auth", "Sentry " <> auth_query} ] - {endpoint, auth_headers} + {dsn.endpoint_uri, auth_headers} end end diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index 2b120b9e..4b7cc429 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -5,16 +5,24 @@ defmodule Sentry.ConfigTest do describe "validate!/0" do test ":dsn from option" do - assert Config.validate!(dsn: "https://public:secret@app.getsentry.com/1")[:dsn] == - {"https://app.getsentry.com/api/1/envelope/", "public", "secret"} + assert %Sentry.DSN{} = + dsn = Config.validate!(dsn: "https://public:secret@app.getsentry.com/1")[:dsn] + + assert dsn.endpoint_uri == "https://app.getsentry.com/api/1/envelope/" + assert dsn.public_key == "public" + assert dsn.secret_key == "secret" + assert dsn.original_dsn == "https://public:secret@app.getsentry.com/1" assert Config.validate!(dsn: nil)[:dsn] == nil end test ":dsn from system environment" do with_system_env("SENTRY_DSN", "https://public:secret@app.getsentry.com/1", fn -> - assert Config.validate!([])[:dsn] == - {"https://app.getsentry.com/api/1/envelope/", "public", "secret"} + assert %Sentry.DSN{} = dsn = Config.validate!([])[:dsn] + assert dsn.endpoint_uri == "https://app.getsentry.com/api/1/envelope/" + assert dsn.public_key == "public" + assert dsn.secret_key == "secret" + assert dsn.original_dsn == "https://public:secret@app.getsentry.com/1" end) end @@ -212,8 +220,12 @@ defmodule Sentry.ConfigTest do new_dsn = "https://public:secret@app.getsentry.com/2" assert :ok = Config.put_config(:dsn, new_dsn) - assert Config.dsn() == - {"https://app.getsentry.com/api/2/envelope/", "public", "secret"} + assert %Sentry.DSN{ + original_dsn: ^new_dsn, + endpoint_uri: "https://app.getsentry.com/api/2/envelope/", + public_key: "public", + secret_key: "secret" + } = Config.dsn() end test "validates the given key" do diff --git a/test/sentry_test.exs b/test/sentry_test.exs index 29b8513e..77825ff5 100644 --- a/test/sentry_test.exs +++ b/test/sentry_test.exs @@ -201,4 +201,21 @@ defmodule SentryTest do assert {:ok, "1923"} = Sentry.capture_check_in(status: :ok, monitor_slug: "default-slug") end end + + describe "get_dsn/0" do + test "returns nil if the :dsn option is not configured" do + put_test_config(dsn: nil) + assert Sentry.get_dsn() == nil + end + + test "returns the DSN if it's configured" do + random_string = fn -> 5 |> :crypto.strong_rand_bytes() |> Base.encode16() end + + random_dsn = + "https://#{random_string.()}:#{random_string.()}@#{random_string.()}:3000/#{System.unique_integer([:positive])}" + + put_test_config(dsn: random_dsn) + assert Sentry.get_dsn() == random_dsn + end + end end From 5587260b1d1d1117e145d97d3ad648dfed54cd45 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sat, 11 May 2024 08:47:19 -0400 Subject: [PATCH 2/2] Dialyzer --- lib/sentry/config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index ac887a0c..59a2c6e5 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -459,7 +459,7 @@ defmodule Sentry.Config do """ end - @spec dsn() :: nil | {String.t(), String.t(), String.t()} + @spec dsn() :: nil | Sentry.DSN.t() def dsn, do: get(:dsn) # TODO: remove me on v11.0.0, :included_environments has been deprecated