diff --git a/lib/mix/tasks/sentry.package_source_code.ex b/lib/mix/tasks/sentry.package_source_code.ex index 0f80a311..6423b27e 100644 --- a/lib/mix/tasks/sentry.package_source_code.ex +++ b/lib/mix/tasks/sentry.package_source_code.ex @@ -43,9 +43,34 @@ defmodule Mix.Tasks.Sentry.PackageSourceCode do > source-context-related options in compile-time config files (like `config/config.exs` > or `config/prod.exs`). + > #### Building with Nix {: .tip} + > + > If you build your application using nixpkgs' `mixRelease`, + > `mix sentry.package_source_code` will fail, because Sentry's source is read-only. + > + > To fix this, you can use the `--output` option to write the map file to a writable location, + > then copy it to Sentry's `priv` in the final OTP release. + > + > Example: + > + > ```nix + > mixRelease { + > preBuild = '' + > mix sentry.package_source_code --output sentry.map + > ''; + > + > postInstall = '' + > sentryPrivDir="$(find $out/lib -maxdepth 1 -name "sentry-*")/priv" + > mkdir "$sentryPrivDir" + > cp -a sentry.map "$sentryPrivDir" + > ''; + > } + > ``` + ## Options * `--debug` - print more information about collecting and encoding source code + * `--output path` - write file to the given path. Example: `path/to/sentry.map` """ @@ -59,7 +84,10 @@ defmodule Mix.Tasks.Sentry.PackageSourceCode do @bytes_in_mb 1024 * 1024 @bytes_in_gb 1024 * 1024 * 1024 - @switches [debug: :boolean] + @switches [ + debug: :boolean, + output: :string + ] @impl true def run(args) do @@ -83,7 +111,7 @@ defmodule Mix.Tasks.Sentry.PackageSourceCode do {elapsed, contents} = :timer.tc(fn -> Sources.encode_source_code_map(source_map) end) log_debug(opts, "Encoded source code map in #{format_time(elapsed)}") - output_path = Sources.path_of_packaged_source_code() + output_path = Keyword.get_lazy(opts, :output, &Sources.path_of_packaged_source_code/0) File.mkdir_p!(Path.dirname(output_path)) File.write!(output_path, contents) diff --git a/lib/sentry/config.ex b/lib/sentry/config.ex index fc57db96..29226101 100644 --- a/lib/sentry/config.ex +++ b/lib/sentry/config.ex @@ -61,6 +61,15 @@ defmodule Sentry.Config do The name of the server running the application. Not used by default. """ ], + source_code_map_path: [ + type: {:custom, __MODULE__, :__validate_path__, []}, + default: nil, + type_doc: "`t:Path.t/0` or `nil`", + doc: """ + The path to the map file when mix task `sentry.package_source_code` was invoked with + `--output path/to/file.map` + """ + ], sample_rate: [ type: {:custom, __MODULE__, :__validate_sample_rate__, []}, default: 1.0, @@ -399,6 +408,9 @@ defmodule Sentry.Config do @spec server_name() :: String.t() | nil def server_name, do: get(:server_name) + @spec source_code_map_path() :: Path.t() | nil + def source_code_map_path, do: get(:source_code_map_path) + @spec filter() :: module() def filter, do: fetch!(:filter) @@ -528,6 +540,16 @@ defmodule Sentry.Config do :persistent_term.get({:sentry_config, key}, nil) end + def __validate_path__(nil), do: {:ok, nil} + + def __validate_path__(path) when is_binary(path) do + if File.exists?(path) do + {:ok, path} + else + {:error, "path does not exist"} + end + end + def __validate_sample_rate__(float) do if is_float(float) and float >= 0.0 and float <= 1.0 do {:ok, float} diff --git a/lib/sentry/sources.ex b/lib/sentry/sources.ex index 0ef63b0a..aa3b9979 100644 --- a/lib/sentry/sources.ex +++ b/lib/sentry/sources.ex @@ -14,7 +14,9 @@ defmodule Sentry.Sources do # Default argument is here for testing. @spec load_source_code_map_if_present(Path.t()) :: {:loaded, source_map()} | {:error, term()} - def load_source_code_map_if_present(path \\ path_of_packaged_source_code()) do + def load_source_code_map_if_present( + path \\ Config.source_code_map_path() || path_of_packaged_source_code() + ) do path = Path.relative_to_cwd(path) with {:ok, contents} <- File.read(path), diff --git a/test/mix/sentry.package_source_code_test.exs b/test/mix/sentry.package_source_code_test.exs index 6c24dda1..5755dfcf 100644 --- a/test/mix/sentry.package_source_code_test.exs +++ b/test/mix/sentry.package_source_code_test.exs @@ -20,15 +20,24 @@ defmodule Mix.Tasks.Sentry.PackageSourceCodeTest do assert :ok = Mix.Task.rerun("sentry.package_source_code") - assert_receive {:mix_shell, :info, ["Wrote " <> _ = message]} - assert message =~ expected_path + validate_map_file!(expected_path) + end - assert {:ok, contents} = File.read(expected_path) + @tag :tmp_dir + test "packages source code into custom path", %{tmp_dir: tmp_dir} do + put_test_config( + root_source_code_paths: [File.cwd!()], + enable_source_code_context: true + ) - assert %{"version" => 1, "files_map" => source_map} = - :erlang.binary_to_term(contents, [:safe]) + expected_path = + [tmp_dir, "sentry.map"] + |> Path.join() + |> Path.relative_to_cwd() - assert Map.has_key?(source_map, "lib/mix/tasks/sentry.package_source_code.ex") + assert :ok = Mix.Task.rerun("sentry.package_source_code", ["--output", expected_path]) + + validate_map_file!(expected_path) end test "supports the --debug option" do @@ -51,4 +60,16 @@ defmodule Mix.Tasks.Sentry.PackageSourceCodeTest do {:mix_shell, :info, ["Wrote " <> _]} ]} = Process.info(self(), :messages) end + + defp validate_map_file!(path) do + assert_receive {:mix_shell, :info, ["Wrote " <> _ = message]} + assert message =~ path + + assert {:ok, contents} = File.read(path) + + assert %{"version" => 1, "files_map" => source_map} = + :erlang.binary_to_term(contents, [:safe]) + + assert Map.has_key?(source_map, "lib/mix/tasks/sentry.package_source_code.ex") + end end diff --git a/test/sentry/config_test.exs b/test/sentry/config_test.exs index a6152ae7..c0355551 100644 --- a/test/sentry/config_test.exs +++ b/test/sentry/config_test.exs @@ -50,6 +50,22 @@ defmodule Sentry.ConfigTest do end end + @tag :tmp_dir + test ":source_code_map_path from option", %{tmp_dir: tmp_dir} do + source_code_map_path = Path.join([tmp_dir, "test.map"]) + + assert Config.validate!(source_code_map_path: nil)[:source_code_map_path] == nil + + assert_raise ArgumentError, ~r/path does not exist/, fn -> + assert Config.validate!(source_code_map_path: source_code_map_path) + end + + File.touch!(source_code_map_path) + + assert Config.validate!(source_code_map_path: source_code_map_path)[:source_code_map_path] == + source_code_map_path + end + test ":release from option" do assert Config.validate!(release: "1.0.0")[:release] == "1.0.0" end