Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose Sentry.get_dsn/0 #731

Merged
merged 2 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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