lib/mix/tasks/sentry.package_source_code.ex

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
  ```

  ## Options

    * `--debug` - print more information about collecting and encoding source code

  """

  @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]
  @requirements ["app.config"]

  @impl true
  def run(args) do
    {opts, _args} = OptionParser.parse!(args, strict: @switches)

    {elapsed, source_map} =
      :timer.tc(fn ->
        case Sources.load_files() 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 = Sources.path_of_packaged_source_code()
    File.mkdir_p!(Path.dirname(output_path))
    File.write!(output_path, contents)

    Mix.shell().info([
      "Wrote ",
      :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