Global Analytics

I'm trying to generate global analytics from my resources: - my first attempt was to define aggregates on the resources itself but in my understanding those only work within relationships - now I'm working my way through defining a dataless resource which has a bunch of no_attributes? true relationships but I'm getting some errors including
errors: [
%Ash.Error.SimpleDataLayer.NoDataProvided{
errors: [
%Ash.Error.SimpleDataLayer.NoDataProvided{
Before I keep digging in this direction, is there an idiomatic way to generate analytics with global aggregation that do not have any entypoint resource?
5 Replies
ZachDaniel
ZachDaniel3mo ago
My suggestion is to use Ecto for this. We have have some features for reporting/analytics in the future but right now it's just not a strong suit. You can use your Ash resources as Ecto queries directly.
Marco Dell'Olio
Marco Dell'OlioOP3mo ago
ok, yeah looks like aggregates only work with a datelayer supported resource, any variation of this
relationships do
has_many :llm_transactions, Llm.LlmTransaction do
public? true
no_attributes? true

filter expr(not is_nil(action) and not is_nil(feature))
end
end

calculations do
calculate :avg_llm_spend_for_contact,
:decimal,
expr(avg(:llm_transactions, :cost_usd)) do
public? true
end
end
relationships do
has_many :llm_transactions, Llm.LlmTransaction do
public? true
no_attributes? true

filter expr(not is_nil(action) and not is_nil(feature))
end
end

calculations do
calculate :avg_llm_spend_for_contact,
:decimal,
expr(avg(:llm_transactions, :cost_usd)) do
public? true
end
end
returns %Ash.Error.Query.AggregatesNotSupported{ looks like this works just fine without using Ecto
def read(query, _, _opts, _context) do
{:ok, [%Llm.LlmAnalytics{avg_llm_spend_for_contact: avg_llm_spend_for_contact()}]}
end

defp avg_llm_spend_for_contact do
Llm.LlmTransaction
|> Ash.Query.filter(expr(not is_nil(contact_id) or not is_nil(person_id)))
|> Ash.aggregate!({:avg_llm_spend_for_contact, :avg, field: :cost_usd})
end
def read(query, _, _opts, _context) do
{:ok, [%Llm.LlmAnalytics{avg_llm_spend_for_contact: avg_llm_spend_for_contact()}]}
end

defp avg_llm_spend_for_contact do
Llm.LlmTransaction
|> Ash.Query.filter(expr(not is_nil(contact_id) or not is_nil(person_id)))
|> Ash.aggregate!({:avg_llm_spend_for_contact, :avg, field: :cost_usd})
end
actions do
defaults [:read]

read :get_llm_analytics do
get? true

manual Llm.LlmAnalytics.Actions.GetLlmAnalytics
end
end
actions do
defaults [:read]

read :get_llm_analytics do
get? true

manual Llm.LlmAnalytics.Actions.GetLlmAnalytics
end
end
ZachDaniel
ZachDaniel3mo ago
Read actions should return lists of records You should prefer generic actions for what you're doing.
Marco Dell'Olio
Marco Dell'OlioOP3mo ago
the manual ready allow me to expose the action to the graphql interface without having to specify the type, am I wrong?
ZachDaniel
ZachDaniel3mo ago
But it would be an incorrect type AshGraphql supports generic actions Ohhh sorry I realize you're making it a record nvm I thought you were just returning an integer I think I would still do a generic action since it can't be filtered etc and only ever returns a single thing
action :llm_analytics, :struct do
constraints [instance_of: __MODULE__]

run ...
end
action :llm_analytics, :struct do
constraints [instance_of: __MODULE__]

run ...
end

Did you find this page helpful?