Skip to main content

src/langfuse_client@metrics.erl

-module(langfuse_client@metrics).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/langfuse_client/metrics.gleam").
-export([scorer_names/1, score_count_query/4, decode/1, list_score_counts/2, score_value_query/3, list_score_values/2, decode_score_values/1]).
-export_type([score_view/0, score_count_row/0, filter/0, string_options_operator/0, score_count_query/0, score_value_row/0, score_value_query/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " `GET /api/public/v2/metrics` — server-side aggregations over Langfuse\n"
    " score data. Build a query with `score_count_query` /\n"
    " `score_value_query` and pass it to the matching `list_*` function.\n"
    "\n"
    " The Langfuse v2 metrics endpoint is BETA. This module exposes score\n"
    " count + avg-value queries grouped by `(name, dataType, source)`, with\n"
    " optional server-side filters. Broader surface (other measures, views,\n"
    " dimensions) will follow the same shape once the endpoint stabilises.\n"
).

-type score_view() :: scores_numeric | scores_categorical.

-type score_count_row() :: {score_count_row,
        binary(),
        binary(),
        binary(),
        integer()}.

-type filter() :: {string_options,
        binary(),
        string_options_operator(),
        list(binary())}.

-type string_options_operator() :: any_of | none_of.

-type score_count_query() :: {score_count_query,
        score_view(),
        binary(),
        binary(),
        list(filter())}.

-type score_value_row() :: {score_value_row,
        binary(),
        binary(),
        binary(),
        float()}.

-type score_value_query() :: {score_value_query,
        binary(),
        binary(),
        list(filter())}.

-file("src/langfuse_client/metrics.gleam", 57).
?DOC(
    " Convenience: filter to scores whose `name` (scorer) is in the given\n"
    " list. Saves bytes on the wire and downstream work — server-side filter\n"
    " always preferred over client-side.\n"
).
-spec scorer_names(list(binary())) -> filter().
scorer_names(Names) ->
    {string_options, <<"name"/utf8>>, any_of, Names}.

-file("src/langfuse_client/metrics.gleam", 74).
?DOC(
    " Build a query for counts of scores grouped by `(name, data_type,\n"
    " source)` in the given window, with optional server-side filters.\n"
).
-spec score_count_query(score_view(), binary(), binary(), list(filter())) -> score_count_query().
score_count_query(View, From_timestamp, To_timestamp, Filters) ->
    {score_count_query, View, From_timestamp, To_timestamp, Filters}.

-file("src/langfuse_client/metrics.gleam", 103).
?DOC(
    " Parse a `GET /api/public/v2/metrics` response body for a score-count\n"
    " query. Useful if you already have the raw body in hand (e.g. from a\n"
    " cached/recorded response).\n"
).
-spec decode(binary()) -> {ok, list(score_count_row())} |
    {error, gleam@json:decode_error()}.
decode(Body) ->
    gleam@json:parse(Body, rows_decoder()).

-file("src/langfuse_client/metrics.gleam", 208).
-spec row_decoder() -> gleam@dynamic@decode:decoder(score_count_row()).
row_decoder() ->
    gleam@dynamic@decode:field(
        <<"name"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Name) ->
            gleam@dynamic@decode:field(
                <<"dataType"/utf8>>,
                {decoder, fun gleam@dynamic@decode:decode_string/1},
                fun(Data_type) ->
                    gleam@dynamic@decode:field(
                        <<"source"/utf8>>,
                        {decoder, fun gleam@dynamic@decode:decode_string/1},
                        fun(Source) ->
                            gleam@dynamic@decode:field(
                                <<"sum_count"/utf8>>,
                                {decoder,
                                    fun gleam@dynamic@decode:decode_string/1},
                                fun(Sum_count_str) ->
                                    Count = gleam@result:unwrap(
                                        gleam_stdlib:parse_int(Sum_count_str),
                                        0
                                    ),
                                    gleam@dynamic@decode:success(
                                        {score_count_row,
                                            Name,
                                            Data_type,
                                            Source,
                                            Count}
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/langfuse_client/metrics.gleam", 203).
-spec rows_decoder() -> gleam@dynamic@decode:decoder(list(score_count_row())).
rows_decoder() ->
    gleam@dynamic@decode:field(
        <<"data"/utf8>>,
        gleam@dynamic@decode:list(row_decoder()),
        fun(Rows) -> gleam@dynamic@decode:success(Rows) end
    ).

-file("src/langfuse_client/metrics.gleam", 274).
-spec string_options_operator_string(string_options_operator()) -> binary().
string_options_operator_string(Op) ->
    case Op of
        any_of ->
            <<"any of"/utf8>>;

        none_of ->
            <<"none of"/utf8>>
    end.

-file("src/langfuse_client/metrics.gleam", 262).
-spec filter_to_json(filter()) -> gleam@json:json().
filter_to_json(F) ->
    case F of
        {string_options, Column, Operator, Values} ->
            gleam@json:object(
                [{<<"type"/utf8>>, gleam@json:string(<<"stringOptions"/utf8>>)},
                    {<<"column"/utf8>>, gleam@json:string(Column)},
                    {<<"operator"/utf8>>,
                        gleam@json:string(
                            string_options_operator_string(Operator)
                        )},
                    {<<"value"/utf8>>,
                        gleam@json:array(Values, fun gleam@json:string/1)}]
            )
    end.

-file("src/langfuse_client/metrics.gleam", 258).
-spec filters_to_json(list(filter())) -> gleam@json:json().
filters_to_json(Filters) ->
    gleam@json:preprocessed_array(gleam@list:map(Filters, fun filter_to_json/1)).

-file("src/langfuse_client/metrics.gleam", 187).
-spec score_count_metrics() -> list(gleam@json:json()).
score_count_metrics() ->
    [gleam@json:object(
            [{<<"measure"/utf8>>, gleam@json:string(<<"count"/utf8>>)},
                {<<"aggregation"/utf8>>, gleam@json:string(<<"sum"/utf8>>)}]
        )].

-file("src/langfuse_client/metrics.gleam", 179).
-spec score_count_dimensions() -> list(gleam@json:json()).
score_count_dimensions() ->
    [gleam@json:object([{<<"field"/utf8>>, gleam@json:string(<<"name"/utf8>>)}]),
        gleam@json:object(
            [{<<"field"/utf8>>, gleam@json:string(<<"dataType"/utf8>>)}]
        ),
        gleam@json:object(
            [{<<"field"/utf8>>, gleam@json:string(<<"source"/utf8>>)}]
        )].

-file("src/langfuse_client/metrics.gleam", 196).
-spec view_string(score_view()) -> binary().
view_string(V) ->
    case V of
        scores_numeric ->
            <<"scores-numeric"/utf8>>;

        scores_categorical ->
            <<"scores-categorical"/utf8>>
    end.

-file("src/langfuse_client/metrics.gleam", 167).
-spec query_body(score_count_query()) -> binary().
query_body(Q) ->
    _pipe = gleam@json:object(
        [{<<"view"/utf8>>, gleam@json:string(view_string(erlang:element(2, Q)))},
            {<<"fromTimestamp"/utf8>>, gleam@json:string(erlang:element(3, Q))},
            {<<"toTimestamp"/utf8>>, gleam@json:string(erlang:element(4, Q))},
            {<<"dimensions"/utf8>>,
                gleam@json:preprocessed_array(score_count_dimensions())},
            {<<"metrics"/utf8>>,
                gleam@json:preprocessed_array(score_count_metrics())},
            {<<"filters"/utf8>>, filters_to_json(erlang:element(5, Q))}]
    ),
    gleam@json:to_string(_pipe).

-file("src/langfuse_client/metrics.gleam", 88).
?DOC(
    " Aggregate score counts for the query. Returns one row per distinct\n"
    " `(name, data_type, source)` combination present in the window after\n"
    " filters are applied. Erlang-only — see `langfuse_client/client` module\n"
    " docs.\n"
).
-spec list_score_counts(langfuse_client@client:client(), score_count_query()) -> {ok,
        list(score_count_row())} |
    {error, langfuse_client@client:error()}.
list_score_counts(C, Q) ->
    langfuse_client@client:send_get(
        C,
        <<"/api/public/v2/metrics"/utf8>>,
        [{<<"query"/utf8>>, query_body(Q)}],
        rows_decoder()
    ).

-file("src/langfuse_client/metrics.gleam", 133).
?DOC(
    " Build a query for avg score values grouped by `(name, data_type,\n"
    " source)` in the given window, with optional server-side filters. Only\n"
    " numeric and boolean scores are returned.\n"
).
-spec score_value_query(binary(), binary(), list(filter())) -> score_value_query().
score_value_query(From_timestamp, To_timestamp, Filters) ->
    {score_value_query, From_timestamp, To_timestamp, Filters}.

-file("src/langfuse_client/metrics.gleam", 254).
-spec lenient_float() -> gleam@dynamic@decode:decoder(float()).
lenient_float() ->
    gleam@dynamic@decode:one_of(
        {decoder, fun gleam@dynamic@decode:decode_float/1},
        [begin
                _pipe = {decoder, fun gleam@dynamic@decode:decode_int/1},
                gleam@dynamic@decode:map(_pipe, fun erlang:float/1)
            end]
    ).

-file("src/langfuse_client/metrics.gleam", 244).
-spec value_row_decoder() -> gleam@dynamic@decode:decoder(score_value_row()).
value_row_decoder() ->
    gleam@dynamic@decode:field(
        <<"name"/utf8>>,
        {decoder, fun gleam@dynamic@decode:decode_string/1},
        fun(Name) ->
            gleam@dynamic@decode:field(
                <<"dataType"/utf8>>,
                {decoder, fun gleam@dynamic@decode:decode_string/1},
                fun(Data_type) ->
                    gleam@dynamic@decode:field(
                        <<"source"/utf8>>,
                        {decoder, fun gleam@dynamic@decode:decode_string/1},
                        fun(Source) ->
                            gleam@dynamic@decode:field(
                                <<"avg_value"/utf8>>,
                                lenient_float(),
                                fun(Avg_value) ->
                                    gleam@dynamic@decode:success(
                                        {score_value_row,
                                            Name,
                                            Data_type,
                                            Source,
                                            Avg_value}
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/langfuse_client/metrics.gleam", 239).
-spec value_rows_decoder() -> gleam@dynamic@decode:decoder(list(score_value_row())).
value_rows_decoder() ->
    gleam@dynamic@decode:field(
        <<"data"/utf8>>,
        gleam@dynamic@decode:list(value_row_decoder()),
        fun(Rows) -> gleam@dynamic@decode:success(Rows) end
    ).

-file("src/langfuse_client/metrics.gleam", 230).
-spec score_value_metrics() -> list(gleam@json:json()).
score_value_metrics() ->
    [gleam@json:object(
            [{<<"measure"/utf8>>, gleam@json:string(<<"value"/utf8>>)},
                {<<"aggregation"/utf8>>, gleam@json:string(<<"avg"/utf8>>)}]
        )].

-file("src/langfuse_client/metrics.gleam", 218).
-spec value_query_body(score_value_query()) -> binary().
value_query_body(Q) ->
    _pipe = gleam@json:object(
        [{<<"view"/utf8>>, gleam@json:string(<<"scores-numeric"/utf8>>)},
            {<<"fromTimestamp"/utf8>>, gleam@json:string(erlang:element(2, Q))},
            {<<"toTimestamp"/utf8>>, gleam@json:string(erlang:element(3, Q))},
            {<<"dimensions"/utf8>>,
                gleam@json:preprocessed_array(score_count_dimensions())},
            {<<"metrics"/utf8>>,
                gleam@json:preprocessed_array(score_value_metrics())},
            {<<"filters"/utf8>>, filters_to_json(erlang:element(4, Q))}]
    ),
    gleam@json:to_string(_pipe).

-file("src/langfuse_client/metrics.gleam", 145).
?DOC(
    " Aggregate avg score values for the query. One row per `(name,\n"
    " data_type, source)` combination over the window after filters are\n"
    " applied. Erlang-only — see `langfuse_client/client` module docs.\n"
).
-spec list_score_values(langfuse_client@client:client(), score_value_query()) -> {ok,
        list(score_value_row())} |
    {error, langfuse_client@client:error()}.
list_score_values(C, Q) ->
    langfuse_client@client:send_get(
        C,
        <<"/api/public/v2/metrics"/utf8>>,
        [{<<"query"/utf8>>, value_query_body(Q)}],
        value_rows_decoder()
    ).

-file("src/langfuse_client/metrics.gleam", 159).
?DOC(
    " Parse a `GET /api/public/v2/metrics` response body for a score-value\n"
    " query.\n"
).
-spec decode_score_values(binary()) -> {ok, list(score_value_row())} |
    {error, gleam@json:decode_error()}.
decode_score_values(Body) ->
    gleam@json:parse(Body, value_rows_decoder()).