defmodule Mix.Tasks.Sentry.PackageSourceCode do @shortdoc "Packages source code for Sentry to use when reporting errors" @moduledoc """ Packages source code for Sentry to use when reporting errors. This task should be used in production settings, before building a release of your application. It packages all the source code of your application in a single file (called `sentry.map`), which is optimized for fast retrieval of source code lines. Sentry then uses this to report source code context. See the documentation for the `Sentry` module for configuration options related to the source code context. *This task is available since v10.0.0 of this library*. ## Usage ```shell mix sentry.package_source_code ``` ### Using in Production In production settings, call this task before building a release. This way, the source code packaged by this task will be included in the release. For example, in a release script (this could also be in a `Dockerfile`, if you're using Docker): ```shell # ... mix sentry.package_source_code mix release ``` > #### Runtime Configuration {: .tip} > > If you use `config/runtime.exs` for runtime configuration of your application and > release, *and* you also configure the source-context-related options for the `:sentry` > application in that file (such as `:root_source_code_paths`), you'll need to call the > `mix app.config` task before calling this Mix task. `mix app.config` loads the > `config/runtime.exs` configuration. Generally, we recommend configuring all the > source-context-related options in compile-time config files (like `config/config.exs` > or `config/prod.exs`). ## Options * `--debug` - print more information about collecting and encoding source code * `--output PATH` - write source map file to the given `PATH`. If you don't specify this option, the file will be written to the default path, which is inside the `priv` directory of the `:sentry` application. If you use this option, remember to also **configure the `:source_code_map_path` option**, otherwise Sentry will try to *read* the file from the default location. See `Sentry` for more information. This option can be useful when the source `priv` directory is read-only, such as with [NixOS](https://github.com/NixOS/nixpkgs) and similar tools. *Available since v10.2.0*. * `--no-compile` - does not compile applications before running * `--no-deps-check` - does not check dependencies before running """ @moduledoc since: "10.0.0" use Mix.Task alias Sentry.Sources @bytes_in_kb 1024 @bytes_in_mb 1024 * 1024 @bytes_in_gb 1024 * 1024 * 1024 @switches [ debug: :boolean, output: :string, no_compile: :boolean, no_deps_check: :boolean ] @impl true def run(args) do {opts, _args} = OptionParser.parse!(args, strict: @switches) # Don't call the app.config task here, see # https://github.com/getsentry/sentry-elixir/commit/b2a04ddcf5d5c5f861da9888b3dec2f4f12cee01 if not Keyword.get(opts, :no_deps_check, false) do Mix.Task.run("loadpaths") end if not Keyword.get(opts, :no_compile, false) do Mix.Task.run("compile") end config = Application.get_all_env(:sentry) {elapsed, source_map} = :timer.tc(fn -> case Sources.load_files(config) do {:ok, source_map} -> source_map {:error, message} -> Mix.raise(message) end end) log_debug( opts, "Loaded source code map with #{map_size(source_map)} files in #{format_time(elapsed)}" ) {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 = Keyword.get_lazy(opts, :output, &Sources.path_of_packaged_source_code/0) File.mkdir_p!(Path.dirname(output_path)) File.write!(output_path, contents) Mix.shell().info([ "Wrote #{map_size(source_map)} files in ", [:cyan, format_bytes(byte_size(contents)), :reset], " to: #{Path.relative_to_cwd(output_path)}" ]) end ## Helpers defp log_debug(opts, str) do if opts[:debug] do Mix.shell().info([:magenta, str, :reset]) end end defp format_bytes(n) when n < @bytes_in_kb, do: "#{n} bytes" defp format_bytes(n) when n < @bytes_in_mb, do: "#{Float.round(n / @bytes_in_kb, 2)} kb" defp format_bytes(n) when n < @bytes_in_gb, do: "#{Float.round(n / @bytes_in_mb, 2)} Mb" defp format_time(n) when n < 1000, do: "#{n} µs" defp format_time(n) when n < 1_000_000, do: "#{div(n, 1000)} ms" defp format_time(n), do: "#{Float.round(n / 1_000_000, 2)} s" end