Add "hooks" to read actions to run extra code similar to change after_transaction

This post actually have 2 questions that are kinda related one to another.

Here is what I’m trying to solve, I have some read action in my resource that, when called by our GraphQL API, should also do a :telemetry call to bump our view count metric.

My current solution was to create a manual Absynthe object that calls the read action directly and execute the telemetry as-well:

defmodule Marketplace.Markets.Property.GraphQL do
  @moduledoc false

  alias Marketplace.Markets.Property

  use Absinthe.Schema.Notation

  object :markets_property_queries do
    field :get_valid_property, :property do
      arg :id, non_null(:id)

      resolve fn _, %{id: id}, %{context: %{actor: actor}} ->
        case Property.get_valid(%{id: id}, actor: actor) do
          {:ok, property} ->
            execute(property, actor)

          {:error, %Ash.Error.Forbidden{}} ->
            {:error, :forbidden}

          {:error, _} ->
            {:error, :not_found}
        end
      end
    end
  end

  defp execute(%{id: property_id} = property, %{id: actor_id} = _actor) do
    :telemetry.execute([:property, :view], %{}, %{
      property_id: property_id,
      user_id: actor_id
    })

    {:ok, property}
  end

  defp execute(%{id: property_id} = property, _actor) do
    :telemetry.execute([:property, :view], %{}, %{
      property_id: property_id,
      user_id: nil
    })

    {:ok, property}
  end
end

This works, but it is very verbose and sits outside the resource.

It also has a problem when the GraphQL user requests to load some other reference inside the resource, it will just fail since the action itself doesn’t load that resource by default.

So, my first question is, can I move this telemetry call inside the action somehow? If it was a create, update, destroy action, I would just simply create a change with after_transaction and put the telemetry there, but this is a read action, so I’m not sure if there is anything “similar” to use for this.

My second question is, assuming the first one is possible, does AshGraphQL add some metadata to the context or somewhere else so I can know that the actions was actually called by my GraphQL API and not by calling the action directly? I just want to trigger the telemetry call when the action is called via my GraphQL API.

1 Like

Hey! So to answer your specific question:

read :... do
  prepare fn query, _ -> 
    query
    |> Ash.Query.before_action(...)
    |> Ash.Query.after_action(...)
  end
end

You can use preparations and the hooks available to queries to do this.

However, to solve for this general kind of thing, we already broadcast telemetry hooks. You could do something like this when your application starts up.

:telemetry.attach(
  "some_id",
  [:ash, :your_api_short_name, :read], 
  &emit_your_events_when_you_see_a_matching_event/4
)
3 Likes

Actually, I just realized that you want to emit an event that includes information about the specific property that was viewd. In that case you would likely need to do it with an after_action hook, not using the telemetry event because I don’t believe we provide the query results to the telemetry hooks.

Thanks @zachdaniel , I was not aware that Ash.Query hade these hooks!

Worked like a charm!