Skip to content

Commit

Permalink
introduce :send and :send_error telemetry events
Browse files Browse the repository at this point in the history
  • Loading branch information
asakura committed Nov 3, 2023
1 parent 90565e0 commit 83ec8d1
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 32 deletions.
35 changes: 30 additions & 5 deletions lib/mixpanel/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ defmodule Mixpanel.Client do
{gen_server_opts, opts} =
Keyword.split(init_args, [:debug, :name, :timeout, :spawn_opt, :hibernate_after])

opts = Keyword.take(opts, [:project_token, :base_url, :http_adapter])
opts =
opts
|> Keyword.take([:project_token, :base_url, :http_adapter])
|> Keyword.put(:name, gen_server_opts[:name])

GenServer.start_link(__MODULE__, opts, gen_server_opts)
end
Expand Down Expand Up @@ -107,6 +110,7 @@ defmodule Mixpanel.Client do

client_span =
Mixpanel.Telemetry.start_span(:client, %{}, %{
name: state.name,
base_url: state.base_url,
http_adapter: state.http_adapter
})
Expand All @@ -128,9 +132,30 @@ defmodule Mixpanel.Client do

case HTTP.get(state.http_adapter, state.base_url <> @track_endpoint, [], params: [data: data]) do
{:ok, _, _, _} ->
Mixpanel.Telemetry.untimed_span_event(
state.span,
:send,
%{
event: event
# payload_size: byte_size(payload)
},
%{name: state.name}
)

:ok

_ ->
{:error, reason} ->
Mixpanel.Telemetry.span_event(
state.span,
:send_error,
%{
event: event,
error: reason
# payload_size: byte_size(payload)
},
%{name: state.name}
)

Logger.warning(%{message: "Problem tracking event", event: event, properties: properties})
end

Expand All @@ -150,7 +175,7 @@ defmodule Mixpanel.Client do
{:ok, _, _, _} ->
:ok

_ ->
{:error, _reason} ->
Logger.warning(%{message: "Problem tracking profile update", event: event})
end

Expand Down Expand Up @@ -181,7 +206,7 @@ defmodule Mixpanel.Client do
{:ok, _, _, _} ->
:ok

:ignore ->
{:error, _} ->
Logger.warning(%{
message: "Problem creating profile alias",
alias: alias,
Expand All @@ -196,7 +221,7 @@ defmodule Mixpanel.Client do
@spec terminate(reason, State.t()) :: :ok
when reason: :normal | :shutdown | {:shutdown, term} | term
def terminate(_reason, state),
do: Mixpanel.Telemetry.stop_span(state.span)
do: Mixpanel.Telemetry.stop_span(state.span, %{}, %{name: state.name})

defp put_token(events, project_token) when is_list(events),
do: Enum.map(events, &put_token(&1, project_token))
Expand Down
16 changes: 11 additions & 5 deletions lib/mixpanel/client/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,38 @@ defmodule Mixpanel.Client.State do

@type project_token :: String.t()
@type base_url :: String.t()
@type http_adapter :: module()
@type name :: atom

@type option ::
{:project_token, project_token}
| {:base_url, base_url}
| {:http_adapter, module}
| {:http_adapter, http_adapter}
| {:name, name}

@type t :: %__MODULE__{
project_token: project_token,
base_url: base_url,
http_adapter: module,
http_adapter: http_adapter,
name: name,
span: nil | Mixpanel.Telemetry.t()
}

@enforce_keys [:project_token, :base_url, :http_adapter]
defstruct [:project_token, :base_url, :http_adapter, :span]
@enforce_keys [:project_token, :base_url, :http_adapter, :name]
defstruct @enforce_keys ++ [:span]

@spec new([option, ...]) :: t()
def new(opts) do
project_token = Keyword.fetch!(opts, :project_token)
base_url = Keyword.fetch!(opts, :base_url)
http_adapter = Keyword.fetch!(opts, :http_adapter)
name = Keyword.fetch!(opts, :name)

%__MODULE__{
project_token: project_token,
base_url: base_url,
http_adapter: http_adapter
http_adapter: http_adapter,
name: name
}
end

Expand Down
8 changes: 4 additions & 4 deletions lib/mixpanel/http.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule Mixpanel.HTTP do
opts :: keyword
) ::
{:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
| :ignore
| {:error, String.t()}
def get(client, url, headers, opts) do
{params, opts} = Keyword.pop(opts, :params, nil)
retry(url, fn -> client.get(build_url(url, params), headers, opts) end, @max_retries)
Expand All @@ -41,15 +41,15 @@ defmodule Mixpanel.HTTP do
opts :: keyword
) ::
{:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
| :ignore
| {:error, String.t()}
def post(client, url, payload, headers, opts \\ []) do
retry(url, fn -> client.post(url, payload, headers, opts) end, @max_retries)
end

@spec retry(String.t(), (-> {:ok, any, any, any} | {:error, String.t()}), non_neg_integer) ::
{:ok, any, any, any} | :ignore
{:ok, any, any, any} | {:error, String.t()}
defp retry(_url, _fun, 0) do
:ignore
{:error, "Max retries reached"}
end

defp retry(url, fun, attempts_left) do
Expand Down
62 changes: 48 additions & 14 deletions lib/mixpanel/telemetry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ defmodule Mixpanel.Telemetry do
This event contains the following measurements:
* `monotonic_time` - The time of this event, in `:native` units
* `monotonic_time`: The time of this event, in `:native` units
This event contains the following metadata:
* `base_url` - The URL which a client instance uses to communicate with
* `name`: The name of the client
* `base_url`: The URL which a client instance uses to communicate with
the Mixpanel API
* `http_adapter` - The HTTP adapter which a client instance uses to send
* `http_adapter`: The HTTP adapter which a client instance uses to send
actual requests to the backend
This span is ended by the following event:
Expand All @@ -36,11 +37,42 @@ defmodule Mixpanel.Telemetry do
This event contains the following metadata:
* `base_url` - The URL which a client instance uses to communicate with
* `name`: The name of the client
* `base_url`: The URL which a client instance uses to communicate with
the Mixpanel API
* `http_adapter` - The HTTP adapter which a client instance uses to send
* `http_adapter`: The HTTP adapter which a client instance uses to send
actual requests to the backend
The following events may be emitted within this span:
* `[:mixpanel_api_ex, :client, :send]`
Represents a request sent to the Mixpanel API
This event contains the following measurements:
* `event`: The name of the event that was sent
* `payload_size`: The size (in bytes) of the payload has been sent
This event contains the following metadata:
* `telemetry_span_context`: A unique identifier for this span
* `name`: The name of the client
* `[:mixpanel_api_ex, :client, :send_error]`
An error occurred while sending a request to the Mixpanel API
This event contains the following measurements:
* `event`: The name of the event that was attempted to send
* `error`: A description of the error
* `payload_size`: The size (in bytes) of the payload that were attempted to send
This event contains the following metadata:
* `telemetry_span_context`: A unique identifier for this span
* `name`: The name of the client
"""

@enforce_keys [:span_name, :telemetry_span_context, :start_time, :start_metadata]
Expand All @@ -60,15 +92,15 @@ defmodule Mixpanel.Telemetry do
@type measurements :: :telemetry.event_measurements()

@typedoc false
@type event_name :: :ready
@type event_name :: :ready | :send_error

@typedoc false
@type untimed_event_name :: :stop
@type untimed_event_name :: :stop | :send

@app_name :mixpanel_api_ex

@doc false
@spec start_span(span_name(), measurements(), metadata()) :: t()
@spec start_span(span_name, measurements, metadata) :: t
def start_span(span_name, measurements, metadata) do
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
telemetry_span_context = make_ref()
Expand All @@ -84,7 +116,7 @@ defmodule Mixpanel.Telemetry do
end

@doc false
@spec stop_span(t(), measurements(), metadata()) :: :ok
@spec stop_span(t, measurements, metadata) :: :ok
def stop_span(span, measurements \\ %{}, metadata \\ %{}) do
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)

Expand All @@ -96,13 +128,15 @@ defmodule Mixpanel.Telemetry do
untimed_span_event(span, :stop, measurements, metadata)
end

# @doc false
# @spec span_event(t(), event_name(), measurements(), metadata()) :: :ok
# def span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
# end
@doc false
@spec span_event(t, event_name, measurements, metadata) :: :ok
def span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
measurements = Map.put_new_lazy(measurements, :monotonic_time, &monotonic_time/0)
untimed_span_event(span, name, measurements, metadata)
end

@doc false
@spec untimed_span_event(t(), event_name() | untimed_event_name(), measurements(), metadata()) ::
@spec untimed_span_event(t, event_name | untimed_event_name, measurements, metadata) ::
:ok
def untimed_span_event(span, name, measurements \\ %{}, metadata \\ %{}) do
metadata = Map.put(metadata, :telemetry_span_context, span.telemetry_span_context)
Expand Down
42 changes: 38 additions & 4 deletions test/mixpanel/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ defmodule MixpanelTest.ClientTest do
{:ok, collector_pid} =
start_supervised({Mixpanel.TelemetryCollector, [[:mixpanel_api_ex, :client, :start]]})

{:ok, %Mixpanel.Client.State{}} =
{:ok, %Mixpanel.Client.State{} = state} =
Mixpanel.Client.init(
project_token: "token",
base_url: "base url",
http_adapter: MixpanelTest.NoOp
http_adapter: Mixpanel.HTTP.NoOp,
name: make_ref()
)

# We expect a monotonic start time as a measurement in the event.
Expand All @@ -20,8 +21,9 @@ defmodule MixpanelTest.ClientTest do
{[:mixpanel_api_ex, :client, :start], %{monotonic_time: integer()},
%{
telemetry_span_context: reference(),
name: state.name,
base_url: "base url",
http_adapter: MixpanelTest.NoOp
http_adapter: Mixpanel.HTTP.NoOp
}}
]
end
Expand All @@ -33,7 +35,8 @@ defmodule MixpanelTest.ClientTest do
Mixpanel.Client.init(
project_token: "token",
base_url: "base url",
http_adapter: MixpanelTest.NoOp
http_adapter: Mixpanel.HTTP.NoOp,
name: make_ref()
)

{:ok, collector_pid} =
Expand All @@ -57,4 +60,35 @@ defmodule MixpanelTest.ClientTest do
assert stop_metadata == state.span.start_metadata
end
end

describe "handle_cast/2" do
setup do
{:ok, state} =
Mixpanel.Client.init(
project_token: "token",
base_url: "base url",
http_adapter: Mixpanel.HTTP.NoOp,
name: make_ref()
)

{:ok, state: state}
end

test "emits telemetry event with expected timings", %{state: state} do
{:ok, collector_pid} =
start_supervised({Mixpanel.TelemetryCollector, [[:mixpanel_api_ex, :client, :send]]})

{:noreply, %Mixpanel.Client.State{}} =
Mixpanel.Client.handle_cast({:track, "event", %{}}, state)

assert Mixpanel.TelemetryCollector.get_events(collector_pid)
~> [
{[:mixpanel_api_ex, :client, :send], %{event: "event"},
%{
telemetry_span_context: reference(),
name: state.name
}}
]
end
end
end

0 comments on commit 83ec8d1

Please sign in to comment.