Create and Download with Phoenix

Published by: Todd Resudek on October 25, 2023

After years of using Phoenix, I finally ran into my first need to generate and then download a file from an endpoint.

I was happy to discover Phoenix.Controller includes an easy was to do this:

def download(conn, params) do
  send_download(conn, {:binary, "world"}, filename: "hello.txt")
end

In this example, Phoenix packages up the file with the contents provided and downloads it.

You can also point to a file, using the {:file, path_to_file} and Phoenix will infer the name and mime type.

My one gripe - Phoenix sends a 200 status code by default. Luckily, you can override that easily by putting your preferred status in the conn:

def download(conn, params) do
  path_to_file = Application.app_dir(:my_app, "priv/prospectus.pdf")

  conn
  |> put_status(:created)
  |> send_download(conn, {:file, path_to_file})
end

Building Breadcrumbs with LiveView and TailwindCSS

Published by: Todd Resudek on October 10, 2023

A project I am working on required adding breadcrumb navigation. If you’re not familiar with the term, it is a list of links usually at the top of the page that represents how you got to the page you are on, like this: Image of breadcrumbs

There were two challenges. First, making this into a reusable component, since it would exist on most pages.

The end result looked like this in the LiveView:

<.breadcrumb links={[
  {"Feature Flags", "/flags"},
  {"Projects", "/flags/projects"},
  {"Environments", "/flags/projects/#{@project_id}/environments"},
  {"Flags", "/flags/projects/#{@project_id}/environments/#{@environment_id}/flags"}
]} />

A tidy list of labels and links for the breadcrumbs. That uses a component defined:

  attr :links, :any, required: true, doc: "two-tuple containing a label and a link"

  def breadcrumb(assigns) do
    ~H"""
    <ul class="mb-4 text-sm">
      <li :for={{label, link} <- @links} class="inline-block pr-1"><a href={link} class="hover:underline pr-1"><%= label %></a></li> 
    </ul>
    """
  end

That gave me a nice inline list of links. But, one thing most breadcrumbs have is a visual separator. In the screenshot, it is single right angle quote.

To add that, I learned that TailwindCSS has great support for psuedo classes. In this case, what I wanted to do was inject content after each link except the final one. The resulting code looks like this:

<li :for={{label, link} <- @links} class="after:content-['›'] inline-block pr-1 last-of-type:after:content-['']">
...

Extending the existing LiveView components

Published by: Todd Resudek on September 27, 2023

While building a new Phoenix LiveView project, I noticed the generators created an index view that included a table:

<.table
  id="items"
  rows={@streams.examples}
>
  <:col :let={{_id, example}} label="Key">
    <%= env.key %>
  </:col>
  ...

This is great for a bootstrap view, but I wanted to stylize the elements in each row.

It turns out Phoenix ships with a file called lib/app_web/components/core_components.ex that defines the HTML for a bunch of different elements.

My goal of extending that component to pass in additional classes required:

  1. adding a class attribute to the slot definition
    slot :col, required: true do
      attr :label, :string
      attr :class, :string
    end
  2. finding the existing template code, which looks like this:
<div class="block py-4 pr-6">
  <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
  <span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
    <%= render_slot(col, @row_item.(row)) %>
  </span>
</div>

and adding the new slot attribute to where the class is defined:

<span class={["relative", i == 0 && "font-semibold text-zinc-900 ", col[:class]]}>

Checking for unused dependencies

Published by: Tyler Clemens on August 11, 2023

During a recent pull-request review, I noticed that we had unused dependencies in our mix.lock file due to adding and removing dependencies that were no longer being used. I wondered if there was a quick way to detect this type of issue in Github actions, and there is!

Mix has a handy task that you can run to check for unused dependencies:

mix deps.unlock --check-unused

Within a Github action, you can use the following command to fail CI if there are any unused dependencies in the lock file:

- name: Check mix.lock for unused dependencies
  run: mix deps.unlock --check-unused

Getting the release version

Published by: Todd Resudek on June 14, 2023

I recently ran into an issue with an app that is installed in multiple locations needing to warn users when a new version is available.

In Elixir, we put the release version in the Mix file, but Mix is not available in an Elixir release. So, doing something like:

Mix.Project.get().project[:version]

will not work.

The solution is to use the application spec:

:app_name 
|> Application.spec(:vsn) 
|> to_string()

Auto-rotation of logs in Elixir

Published by: Todd Resudek on June 1, 2023

By default, a Phoenix app will configure the built-in logger to an unlimited size.

Technically, the configuration defaults to the values set in Erlang’s logger_std_h module.

Fortunately, Elixir allows you to use a keyword list in the config to override these settings.

config :logger, :default_handler,
  config: [
    file: ~c"system.log",
    max_no_bytes: 10_000_000,
    max_no_files: 5
  ]

This allows you to easy rotate on file size (in this case 10M bytes.)


Excluding logging from a route in Elixir

Published by: Todd Resudek on May 25, 2023

Storing logs in an external service like Datadog is extremely helpful for searching and alerting, but also expensive.

I recently setup a few of our Elixir services to send json-formatted logging to Datadog. The problem is the logs get a lot of noise (and extra cost) from the uptime pings we make to the services. Those are required for other alerting to coordination in the system, but they offer little value in Datadog.

So, how do you skip logging for one particular route? It turns out one great way is through Plug. Alex Koutmos created a Hex package that makes it even easier called Unplug.

For the most part, it just requires making a change to the endpoint.ex file where Plug.Telemetry is added:

  plug Unplug,
    if: {Unplug.Predicates.RequestPathNotIn, ["/healthz"]},
    do: {Plug.Telemetry, event_prefix: [:phoenix, :endpoint]}

That simple logic will exclude the /healthz route. Unplug offers other predicates to follow other logic.


Ruby Regexp Capture Groups

Published by: Todd Resudek on April 16, 2023

I recently ran into a weird issue in code that resulted in a long number not being formatted the way I was expecting.

The code was expected to take a long string of numbers, and format them in a pre-defined way.

num_string = "123234345456567678789890901012"
num_string.length  \\ 30

In this case, we want to break this into groups of 3 digits. The expected result would be:

"123-234-345-456-567-678-789-890-901-012"

The regular expression uses 10 capture groups to perform the formatting:

num_string.sub(/(\d{3})(\d{3})(\d{3})(\d{3})(\d{3})(\d{3})(\d{3})(\d{3})
(\d{3})(\d{3})/, '\1-\2-\3-\4-\5-\6-\7-\8-\9-\10')

The issue is that the output of this is:

"123-234-345-456-567-678-789-890-901-1230"

The difference is subtle, which is probably why we didn’t detect it earlier. if you look at the last replacement group, it actually users capture group 1 again, followed by the literal “0” instead of the tenth capture group.

As it turns out, Ruby only supports capture groups 0-9 in this style.


New dbg/2 function

Published by: Todd Resudek on January 30, 2023

Andrea Leopardi from the Elixir Core team recently introduced a new debug feature in Elixir 1.14+.

  "Example"
  |> String.upcase()
  |> String.split("")
  |> Enum.shuffle()
  |> dbg()

When running this code, the pipeline will stop at each step.

  pry(1)> next
  "Example" #=> "Example"
  pry(1)> next
  |> String.upcase() #=> "EXAMPLE"
  pry(1)> next
  |> String.split("") #=> ["", "E", "X", "A", ...]
  pry(1)> next
  |> Enum.shuffle() #=> ["M", "X", "L", "P", ...]
  :ok

Creating a mix task

Published by: Todd Resudek on January 27, 2023

Creating a custom Mix task in Elixir is as easy as defining a module in the Mix.Tasks namespace.

defmodule Mix.Tasks.Custom do
  use Mix.Task

  @shortdoc "Runs a custom task"
  def run(kw_args) do
    #do something with those keyword args
    IO.inspect(kw_args)
  end
end

Now, you can run mix custom to execute this task.