SmSmsCampaignWebhook

Southern Made - Galaxy Logo by Southern Made

Gem Version CI Code Climate Maintainability Code Climate Test Coverage

This gem will help app implementors using Rails setup a webhook to ingest data from Southern Made's SMS campaign service. Processing the data can help with:

  • user engagement
  • entry tracking
  • vote tracking
  • data report
  • other useful things

Work closely with your Southern Made project manager to gather details about what needs to be tracked, what fields to verify, and which scenarios are expected to be supported!

Table of Contents

Installation

This gem is tested with Rails 6.0.x, 6.1.x, 7.0.x versions.

Add this line to your application's Gemfile:

gem "sm_sms_campaign_webhook", "~> 2.1"

And then execute:

$ bundle

Or install it yourself as:

$ gem install sm_sms_campaign_webhook

Configuration

These are the steps to configure your app to be ready to capture SMS campaign service payloads.

Auto Generate Config

You can setup most app configuration by running the generator:

$ bundle exec rails generate sm_sms_campaign_webhook:install

Some things will still require manual configuration and will be identified after generation:

After that, be sure to read the Usage section for payload processor details!

If you prefer to setup everything by hand, be sure to check out:

Webhook Auth Token

The SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN value is required to be an ENV value to avoid leaking production values. It will be used to authorize payload requests from the SMS campaign service.

Set this value using the rails secret generator:

$ bundle exec rails secret

And copy the result to your .env or applicable config file:

SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN="******"

ActiveJob

Payloads will be dispatched and processed asynchronously using ActiveJob. Southern Made prefers that the app be configured with Sidekiq as the queue adapter.

If you have already chosen another queue adapter then feel free to ignore this step!

You can set the adapter in config/application.rb with:

class Application < Rails::Application
  config.active_job.queue_adapter = :sidekiq
end

Add config/sidekiq.yml config with:

---
:concurrency: <%= ENV.fetch("SIDEKIQ_CONCURRENCY") { 5 }.to_i %>
:timeout: <%= ENV.fetch("SIDEKIQ_TIMEOUT") { 25 }.to_i %>
:queues:
  - default
  - mailers

Add config/initializers/sidekiq.rb with:

# @note Sidekiq server + client must both be configured for Redis.
# @see https://github.com/mperham/sidekiq/wiki/Using-Redis

Sidekiq.configure_server do |config|
  config.redis = {
    url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
    network_timeout: ENV.fetch("REDIS_NETWORK_TIMEOUT") { 5 }.to_i
  }
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
    network_timeout: ENV.fetch("REDIS_NETWORK_TIMEOUT") { 5 }.to_i
  }
end

Update your Procfile or appropriate config to launch worker processes:

worker: RAILS_MAX_THREADS=${SIDEKIQ_CONCURRENCY:-5} bundle exec sidekiq --config config/sidekiq.yml

More detailed instructions about using Sidekiq can be found in the Sidekiq Wiki.

Mount the Webhook Engine

If you opted to auto generate the config, this can be skipped.

Add the following to config/routes.rb in your app to mount the webhook:

mount SmSmsCampaignWebhook::Engine => "/sms_campaign"

This sets the app up to receive POST requests from the SMS campaign service:

POST /sms_campaign/api/webhook

Be sure to replace /sms_campaign with whatever mount point you choose. Once you share the webhook URI with your project manager, avoid changing it; they will configure it with the correspending SMS campaign!

Webhook Initializer

If you opted to auto generate the config, this can be skipped.

App implementors must configure some library options. Here are all supported configuration options identifying their default values for config/initializers/sm_sms_campaign_webhook.rb:

require "sm_sms_campaign_webhook"

SmSmsCampaignWebhook.config do |config|
  # SMS campaign payload processor implementing SmSmsCampaignWebhook::Processable behavior.
  # default: SmSmsCampaignWebhook::DefaultProcessor (raises errors for processing)
  # config.processor = SmsPayloadProcessor
end

Payload Processor

If you opted to auto generate the config, this can be skipped. However, you will still need to implement the processor methods!

The default payload processor will raise errors while processing. You are required to provide a working payload processor to properly handle the data received from the SMS campaign service.

To create a processor, create a custom class mixing in SmSmsCampaignWebhook::Processable behavior. For example, we can create a custom processor named SmsPayloadProcessor:

class SmsPayloadProcessor
  include SmSmsCampaignWebhook::Processable

  # Implement required methods for Processable behavior.

  # @param campaign_engagement [SmSmsCampaignWebhook::CampaignEngagement]
  # def self.process_campaign_engagement(campaign_engagement)
  #   # NOOP - I need to be implemented.
  # end
end

This class will continue raising errors until the required methods are implemented. Please see the Processable mixin for expected method definitions.

Finally the configuration needs to be updated to use the custom processor. Add this within the config block in config/initializers/sm_sms_campaign_webhook.rb:

SmSmsCampaignWebhook.config do |config|
  #...
  config.processor = SmsPayloadProcessor
  #...
end

Usage

The main goal is to ingest the data contained in payloads received from the SMS campaign service. Your app knows best what to do with the data, so your primary focus is implementing the required methods of a payload processor.

Assuming that you completed configuration with the auto generate installer or manually created a processor, this section will expand what to do with it.

Campaign Engagement

This payload represents a user's phone interaction with the SMS campaign. This includes:

  • First contact with a SMS campaign by keyword
  • Responding to subsequent SMS campaign messages
  • Continued engagement for multi-entry SMS campaigns

Payloads will POST to the webhook every time a phone interacts with the campaign, so the processor behavior should expect to see repeats of inbound payloads from a phone!

It is important that you work closely with your Southern Made project manager to determine which scenarios are relevant for your app. They will be able to tell you:

  • Fields + value types that will be in answers
  • Required fields to complete registration/entry
  • How to interpret voting style numeric answers

Processor Expections

You must implement behavior for this method to ingest campaign engagement data in your paylod processor:

def self.process_campaign_engagement(campaign_engagement)
  # ...
end

It will receive an instance of the SmSmsCampaignWebhook::CampaignEngagement data model. This method will need to handle scenarios such as:

  • Registering/creating a user account
  • Logging registration/entries
  • Interpreting + logging vote responses

Check with your Southern Made project manager for expectations.

Campaign Engagement Data Model

The payload will be modeled with the SmSmsCampaignWebhook::CampaignEngagement class. It provides basic methods to extract values out of the payload. The data model coerces values to the appropriate types in Ruby.

Some example message passing to an instance:

campaign_engagement.event_uuid        # UUID - unique payload event
campaign_engagement.campaign_keyword  # String - SMS campaign entry point
campaign_engagement.phone_id          # Integer - represents specific phone
campaign_engagement.phone_number      # String - phone number interacting with SMS campaign

# This represents a specific phone engaging with a specific campaign.
# The value will differ for each entry in multi-entry campaigns.
# For standard campaigns we will only see one value.
campaign_engagement.phone_campaign_state_id   # Integer

# These values help determine if and when answers were received
# for all campaign messages.
campaign_engagement.phone_campaign_state_completed?     # TrueClass,FalseClass
campaign_engagement.phone_campaign_state_completed_at   # DateTime

It also provides a useful helper methods related to campaign engagement answers. For example:

# Are any campaign engagement answers in the payload?
campaign_engagement.phone_campaign_state_answers? # TrueClass,FalseClass

# This tries to find an answer for the requested field.
# If a match is found it returns instance of
# SmSmsCampaignWebhook::CampaignEngagement::Answer data model.
# If a match is not found it return nil (NilClass).
campaign_engagement.answer_for(field: "email")     # Returned type answer specific

Campaign Engagement Answer Data Model

The SmSmsCampaignWebhook::CampaignEngagement::Answer class models the answer data contained in the campaign engagement payload. It consists of:

  • field (String)
  • value (varies)
  • collected_at (DateTime)

The value data types could be one of the following:

  • string (String)
  • email (String)
  • date (Date)
  • number (Integer)
  • boolean (TrueClass, FalseClass)
  • us_state (String)

Campaign Engagement Payload Example

Here is an example payload for campaign engagement that could come through to the payload processor. Be sure to check with your Southern Made project manager to gather details about the answer fields and data types:

{
  "uuid": "99aaafe3-b52b-413f-a9cd-db52fa13b77a",
  "object": "event",
  "type": "campaign.engagement",
  "created_at": "2019-08-09T18:29:05.052Z",
  "data": {
    "campaign": {
      "id": 55,
      "keyword": "KEYWORD"
    },
    "phone": {
      "id": 80,
      "number": "3335557777"
    },
    "phone_campaign_state": {
      "id": 95,
      "answers": {
        "DOB": {
          "value": "2001-07-04",
          "collected_at": "2019-08-09T18:26:59.052Z"
        },
        "email": {
          "value": "[email protected]",
          "collected_at": "2019-08-09T18:27:59.052Z"
        },
        "vote-september": {
          "value": 1,
          "collected_at": "2019-08-09T18:28:59.052Z"
        }
      },
      "completed": true,
      "completed_at": "2019-08-09T18:28:59.052Z"
    }
  }
}

cURL example assuming the payload file path is tmp/sms_campaign_payload.json, app is running running with mount point sms_campaign, web server uses port 3000, and that you use your app's webhook auth token:

$ curl \
--header "Authorization: Bearer WEBHOOKAUTHTOKEN" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data @tmp/sms_campaign_payload.json \
http://localhost:3000/sms_campaign/api/webhook

Development

This gem uses git-flow to manage deployments. The default branches are used to manage development and production code.

StandardRB

This project uses StandardRB, a hands-off wrapper around Rubocop, to manage style/formatting/etc. Please apply changes before submitting pull requests:

$ bundle exec standardrb --fix

Versioning

Gem versioning follows Semantic Versioning.

Testing

This project uses Rspec for testing. Specs must be green for any PR to be accepted!

$ bundle exec rspec

The project is setup with GitHub Actions to automate testing. The various workflows and environments can be seen in .github/workflows/ci.yml.

Documentation

Library documentation is managed using YARD. Ideally, the documentation remains 100% covered as development progresses.

$ bundle exec yard

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/SouthernMade/sm_sms_campaign_webhook. Pull requests should be made against the develop branch. Extreme cases that require updates to supported gem versions will be handled case by case.

License

The gem is available as open source under the terms of the MIT License.