Class: Promoted::Ruby::Client::PromotedClient

Inherits:
Object
  • Object
show all
Defined in:
lib/promoted/ruby/client.rb

Overview

Client for working with Promoted’s Metrics and Delivery APIs. See Github for more info.

Defined Under Namespace

Classes: Error

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(params = {}) ⇒ PromotedClient

Create and configure a new Promoted client.

Raises:

  • (ArgumentError)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/promoted/ruby/client.rb', line 36

def initialize(params={})
  @perform_checks = true
  if params[:perform_checks] != nil
    @perform_checks = params[:perform_checks]
  end

  @logger               = params[:logger] # Example:  Logger.new(STDERR, :progname => "promotedai")
  @request_logging_on   = params[:request_logging_on] || false

  @default_request_headers = params[:default_request_headers] || {}
  @metrics_api_key = params[:metrics_api_key] || ''
  @delivery_api_key = params[:delivery_api_key] || ''

  @default_only_log        = params[:default_only_log] || false
  @should_apply_treatment_func  = params[:should_apply_treatment_func]
  
  @shadow_traffic_delivery_percent = params[:shadow_traffic_delivery_percent] || 0.0
  raise ArgumentError.new("Invalid shadow_traffic_delivery_percent, must be between 0 and 1") if @shadow_traffic_delivery_percent < 0 || @shadow_traffic_delivery_percent > 1.0

  @sampler = Sampler.new
  @pager   = Pager.new

  # HTTP Client creation
  @delivery_endpoint = params[:delivery_endpoint] || DEFAULT_DELIVERY_ENDPOINT
  raise ArgumentError.new("delivery_endpoint is required") if @delivery_endpoint.strip.empty?

  @metrics_endpoint = params[:metrics_endpoint] || DEFAULT_METRICS_ENDPOINT
  raise ArgumentError.new("metrics_endpoint is required") if @metrics_endpoint.strip.empty?

  @delivery_timeout_millis = params[:delivery_timeout_millis] || DEFAULT_DELIVERY_TIMEOUT_MILLIS
  @metrics_timeout_millis  = params[:metrics_timeout_millis] || DEFAULT_METRICS_TIMEOUT_MILLIS

  @http_client = FaradayHTTPClient.new(@logger)
  @validator = Promoted::Ruby::Client::Validator.new

  @async_shadow_traffic = true
  if params[:async_shadow_traffic] != nil
    @async_shadow_traffic = params[:async_shadow_traffic] || false
  end

  @send_shadow_traffic_for_control = true
  if params[:send_shadow_traffic_for_control] != nil
    @send_shadow_traffic_for_control = params[:send_shadow_traffic_for_control] || false
  end

  @max_request_insertions = params[:max_request_insertions] || DEFAULT_MAX_REQUEST_INSERTIONS

  @pool = nil
  if @async_shadow_traffic
    # Thread pool to process delivery of shadow traffic. Will silently drop excess requests beyond the queue
    # size, and silently eat errors on the background threads.
    @pool = Concurrent::ThreadPoolExecutor.new(
      min_threads: 0,
      max_threads: 10,
      max_queue: 100,
      fallback_policy: :discard
    )
  end

  @enabled = true
  if params[:enabled] != nil
    @enabled = params[:enabled] || false
  end

  if params[:warmup]
    do_warmup
  end
end

Instance Attribute Details

#async_shadow_trafficObject (readonly)

Returns the value of attribute async_shadow_traffic.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def async_shadow_traffic
  @async_shadow_traffic
end

#default_only_logObject (readonly)

Returns the value of attribute default_only_log.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def default_only_log
  @default_only_log
end

#default_request_headersObject (readonly)

Returns the value of attribute default_request_headers.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def default_request_headers
  @default_request_headers
end

#delivery_timeout_millisObject (readonly)

Returns the value of attribute delivery_timeout_millis.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def delivery_timeout_millis
  @delivery_timeout_millis
end

#enabledObject

Returns the value of attribute enabled.



26
27
28
# File 'lib/promoted/ruby/client.rb', line 26

def enabled
  @enabled
end

#http_clientObject (readonly)

Returns the value of attribute http_client.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def http_client
  @http_client
end

#loggerObject (readonly)

Returns the value of attribute logger.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def logger
  @logger
end

#max_request_insertionsObject (readonly)

Returns the value of attribute max_request_insertions.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def max_request_insertions
  @max_request_insertions
end

#metrics_timeout_millisObject (readonly)

Returns the value of attribute metrics_timeout_millis.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def metrics_timeout_millis
  @metrics_timeout_millis
end

#perform_checksObject (readonly)

Returns the value of attribute perform_checks.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def perform_checks
  @perform_checks
end

#request_logging_onObject

Returns the value of attribute request_logging_on.



26
27
28
# File 'lib/promoted/ruby/client.rb', line 26

def request_logging_on
  @request_logging_on
end

#send_shadow_traffic_for_controlObject (readonly)

Returns the value of attribute send_shadow_traffic_for_control.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def send_shadow_traffic_for_control
  @send_shadow_traffic_for_control
end

#shadow_traffic_delivery_percentObject (readonly)

Returns the value of attribute shadow_traffic_delivery_percent.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def shadow_traffic_delivery_percent
  @shadow_traffic_delivery_percent
end

#should_apply_treatment_funcObject (readonly)

Returns the value of attribute should_apply_treatment_func.



22
23
24
# File 'lib/promoted/ruby/client.rb', line 22

def should_apply_treatment_func
  @should_apply_treatment_func
end

Instance Method Details

#closeObject

Politely shut down a Promoted client.



107
108
109
110
111
112
# File 'lib/promoted/ruby/client.rb', line 107

def close
  if @pool
    @pool.shutdown
    @pool.wait_for_termination
  end
end

#deliver(args, headers = {}) ⇒ Object

Make a delivery request. If @perform_checks is set, input validation will occur and possibly raise errors.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/promoted/ruby/client.rb', line 116

def deliver args, headers={}
  args = Promoted::Ruby::Client::Util.translate_hash(args)
  retrieval_insertion_offset = args[:retrieval_insertion_offset] || 0

  # Respect the enabled state
  if !@enabled
    return {
      insertion: @pager.apply_paging(args[:request][:insertion], retrieval_insertion_offset, args[:request][:paging])
      # No log request returned when disabled
    }
  end
  
  delivery_request_builder = RequestBuilder.new
  delivery_request_builder.set_request_params(args)

  only_log = delivery_request_builder.only_log != nil ? delivery_request_builder.only_log : @default_only_log

  # Gets modified depending on the call.
  should_send_shadow_traffic = @shadow_traffic_delivery_percent > 0
  # perform_checks raises errors.
  if @perform_checks
    perform_common_checks!(args)
  end

  delivery_request_builder.ensure_client_timestamp
  
  response_insertions = []
  cohort_membership_to_log = nil
  insertions_from_delivery = false

  deliver_err = false

  # Trim any request insertions over the maximum allowed.
  if delivery_request_builder.insertion.length > @max_request_insertions then
    @logger.warn("Exceeded max request insertions, trimming") if @logger
    delivery_request_builder.insertion = delivery_request_builder.insertion[0, @max_request_insertions]
  end

  begin
    @pager.validate_paging(delivery_request_builder.insertion, retrieval_insertion_offset, delivery_request_builder.request[:paging])
  rescue InvalidPagingError => err
    # Invalid input, log and do SDK-side delivery.
    @logger.warn(err) if @logger
    return {
      insertion: err.default_insertions_page
      # No log request returned when no response insertions due to invalid paging
    }
  end

  if !only_log
    cohort_membership_to_log = delivery_request_builder.new_cohort_membership_to_log

    if should_apply_treatment(cohort_membership_to_log)
      # Call Delivery API to get insertions to use
      delivery_request_params = delivery_request_builder.delivery_request_params  
      # Don't send shadow traffic if we've already tried normal traffic.
      should_send_shadow_traffic = false
      begin
        response = send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, @delivery_api_key, headers)
        @validator.validate_response!(response)
        raise ValidationError.new("Response shoul be a Hash") if !response.is_a?(Hash)
        response_insertions = response && response[:insertion] || []
        insertions_from_delivery = (response != nil && !deliver_err);
      rescue  StandardError => err
        # Currently we don't propagate errors to the SDK caller, but rather default to returning
        # the request insertions.
        deliver_err = true
        @logger.error("Error calling delivery: " + err.message) if @logger
      end
    else
      should_send_shadow_traffic &&= @send_shadow_traffic_for_control
    end
  end

  should_send_shadow_traffic &&= should_send_as_shadow_traffic?
  if should_send_shadow_traffic then
      # Call Delivery API to send shadow traffic. This will create the request params with traffic type set.
      deliver_shadow_traffic args, headers
  end

  if !insertions_from_delivery then
    response_insertions = build_sdk_response_insertions(delivery_request_builder, retrieval_insertion_offset)
  end

  log_req = nil
  exec_server = (insertions_from_delivery ? Promoted::Ruby::Client::EXECUTION_SERVER['API'] : Promoted::Ruby::Client::EXECUTION_SERVER['SDK'])

  # We only return a log request if there's a request or cohort to log.
  if !insertions_from_delivery || cohort_membership_to_log
    log_request_builder = LogRequestBuilder.new
    # TODO - make this more efficient.
    log_request_builder.request = delivery_request_builder.delivery_request_params
    log_request_builder.response_insertions = response_insertions
    log_request_builder.experiment = cohort_membership_to_log

    # On a successful delivery request, we don't log the insertions
    # or the request since they are logged on the server-side.
    log_req = log_request_builder.log_request(
      include_delivery_log: !insertions_from_delivery, 
      exec_server: exec_server)
  end

  client_response = {
    insertion: response_insertions,
    log_request: log_req,
    execution_server: exec_server,
    client_request_id: delivery_request_builder.client_request_id
  }
  return client_response
end

#enabled?Boolean

Whether or not the client is currently enabled for execution.

Returns:

  • (Boolean)


30
31
32
# File 'lib/promoted/ruby/client.rb', line 30

def enabled?
  @enabled
end

#send_log_request(log_request_params, headers = {}) ⇒ Object

Sends a log request to the metrics endpoint.



229
230
231
232
233
234
235
236
# File 'lib/promoted/ruby/client.rb', line 229

def send_log_request log_request_params, headers={}
  begin
    send_request(log_request_params, @metrics_endpoint, @metrics_timeout_millis, @metrics_api_key, headers)
  rescue  StandardError => err
    # Currently we don't propagate errors to the SDK caller.
    @logger.error("Error from metrics: " + err.message) if @logger
  end
end