promoted-ruby-client

Ruby client designed for calling Promoted's Delivery and Metrics API.

More information at http://www.promoted.ai

Installation

gem 'promoted-ruby-client'

Local Development

  1. Clone or fork the repo on your local machine
  2. cd promoted-ruby-client
  3. bundle
  4. To test interactively: irb -Ilib -rpromoted/ruby/client

Dependencies

Faraday

HTTP client for calling Promoted.

Net::HTTP::Persistent

Faraday binding (provides connection pool support)

Concurrent Ruby

Provides a thread pool for making shadow traffic requests to Delivery API in the background on a subset of calls to deliver

Creating a Client

client = Promoted::Ruby::Client::PromotedClient.new

This client will suffice for building log requests. To send actually send traffing to the API, some configuration is required.

client = Promoted::Ruby::Client::PromotedClient.new({
  :metrics_endpoint => "https://<get this from Promoted>",
  :delivery_endpoint => "https://<get this from Promoted>",
  :metrics_api_key => "<get this from Promoted>",
  :delivery_api_key => "<get this from Promoted>"
})

Client Configuration Parameters

Name Type Description
:delivery_endpoint String POST URL for the Promoted Delivery API (get this from Promoted)
:metrics_endpoint String POST URL for the Promoted Metrics API (get this from Promoted)
:metrics_api_key String Used as the x-api-key header on Metrics API requests to Promoted (get this value from Promoted)
:delivery_api_key String Used as the x-api-key header on Delivery API requests to Promoted (get this value from Promoted)
:delivery_timeout_millis Number Timeout on the Delivery API call. Defaults to 3000.
:metrics_timeout_millis Number Timeout on the Metrics API call. Defaults to 3000.
:perform_checks Boolean Whether or not to perform detailed input validation, defaults to true but may be disabled for performance
:logger Ruby Logger-compatible logger Defaults to nil (no logging). Example: Logger.new(STDERR, :progname => 'promotedai')
:shadow_traffic_delivery_percent Number between 0 and 1 % of deliver traffic that gets directed to Delivery API as "shadow traffic". Defaults to 0 (no shadow traffic).
:send_shadow_traffic_for_control Boolean If true, the deliver method will send shadow traffic for users in the CONTROL arm of an experiment. Defaults to true.
:default_request_headers Hash Additional headers to send on the request beyond x-api-key. Defaults to {}
:default_only_log Boolean If true, the deliver method will not direct traffic to Delivery API but rather return a request suitable for logging. Defaults to false.
:should_apply_treatment_func Proc Called during delivery, accepts an experiment and returns a Boolean indicating whether the request should be considered part of the control group (false) or in the experiment (true). If nil, the default behavior of checking the experiement :arm is applied.
:warmup Boolean If true, the client will prime the Net::HTTP::Persistent connection pool on construction; this can make the first few calls to Promoted complete faster. Defaults to false.
:max_request_insertions Number Maximum number of request insertions that will be passed to Delivery API on a single request (any more will be truncated by the SDK). Defaults to 1000.

Data Types

UserInfo

Basic information about the request user. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :user_id | String | Yes | The platform user id, cleared from Promoted logs. :anon_user_id | String | Yes | A different user id (presumably a UUID) disconnected from the platform user id, good for working with unauthenticated users or implementing right-to-be-forgotten. :is_internal_user | Boolean | Yes | If this user is a test user or not, defaults to false. :ignore_usage | Boolean | Yes | If you want to ignore usage from this user, defaults to false.


CohortMembership

Useful fields for experimentation during the delivery phase. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :user_info | UserInfo | Yes | The user info structure.

:arm | String | Yes | 'CONTROL' or one of the TREATMENT values (see constants.rb).

Properties

Properties bag. Has the structure:

  :struct => {
    :product => {
      "id": "product3",
      "title": "Product 3",
      "url": "www.mymarket.com/p/3"
      # other key-value pairs...
    }
  }

Insertion

Content being served at a certain position. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :user_info | UserInfo | Yes | The user info structure. :insertion_id | String | Yes | Generated by the SDK (do not set) :request_id | String | Yes | Generated by the SDK when needed (do not set) :content_id | String | No | Identifier for the content to be shown, must be set. :retrieval_rank | Number | Yes | Optional original ranking of this content item. :retrieval_score | Number | Yes | Optional original quality score of this content item.

:properties | Properties | Yes | Any additional custom properties to associate. For v1 integrations, it is fine not to fill in all the properties.

Size

User's screen dimensions. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :width | Integer | No | Screen width

:height | Integer | No | Screen height

Screen

State of the screen including scaling. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :size | Size | Yes | Screen size

:scale | Float | Yes | Current screen scaling factor

ClientHints

Alternative to user-agent strings. See https://raw.githubusercontent.com/snowplow/iglu-central/master/schemas/org.ietf/http_client_hints/jsonschema/1-0-0 Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :is_mobile | Boolean | Yes | Mobile flag :brand | Array of ClientBrandHint | Yes | :architecture | String | Yes | :model | String | Yes | :platform | String | Yes | :platform_version | String | Yes | :ua_full_version | String | Yes |


ClientBrandHint

See https://raw.githubusercontent.com/snowplow/iglu-central/master/schemas/org.ietf/http_client_hints/jsonschema/1-0-0 Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :brand | String | Yes | Mobile flag :version | String | Yes |


Location

Information about the user's location. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :latitude | Float | No | Location latitude :longitude | Float | No | Location longitude

:accuracy_in_meters | Integer | Yes | Location accuracy if available

Browser

Information about the user's browser. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :user_agent | String | Yes | Browser user agent string :viewport_size | Size | Yes | Size of the browser viewport :client_hints | ClientHints | Yes | HTTP client hints structure

referrer | String | Yes | Request referrer

Device

Information about the user's device. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :device_type | one of (UNKNOWN_DEVICE_TYPE, DESKTOP, MOBILE, TABLET) | Yes | Type of device :brand | String | Yes | "Apple, "google", Samsung", etc. :manufacturer | String | Yes | "Apple", "HTC", Motorola", "HUAWEI", etc. :identifier | String | Yes | Android: android.os.Build.MODEL; iOS: iPhoneXX,YY, etc. :screen | Screen | Yes | Screen dimensions :ip_address | String | Yes | Originating IP address :location | Location | Yes | Location information

:browser | Browser | Yes | Browser information

Paging

Paging parameters.

DeliveryRequest.retrieval_insertion_offset also impacts paging. That field indicate the offset of the retrieved insertions that are passed into Request.insertion. See detailed documentation on paging and retrieval_insertion_offset.

Field Name Type Optional? Description
:offset Integer Yes The 0-based, starting index for the response page. This should be the global position.
:size Integer Yes The number of items to return in a response page.

Request

A request for content insertions. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :user_info | UserInfo | Yes | The user info structure. :request_id | String | Yes | Generated by the SDK when needed (do not set) :use_case | String | Yes | One of the use case values, i.e. 'FEED' (see constants.rb). :properties | Properties | Yes | Any additional custom properties to associate. :paging | Paging | Yes | Paging parameters :device | Device | Yes | Device information (as available) :disable_personalization | Boolean | Yes | If true, disables personalized inputs into Delivery algorithm.


DeliveryRequest

Input to deliver Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :experiment | CohortMembership | Yes | A cohort to evaluation in experimentation. :request | Request | No | The underlying request for content. :only_log | Boolean | Yes | Defaults to false. Set to true to override whether Delivery API is called for this request.

:retrieval_insertion_offset | Integer | Yes | The index of the retrieved insertions set on Request.insertion list. If just sending the top-N from your retrieval, this is 0. If this is the next batch (e.g. 500 to 999), then the value is 500. This can be used to send multiple groups of retrieved insertions. This interacts with the Paging fields. More detailed documentation on paging.

LogRequest

A log object that is sent as a RPC request to Promoted's Metrics API log endpoint. This is outputted from the deliver SDK call. Callers need to either input it into the send_log_request method or send it to the Metrics API directly. Clients should avoid manipulating this object directly.

ClientResponse

Output of deliver, includes the insertions as well as a suitable LogRequest for forwarding to Metrics API. Field Name | Type | Optional? | Description ---------- | ---- | --------- | ----------- :insertion | [] of Insertion | No | The paged insertions, which are from Delivery API (when deliver was called, i.e. we weren't either only-log or part of an experiment) or the input insertions (when the other conditions don't hold). :log_request | LogRequest | Yes | A message suitable for logging to Metrics API via send_log_request. If the call to deliver was made (i.e. the request was not part of the CONTROL arm of an experiment or marked to only log), :log_request will not be set, as you can assume logging was performed on the server-side by Promoted. :client_request_id | String | Yes | Client-generated request id sent to Delivery API and may be useful for logging and debugging.

:execution_server | one of 'API' or 'SDK' | Yes | Indicates if response insertions on a delivery request came from the API or the SDK.

PromotedClient

Method Input Output Description
send_log_request LogRequest n/a Forwards a LogRequest to Promoted using an HTTP client.
deliver DeliveryRequest ClientResponse Depending on flags, either (1) makes a request (subject to experimentation) to Delivery API for insertions, which are then returned along with a LogRequest or (2) implements SDK-side paging and prepares log records that can be logged to Promoted using send_log_request or by using this structure to make the call yourself. Optionally, based on client configuration may send a random subset of requests to Delivery API as shadow traffic for integration purposes.
close n/a n/a Closes down the client at shutdown, currently this is just to drain the thread pool that handles shadow traffic.

Metrics API

Expected flow for Metrics logging

# Retrieve a list of content (i.e. products)
# products = fetch_my_marketplace_products()

products = [
  {
    id: "123",
    type: "SHOE",
    name: "Blue shoe",
    total_sales: 1000
  },
  {
    id: "124",
    type: "SHIRT",
    name: "Green shirt",
    total_sales: 800
  },
  {
    id: "125",
    type: "DRESS",
    name: "Red dress",
    total_sales: 1200
  }
]

# Transform them into an [] of Insertions.
insertions = products.map { |product| 
  {
    :content_id => product[:id],
    :properties => {
      :struct => {
        :type => product[:type],
        :name => product[:name]
        # etc
      }
    }
  }
}

# Form a MetricsRequest
metrics_request = {
  :request => {
    :user_info => { :user_id => "912", :anon_user_id => "912191"},
    :use_case => "FEED",
    :paging => {
      :offset => 0,
      :size => 5
    },
    :properties => {
      :struct => {
        :active => true
      }
    },
    :insertion => insertions
  },
  :only_log => true,
}

# Create a client
client = Promoted::Ruby::Client::PromotedClient.new

# Build a log request
client_response = client.deliver(metrics_request)

# Log (assuming you have configured your client with a :metrics_endpoint)
client.send_log_request(client_response[:log_request]) if client_response[:log_request]

Delivery API

Expected flow for Delivery

# (continuing from the above example for Metrics)

# Form a DeliveryRequest
delivery_request = {
  :request => {
    :user_info => { :user_id => "912", :anon_user_id => "912191"},
    :use_case => "FEED",
    :paging => {
      :offset => 0,
      :size => 5
    },
    :properties => {
      :struct => {
        :active => true
      }
    },
    :insertion => insertions,
  },
  :only_log => false
}

# Request insertions from Delivery API
client_response = client.deliver(delivery_request)

# Use the resulting insertions
client_response[:insertion]

# Log if a log request was provided (if not, deliver was called successfully
# and Promoted logged on the server-side).)
client.send_log_request(client_response[:log_request]) if client_response[:log_request]

TODO Experimentation example