Extract n-th argument of all function calls in a project

I have a number of different notifications, each identified by an id (example “ticket_closed”).

This ID can later be used to select the right E-Mail Template for the right language etc.

So basically, the recipient, the notification id and some metadata is stored as an Oban Job,
and the E-Mail is sent from there.

I would love to automatically extract all the available notification IDs, for 2 reasons:

  1. I could then check if I have an E-Mail template for each ID and
  2. I can check if any references to notification IDs are actually referring to existing IDs

I am pretty sure this is a rather simple task with mix/elixir, but so far I could not figure it out.

How could I extract all calls to a function and the functions n-th argument?


I found the tracers: compile options, however I am not sure how to add a Tracer inside a project that is meant to trace the project itselft (and would therefore needs itself compiled before it can compile itself?)

Do you want trace function calls of a running app which you have access to? Or is it something you want to add to the code of your app which you’ll then deploy and run?
Can you please post an example of the code and function calls you want to trace?

He @alvises, thanks for the reply.

I do not want any runtime tracing.

It would be great to find all functions calls for

Notifications.External.schedule(
  reporting_user.id,
  link_to(helpdesk_ticket),
  :ticket_updated,          # Notification ID
  metadata
)

and extract the third argument Notification ID into a list.

Boundary seems to use a custom compiler (Mix.Task.Compiler implementation) which registeres itself as the tracer module. That probably makes mix load that module, before compilation of the project itself.

2 Likes

@larshei I just saw you don’t need a tracer, you can ignore the post below (I didn’t find a way to delete my post :sweat_smile: )


For debugging purpose, for an app at runtime, I’ve used on a running distributed cluster the :dbg module, which is really simple to use.

# tracing handler
handler = fn {_, pid, _, {module, fun, args}, timestamp}=_data, trace_name ->
    IO.inspect({pid, module, fun, args, timestamp, trace_name})
end

# Start tracer with a handler
:dbg.tracer(:process, {handler, "my tracer"})

#  trace pattern. This function enables call trace for one or more functions.
:dbg.tp(String, :downcase, 1, [])

# traces :all
# :all -> All processes and ports in the system as well as all processes and ports created hereafter are to be traced.
# :call -> traces global function calls for the process according to the trace patterns set in the system
# :timestamp -> Includes a time stamp in all trace messages.
:dbg.p(:all, [:call, :timestamp])
iex> String.downcase("Hello")
{#PID<0.111.0>, String, :downcase, ["hello"], {1704, 212048, 422855},
 "my tracer"}
"hello"

https://www.erlang.org/doc/man/dbg#tracer-2
https://www.erlang.org/doc/man/dbg#tp-2
https://www.erlang.org/doc/man/dbg#p-2

1 Like

You can try using GitHub - ast-grep/ast-grep: ⚡A CLI tool for code structural search, lint and rewriting. Written in Rust.

Given this input on my machine:

# Put this in f.ex. `test.ex`

defmodule Notifications.External do
  def schedule(user_id, link, notification_type, metadata) do
    IO.puts(
      "Scheduling stuff for user #{user_id}: " <>
        " link=#{link}, " <>
        "notification_type=#{notification_type}, " <> "metadata=#{inspect(metadata)}"
    )
  end
end

defmodule UserModuleOne do
  def function_one() do
    Notifications.External.schedule(1, "nope", :ticket_created, %{})
  end
end

defmodule UserModuleTwo do
  def function_two() do
    Notifications.External.schedule(2, "never", :ticket_updated, %{})
  end
end

defmodule UserModuleThree do
  def function_three() do
    Notifications.External.schedule(3, "not_happening", :ticket_closed, %{})

    task1 =
      Task.async(fn ->
        Notifications.External.schedule(4, "gone", :ticket_removed, %{})
      end)

    task2 =
      Task.async(fn ->
        Notifications.External.schedule(5, "you_serious?", :ticket_closed, %{})
      end)

    Task.await_many([task1, task2])
  end
end

Then you just run this:

sg -l ex -p 'Notifications.External.schedule($USER_ID, $LINK, $TYPE, $METADATA)' --rewrite '$TYPE' --json=compact | jq -r '.[] | .replacement' | sort | uniq

And that gives you:

:ticket_closed
:ticket_created
:ticket_removed
:ticket_updated

NOTE: it’s not perfect f.ex. if you have code that does alias Notifications.External and then calls External.schedule(...) then this incantation will not catch it. Though you can also make variants for any levels of nesting i.e. replace the pattern Notifications.External.schedule($USER_ID, $LINK, $TYPE, $METADATA) with External.schedule($USER_ID, $LINK, $TYPE, $METADATA) and you should be good. (And then you should be doing a union of all these results which is pretty easy with cat and then doing sort | uniq again.)

1 Like

That is veeerry cool and will for sure be added to my list of tools.

This can get the job done I think, will try in a bit.

I am still curious about an erlang/elixir solution.