promoted-ruby-client

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

More information at www.promoted.ai

Installation

'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 prepare_for_logging

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 prepare_for_logging 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.

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. :log_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.

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 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 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

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

#### TODO

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 (see TODO)

:device | Device | Yes | Device information (as available)

MetricsRequest

Input to prepare_for_logging Field Name | Type | Optional? | Description ———- | —- | ——— | ———– :request | Request | No | The underlying request for content.

:full_insertion | [] of Insertion | No | The proposed list of insertions.

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. :full_insertion | [] of Insertion | No | The proposed list of insertions with all metadata, will be compacted before forwarding to Promoted.

:only_log | Boolean | Yes | Defaults to false. Set to true to override whether Delivery API is called for this request.

LogRequest

Output of prepare_for_logging as well as an ouput of an SDK call to deliver, input to send_log_request to log to Promoted Field Name | Type | Optional? | Description ———- | —- | ——— | ———– :request | Request | No | The underlying request for content to log.

:insertion | [] of Insertion | No | The insertions, which are either the original request insertions or the insertions resulting from a call to deliver if such call occurred.

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 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 —— | —– | —— | ———– prepare_for_logging | MetricsRequest | LogRequest | Builds a request suitable for logging locally and/or to Promoted, either via a subsequent call to send_log_request in the SDK client 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. send_log_request | LogRequest | n/a | Forwards a LogRequest to Promoted using an HTTP client. deliver | DeliveryRequest | ClientResponse | Makes a request (subject to experimentation) to Delivery API for insertions, which are then returned along with a LogRequest.

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

Pagination

The prepare_for_logging call assumes the client has already handled pagination. It needs a Request.paging.offset to be passed in for the number of items deep that the page is. TODO: Needs more details.

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", :log_user_id => "912191"},
    :use_case => "FEED",
    :paging => {
      :offset => 0,
      :size => 5
    },
    :properties => {
      :struct => {
        :active => true
      }
    }
  },
  :full_insertion => insertions
}

# OPTIONAL: You can pass a custom function to "compact" insertions before metrics logging.
# Note that the PromotedClient has a class method helper, remove_all_properties, that does
# an implementation of this, returning nil to remove ALL properties.
to_compact_metrics_properties_func = Proc.new do |properties|
  properties[:struct].delete(:active)
  properties
end
# metrics_request[:to_compact_metrics_properties_func] = to_compact_metrics_properties_func

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

# Build a log request
log_request = client.prepare_for_logging(metrics_request)

# Log (assuming you have configured your client with a :metrics_endpoint)
client.send_log_request(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", :log_user_id => "912191"},
    :use_case => "FEED",
    :paging => {
      :offset => 0,
      :size => 5
    },
    :properties => {
      :struct => {
        :active => true
      }
    }
  },
  :full_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