Analytics

Our Analytics System is mainly focused around sending server-side events leveraging Segment to forward events to other various services.

For a list of current events that are being tracked, checkout this repository

Context

There are two different strategies for submitting Analytics events:

  1. GenServer.cast directly to Analytics.Worker
  2. Phoenix PubSub

Some history on approach 1: We had a problem where some server side events were not being sent if there was an error somewhere further up the callstack. For example, the Sticky Order would be successfully created, but then the "Order Completed" analytic event wouldn't fire because there was an error with (for example) updating maropost tags. This would cause some discrepencies in the data when building out analytic reports. To combat this, we created a GenServer (named Analytics.Worker) that processes our analytic events. By having an isolated process that is independent from our connection process, we get the benefit of error isolation. Now going back to the original example where a failed Maropost connection would prevent the Order Completed event from sending, as long as we send a message to the Analytics.Worker process with information about the event, we can fail anywhere in the pipeline. Sure you can argue that we could of wrapped the 3rd party call in a Task.async(fn -> .. end), but with GenServers we also get the additional benefit of controlling how we want to handle errors. Since Segment handles retries on their end, we don't have to worry about building a retry mechanism around sending Analytic events. This is not true for other third party services like Sticky or Maropost.

Some history on approach 2: We had retries around segment events which is awesome, but other services like Sticky and Maropost were still a problem. This issue, combined with experimenting with CQRS / ES led me down experimenting with a hybrid solution leveraging Phoenix PubSub to help with managing events. I'm not sure if this is the best approach, as I found that the generic implementation in WealthFit.Sticky.Tasks.CreateProspect is basically what we want across all services. If we can find a way to generalize this GenServer's implementation, we reuse the mechanical bits for retrying and change the GenServer to accept a Module.fn/arity signature.

Creating New Analytics Events

Using Analytics.Worker

  1. Create a handle_cast/2 in Analytics.Worker

  def handle_cast({<EVENT_IDENTIFIER_AS_ATOM>, params}, _state) do
    %{properties: properties, conn: conn, payload: payload} = params

    Segment.Spec.Track.build(conn, payload)
    |> Segment.Spec.Track.set_properties(properties)
    |> Segment.Spec.Track.set_event(<EVENT_NAME>)
    |> Segment.Spec.Track.submit()

    {:noreply, %{}}
  end
  1. Create a validate_token/1 entry for the given <EVENT_NAME>

  2. Create the public interface in Peacemaker.Analytics.

  3. def <NAME>(conn, payload, %{properties: properties}) do
     GenServer.cast(
       Analytics.Worker,
       {<EVENT_IDENTIFIER_AS_ATOM>, %{properties: properties, payload: payload, conn: conn}}
     )
    end

Using Phoenix PubSub

Footnotes

  1. The only events that are being sent from the Wealthfit site (via the Javascript library) are the Page View & Identify events. We opted for the frontend to still send page-view events because rewriting an internal analytics client for the frontend would essentially be rewriting the same logic as Segment's analytics library. However, opting for the javascript library is not a free tradeoff. We will need to do additional work to ensure that the server-side event gets correlated to the frontend's page view event. To accomplish this, we have an internal id commonly referred to as the "(wealthfit) tracking id" (wftid in maropost) that we need to pass the Segment's Identify call. This ensures that any events from the client or serverside under the same tracking_id will be associated together when viewing reports.

For reference, we create a tracking id when a new UserProfile gets created. User Profiles are created when a new Optin or Account is created.

  1. Forestadmin at its current state is mostly used a cruch interface for interacting with the database. The main use cases currently are to provide emergency support to either create or edit accounts, or to perform internal tasks such as creating a new on-demand class, syncing intensives, refreshing oauth tokens, etc. Because of this, we do not have updated analytic calls for the majority of the events that get triggered through Forestadmin. This is because there is no unified system that has been agreed upon for how to perform certain forestadmin tasks. It's hard to create a system around unknown tasks, so we opted to not send analytic events from any forestadmin action currently.