Skip to content

Commit

Permalink
Expose DSN via new Sentry.get_dsn/0 (#731)
Browse files Browse the repository at this point in the history
  • Loading branch information
whatyouhide authored May 11, 2024
1 parent 4267098 commit dfd9a9e
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 82 deletions.
9 changes: 4 additions & 5 deletions lib/mix/tasks/sentry.send_test_event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()))}")
Expand Down
14 changes: 14 additions & 0 deletions lib/sentry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
69 changes: 2 additions & 67 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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: """
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
95 changes: 95 additions & 0 deletions lib/sentry/dsn.ex
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions lib/sentry/transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -106,6 +106,6 @@ defmodule Sentry.Transport do
{"X-Sentry-Auth", "Sentry " <> auth_query}
]

{endpoint, auth_headers}
{dsn.endpoint_uri, auth_headers}
end
end
24 changes: 18 additions & 6 deletions test/sentry/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions test/sentry_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit dfd9a9e

Please sign in to comment.