Skip to content

Latest commit

 

History

History
141 lines (107 loc) · 4.63 KB

backend.md

File metadata and controls

141 lines (107 loc) · 4.63 KB

Back-end Architecture

The IDP is a Rails application, that follows many typical Rails conventions.

Networking

For consistency, we use Faraday when making HTTP requests. We also wire in notifications so we can log metrics on these requests

# request_metric is logged specifically as a metric to allow for quicker data aggregation and
# historical querying
conn = Faraday.new do |f|
  f.request :instrumentation, name: 'request_metric.faraday'
end

# request_log is logged, but only to the log file, typically for requests where we are
# less interested in aggregation and generally only need to view the attributes of a specific
# request
conn = Faraday.new do |f|
  f.request :instrumentation, name: 'request_log.faraday'
end

# service_name is a required context attribute and is the unique identifier for the request.
# Requests within the same service (e.g. a POST, GET, etc. to different resources) should have a
# distinct service_name.
resp = conn.post do |req|
  req.options.context = { service_name: 'aamva_token' }
end

Forms, FormResponse, Analytics, and Controllers

We aim to keep Controllers simple and lean, and put business logic in Form classes, and hand those results (FormResponse) to our Analytics class to get logged in a consistent way.

For details on frontend form behaviors, refer to the equivalent section of the Front-end Architecture document.

FormResponse

The FormResponse is a simple structure to help bundle up properties for logging. Do not put PII or sensitive information inside these because they are intended to be logged.

FormResponse.new(
  success: true | false,
  errors: Hash | ActiveModel::Errors,
  extra: Hash,
)

Forms

We use ActiveModel::Model validations to help build useful error structures.

Forms should have a #submit method that returns a FormResponse.

  • success: is usually #valid? from ActiveModel
  • errors: is usually #errors from ActiveModel
  • extra: is, by convention, a method called extra_analytics_attributes that returns a Hash
def submit
  FormResponse.new(
    success: valid?,
    errors: errors,
    extra: extra_analytics_attributes,
  )
end

For sensitive properties, or results that are not meant to be logged, add properties to the Form object that get written during #submit

Analytics

Analytics events are appended to log/events.log and contain information both common information as well as custom event properties. Common information includes service provider, user ID, browser details, and other information.

Event names correspond to methods in the AnalyticsEvents mixin. We document these with YARD so that we can auto-generate documentation on them in our handbook.

Note

The convention to name events to match the method name is expected for all new analytics events, but you will find a number of exceptions for analytics which had existed prior to this convention being established.

If you are adding or troubleshooting events, consider running the watch_events Makefile target in a separate terminal. This command will print formatted event data as it happens, so you can see what events are logged as you navigate the application in your local development environment.

make watch_events

You can also watch for specific events by assigning the EVENT_NAME environment variable:

EVENT_NAME="piv_cac_disabled" make watch_events

Controllers

These tie everything together! We aim for lean, "RESTful" controllers

  • Keep as much business logic as possible out of controllers moving that logic into Forms or Services

  • Prefer adding a new controller with one of the CRUD methods over creating a custom method in an existing controller. For example, if your app allows a user to update their email and their password on two different pages, instead of using a single controller with methods called update_email and update_password, create two controllers and name the methods update, i.e. EmailsController#update and PasswordsController#update. See http://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/ for more about this design pattern.

class MyController < ApplicationController
  def update
    form = MyForm.new(params)

    result = form.submit
    analytics.my_event(**result)

    if result.success?
      do_something(form.sensitive_value_here)
    else
      do_something_else
    end
  end
end