promoted-ruby-client 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c6ac96f572587bcc605eb0bd622d0238e3fa2fe7ee55f479afdb913320a5e10
4
- data.tar.gz: d89fc05b6e9701034bfb020fda1a6779e1d3cce4352115d89a3eaaba258dee3a
3
+ metadata.gz: 121200a7e00beda3ecf9643712df3aab9794f95719f9f8d65903b9484bf9857d
4
+ data.tar.gz: 901e657b2391b76d53d1b50d743469033f50f3679c1919d48c5644621e8c7866
5
5
  SHA512:
6
- metadata.gz: 13e6c6245b1b4a277309ab8c18e1797ea324e8569292fdf0727af313f4266f770c19c84218c18cceea05c8ee44cd11926bafa8c50ec3e1f7bc9df77aafc68477
7
- data.tar.gz: f206b67e03cfe7221858606f270185ceb90c690b29b04eb17ca6385a1d935de38bad6a7c6fe605a35fd871a95f55c7259d2dcd3af554bcfee46db0d0cfccd18f
6
+ metadata.gz: 54fd3b49e89e049fe39c45afaa10171fc38cf24df7e402e46f922e22e0b12ed6f570f48b0f273b14a7c6a731b98c0d17493ddc4c4636b30582e8a115b0323b92
7
+ data.tar.gz: 195b628e077cbb154423aa8156fb16410582885f72ce292ea444c25abc1ca523656bdb65f5632088d593f4579a06f78cbfcd19a17da813095bdd144ad4b91ad9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- promoted-ruby-client (0.1.3)
4
+ promoted-ruby-client (0.1.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -2,51 +2,166 @@
2
2
 
3
3
  Ruby client designed for calling Promoted's Delivery and Metrics API.
4
4
 
5
- This version of the library only supports preparing objects for logging. TODO - support Delivery API.
5
+ More information at [http://www.promoted.ai](http://www.promoted.ai)
6
6
 
7
- ## Expected pseudo-code flow for Metrics logging
7
+ ## Installation
8
+ ```gem 'promoted-ruby-client'```
8
9
 
9
- This example is for the integration where we do not want to modify the list of items to include `insertionId`. TODO - add this example too.
10
+ ## Local Development
11
+ 1. Clone or fork the repo on your local machine
12
+ 2. `cd promoted-ruby-client`
13
+ 3. `bundle`
14
+ 4. To test interactively: `irb -Ilib -rpromoted/ruby/client`
10
15
 
16
+ ## Dependencies
17
+
18
+ ### [Faraday](https://github.com/lostisland/faraday)
19
+ HTTP client for calling Promoted.
20
+ ### [Concurrent Ruby](https://github.com/ruby-concurrency/concurrent-ruby)
21
+ Provides a thread pool for making shadow traffic requests to Delivery API in the background on a subset of calls to ```prepare_for_logging```
22
+ ## Creating a Client
23
+ ```rb
24
+ client = Promoted::Ruby::Client::PromotedClient.new
11
25
  ```
12
- def get_items args
13
- items = retrieve_items((args))
14
- async_log_request(items)
15
- return items
16
- end
17
26
 
18
- # Done async
19
- def log_request items
20
- client = Promoted::Ruby::Client::PromotedClient.new
21
- log_request = client.prepare_for_logging(input)
22
- # Send JSON to Metrics API.
23
- log_to_promoted_event_api(log_request)
24
- end
27
+ This client will suffice for building log requests. To send actually send traffing to the API, some configuration is required.
28
+
29
+ ```rb
30
+ client = Promoted::Ruby::Client::PromotedClient.new({
31
+ :metrics_endpoint = "https://<get this from Promoted>",
32
+ :delivery_endpoint = "https://<get this from Promoted>",
33
+ :metrics_api_key = "<get this from Promoted>",
34
+ :delivery_api_key = "<get this from Promoted>"
35
+ })
25
36
  ```
26
37
 
27
- ## Naming details
38
+ ### Client Configuration Parameters
39
+ Name | Type | Description
40
+ ---- | ---- | -----------
41
+ ```:delivery_endpoint``` | String | POST URL for the Promoted Delivery API (get this from Promoted)
42
+ ```:metrics_endpoint``` | String | POST URL for the Promoted Metrics API (get this from Promoted)
43
+ ```:metrics_api_key``` | String | Used as the ```x-api-key``` header on Metrics API requests to Promoted (get this value from Promoted)
44
+ ```:delivery_api_key``` | String | Used as the ```x-api-key``` header on Delivery API requests to Promoted (get this value from Promoted)
45
+ ```:delivery_timeout_millis``` | Number | Timeout on the Delivery API call. Defaults to 3000.
46
+ ```:metrics_timeout_millis``` | Number | Timeout on the Metrics API call. Defaults to 3000.
47
+ ```:perform_checks``` | Boolean | Whether or not to perform detailed input validation, defaults to true but may be disabled for performance
48
+ ```:logger``` | Ruby Logger-compatible logger | Defaults to nil (no logging). Example: ```Logger.new(STDERR, :progname => 'promotedai')```
49
+ ```: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).
50
+ ```:default_request_headers``` | Hash | Additional headers to send on the request beyond ```x-api-key```. Defaults to {}
51
+ ```: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.
52
+ ```: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.
28
53
 
29
- `full_insertion` - for `prepare_for_logging`, this is the current page of `Insertion`s with full `Insertion.properties` filled with the item details. For v1 integrations, it is fine to not fill in the full properties.
54
+ ## Data Types
30
55
 
31
- ## Pagination
56
+ ### UserInfo
57
+ Basic information about the request user.
58
+ Field Name | Type | Optional? | Description
59
+ ---------- | ---- | --------- | -----------
60
+ ```:user_id``` | String | Yes | The platform user id, cleared from Promoted logs.
61
+ ```: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.
32
62
 
33
- 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.
63
+ ---
64
+ ### CohortMembership
65
+ Useful fields for experimentation during the delivery phase.
66
+ Field Name | Type | Optional? | Description
67
+ ---------- | ---- | --------- | -----------
68
+ ```:user_info``` | UserInfo | Yes | The user info structure.
69
+ ```:arm``` | String | Yes | 'CONTROL' or one of the TREATMENT values (see [constants.rb](https://github.com/promotedai/promoted-ruby-client/blob/main/lib/promoted/ruby/client/constants.rb)).
70
+ ---
71
+ ### Properties
72
+ Properties bag. Has the structure:
34
73
 
35
- ## Example to run the client
74
+ ```rb
75
+ :struct => {
76
+ :product => {
77
+ "id": "product3",
78
+ "title": "Product 3",
79
+ "url": "www.mymarket.com/p/3"
80
+ # other key-value pairs...
81
+ }
82
+ }
83
+ ```
84
+ ---
85
+ ### Insertion
86
+ Content being served at a certain position.
87
+ Field Name | Type | Optional? | Description
88
+ ---------- | ---- | --------- | -----------
89
+ ```:user_info``` | UserInfo | Yes | The user info structure.
90
+ ```:insertion_id``` | String | Yes | Generated by the SDK (*do not set*)
91
+ ```:request_id``` | String | Yes | Generated by the SDK (*do not set*)
92
+ ```:content_id``` | String | No | Identifier for the content to be shown, must be set.
93
+ ```:properties``` | Properties | Yes | Any additional custom properties to associate. For v1 integrations, it is fine not to fill in all the properties.
36
94
 
37
- Install our Ruby client.
38
- `promoted-ruby-client (0.1.3)`
95
+ ---
96
+ ### Paging
97
+ #### TODO
98
+ ---
99
+ ### Request
100
+ A request for content insertions.
101
+ Field Name | Type | Optional? | Description
102
+ ---------- | ---- | --------- | -----------
103
+ ```:user_info``` | UserInfo | Yes | The user info structure.
104
+ ```:request_id``` | String | Yes | Generated by the SDK (*do not set*)
105
+ ```:use_case``` | String | Yes | One of the use case values, i.e. 'FEED' (see [constants.rb](https://github.com/promotedai/promoted-ruby-client/blob/main/lib/promoted/ruby/client/constants.rb)).
106
+ ```:properties``` | Properties | Yes | Any additional custom properties to associate.
107
+ ```:paging``` | Paging | Yes | Paging parameters (see TODO)
108
+ ---
109
+ ### MetricsRequest
110
+ Input to ```prepare_for_logging```
111
+ Field Name | Type | Optional? | Description
112
+ ---------- | ---- | --------- | -----------
113
+ ```:request``` | Request | No | The underlying request for content.
114
+ ```:full_insertion``` | [] of Insertion | No | The proposed list of insertions.
115
+ ---
39
116
 
40
- Or
117
+ ### DeliveryRequest
118
+ Input to ```deliver```
119
+ Field Name | Type | Optional? | Description
120
+ ---------- | ---- | --------- | -----------
121
+ ```:experiment``` | CohortMembership | Yes | A cohort to evaluation in experimentation.
122
+ ```:request``` | Request | No | The underlying request for content.
123
+ ```:full_insertion``` | [] of Insertion | No | The proposed list of insertions with all metadata, will be compacted before forwarding to Promoted.
124
+ ```:only_log``` | Boolean | Yes | Defaults to false. Set to true to override whether Delivery API is called for this request.
125
+ ---
41
126
 
42
- 1. Clone the repo on your local machine
43
- 2. `cd promoted-ruby-client`
44
- 3. `bundle`
45
- 4. `irb -Ilib -rpromoted/ruby/client`
127
+ ### LogRequest
46
128
 
47
- A console will launch with the library loaded. Here is example code to use.
129
+ 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
130
+ Field Name | Type | Optional? | Description
131
+ ---------- | ---- | --------- | -----------
132
+ ```:request``` | Request | No | The underlying request for content to log.
133
+ ```: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.
134
+ ---
135
+
136
+ ### ClientResponse
137
+ Output of ```deliver```, includes the insertions as well as a suitable ```LogRequest``` for forwarding to Metrics API.
138
+ Field Name | Type | Optional? | Description
139
+ ---------- | ---- | --------- | -----------
140
+ ```: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).
141
+ ```: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.
142
+ ---
143
+
144
+ ### PromotedClient
145
+ Method | Input | Output | Description
146
+ ------ | ----- | ------ | -----------
147
+ ```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.
148
+ ```send_log_request``` | LogRequest | n/a | Forwards a LogRequest to Promoted using an HTTP client.
149
+ ```deliver``` | DeliveryRequest | ClientResponse | Makes a request (subject to experimentation) to Delivery API for insertions, which are then returned along with a LogRequest.
150
+ ```close``` | n/a | n/a | Closes down the client at shutdown, currently this is just to drain the thread pool that handles shadow traffic.
151
+ ---
152
+
153
+ ## Metrics API
154
+ ### Pagination
155
+
156
+ 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.
157
+ TODO: Needs more details.
158
+
159
+ ### Expected flow for Metrics logging
160
+
161
+ ```rb
162
+ # Retrieve a list of content (i.e. products)
163
+ # products = fetch_my_marketplace_products()
48
164
 
49
- ```
50
165
  products = [
51
166
  {
52
167
  id: "123",
@@ -68,170 +183,91 @@ products = [
68
183
  }
69
184
  ]
70
185
 
71
- # Converts the Products to a list of Insertions.
72
- def to_insertions products
73
- @to_insertions = []
74
- products.each_with_index do |product, index|
75
- @to_insertions << {
76
- content_id: product[:id],
77
- properties: {
78
- struct: {
79
- product: product.reject { |k, v| [:id].include? k }
80
- }
186
+ # Transform them into an [] of Insertions.
187
+ insertions = products.map { |product|
188
+ {
189
+ :content_id => product[:id],
190
+ :properties => {
191
+ :struct => {
192
+ :type => product[:type],
193
+ :name => product[:name]
194
+ # etc
81
195
  }
82
196
  }
83
- end
84
- @to_insertions
85
- end
197
+ }
198
+ }
86
199
 
87
- request_input = {
88
- request: {
89
- user_info: { user_id: "912", log_user_id: "912191"},
90
- use_case: "FEED",
91
- paging: {
92
- offset: 10,
93
- size: 5
200
+ # Form a MetricsRequest
201
+ metrics_request = {
202
+ :request => {
203
+ :user_info => { :user_id => "912", :log_user_id => "912191"},
204
+ :use_case => "FEED",
205
+ :paging => {
206
+ :offset => 0,
207
+ :size => 5
94
208
  },
95
- properties: {
96
- struct: {
97
- active: true
209
+ :properties => {
210
+ :struct => {
211
+ :active => true
98
212
  }
99
213
  }
100
214
  },
101
- full_insertion: to_insertions(products)
215
+ :full_insertion => insertions
102
216
  }
103
217
 
104
- client = Promoted::Ruby::Client::PromotedClient.new
105
- log_request = client.prepare_for_logging(request_input)
106
- log_request.to_json
107
- ```
108
-
109
- ## Pass a custom function for compact insertion
110
- ```
111
- # define a function using proc and it should use one argument.
112
- # Example
218
+ # OPTIONAL: You can pass a custom function to "compact" insertions before metrics logging.
219
+ # Note that the PromotedClient has a class method helper, copy_and_remove_properties, that does just this.
113
220
  to_compact_metrics_insertion_func = Proc.new do |insertion|
114
221
  insertion.delete(:properties)
115
222
  insertion
116
223
  end
224
+ # metrics_request[:to_compact_metrics_insertion_func] = to_compact_metrics_insertion
117
225
 
118
- request_input = {
119
- to_compact_metrics_insertion_func: to_compact_metrics_insertion_func,
120
- request: {
121
- user_info: { user_id: "912", log_user_id: "912191"},
122
- use_case: "FEED",
123
- paging: {
124
- from: 10,
125
- size: 5
126
- },
127
- properties: {
128
- struct: {
129
- active: true
130
- }
131
- }
132
- },
133
- full_insertion: to_insertions(products)
134
- }
135
-
226
+ # Create a client
136
227
  client = Promoted::Ruby::Client::PromotedClient.new
137
- log_request = client.prepare_for_logging(request_input)
138
- log_request.to_json
139
- ```
140
228
 
141
- `log_request.to_json` returns a result that looks like the following
142
- ```
143
- => "{\"user_info\":{\"user_id\":\"912\",\"log_user_id\":\"912191\"},\"timing\":{\"client_log_timestamp\":1623306198},\"request\":[{\"user_info\":{\"user_id\":\"912\",\"log_user_id\":\"912191\"},\"use_case\":\"FEED\",\"paging\":{\"offset\":10,\"size\":10},\"properties\":{\"struct\":{\"active\":true}}}],\"insertion\":[{\"content_id\":\"123\",\"properties\":{\"struct\":{\"product\":{\"type\":\"SHOE\",\"name\":\"Blue shoe\",\"total_sales\":1000}}},\"user_info\":{\"user_id\":\"912\",\"log_user_id\":\"912191\"},\"timing\":{\"client_log_timestamp\":1623306198},\"insertion_id\":\"a87e1b57-a574-424f-8af6-10e0250aa7ab\",\"request_id\":\"54ff4884-2192-4180-8c72-a805a436980f\",\"position\":10},{\"content_id\":\"124\",\"properties\":{\"struct\":{\"product\":{\"type\":\"SHIRT\",\"name\":\"Green shirt\",\"total_sales\":800}}},\"user_info\":{\"user_id\":\"912\",\"log_user_id\":\"912191\"},\"timing\":{\"client_log_timestamp\":1623306198},\"insertion_id\":\"4495f72a-8101-4cb8-94ce-4db76839b8b6\",\"request_id\":\"54ff4884-2192-4180-8c72-a805a436980f\",\"position\":11},{\"content_id\":\"125\",\"properties\":{\"struct\":{\"product\":{\"type\":\"DRESS\",\"name\":\"Red dress\",\"total_sales\":1200}}},\"user_info\":{\"user_id\":\"912\",\"log_user_id\":\"912191\"},\"timing\":{\"client_log_timestamp\":1623306198},\"insertion_id\":\"d1e4f3f6-1783-4059-8fab-fdf2ba343cdf\",\"request_id\":\"54ff4884-2192-4180-8c72-a805a436980f\",\"position\":12}]}"
229
+ # Build a log request
230
+ log_request = client.prepare_for_logging(metrics_request)
231
+
232
+ # Log (assuming you have configured your client with a :metrics_endpoint)
233
+ client.send_log_request(log_request)
144
234
  ```
145
235
 
146
- ## Other input syntaxes
236
+ ## Delivery API
147
237
 
148
- The client should also work if Hash rocket too.
149
- ```
150
- products = [
151
- {
152
- "id"=>"123",
153
- "type"=>"SHOE",
154
- "name"=>"Blue shoe",
155
- "totalSales"=>1000
156
- },
157
- {
158
- "id"=>"124",
159
- "type"=>"SHIRT",
160
- "name"=>"Green shirt",
161
- "totalSales"=>800
162
- },
163
- {
164
- "id"=>"125",
165
- "type"=>"DRESS",
166
- "name"=>"Red dress",
167
- "totalSales"=>1200
168
- }
169
- ]
238
+ ### Expected flow for Delivery
170
239
 
171
- input = {
172
- "request"=>{
173
- "user_info"=>{"user_id"=> "912", "log_user_id"=> "912191"},
174
- "use_case"=>"FEED",
175
- "properties"=>{
176
- "struct"=>{
177
- "active"=>true
178
- }
179
- }
180
- },
181
- "full_insertion"=>to_insertions(products)
182
- }
183
- ```
240
+ ```rb
241
+ # (continuing from the above example for Metrics)
184
242
 
185
- Or inlined full request.
186
- ```
187
- input = {
188
- "request"=>{
189
- "user_info"=>{"user_id"=> "912", "log_user_id"=> "912191"},
190
- "use_case"=>"FEED",
191
- "properties"=>{
192
- "struct"=>{
193
- "active"=>true
194
- }
195
- }
196
- },
197
- "full_insertion"=>[
198
- {
199
- "contentId"=>"123",
200
- "properties"=>{
201
- "struct"=>{
202
- "product"=>{
203
- "type"=>"SHOE",
204
- "name"=>"Blue shoe",
205
- "totalSales"=>1000
206
- }
207
- }
208
- }
209
- },
210
- {
211
- "contentId"=>"124",
212
- "properties"=>{
213
- "struct"=>{
214
- "product"=>{
215
- "type"=>"SHIRT",
216
- "name"=>"Green shirt",
217
- "totalSales"=>800
218
- }
219
- }
220
- }
243
+ # Form a DeliveryRequest
244
+ delivery_request = {
245
+ :request => {
246
+ :user_info => { :user_id => "912", :log_user_id => "912191"},
247
+ :use_case => "FEED",
248
+ :paging => {
249
+ :offset => 0,
250
+ :size => 5
221
251
  },
222
- {
223
- "contentId"=>"125",
224
- "properties"=>{
225
- "struct"=>{
226
- "product"=>{
227
- "type"=>"DRESS",
228
- "name"=>"Red dress",
229
- "totalSales"=>1200
230
- }
231
- }
252
+ :properties => {
253
+ :struct => {
254
+ :active => true
232
255
  }
233
256
  }
234
- ]
257
+ },
258
+ :full_insertion => insertions,
259
+ :only_log => false
235
260
  }
261
+
262
+ # Request insertions from Delivery API
263
+ client_response = client.deliver(delivery_request)
264
+
265
+ # Use the resulting insertions
266
+ client_response[:insertion]
267
+
268
+ # Log if a log request was provided (if not, deliver was called successfully
269
+ # and Promoted logged on the server-side).)
270
+ client.send_log_request(client_response[:log_request]) if client_response[:log_request]
236
271
  ```
237
272
 
273
+ ### TODO Experimentation example
data/dev.md CHANGED
@@ -5,5 +5,5 @@
5
5
  2. Get credentials for deployment from 1password.
6
6
  3. Modify `promoted-ruby-client.gemspec`'s push block.
7
7
  4. Run `gem build promoted-ruby-client.gemspec` to generate `gem`.
8
- 5. Run (using new output) `gem push promoted-ruby-client-0.1.3.gem`
8
+ 5. Run (using new output) `gem push promoted-ruby-client-0.1.4.gem`
9
9
  6. Update README with new version.
@@ -6,18 +6,22 @@ module Promoted
6
6
  module Ruby
7
7
  module Client
8
8
 
9
- DEFAULT_DELIVERY_TIMEOUT_MILLIS = 3000
9
+ DEFAULT_DELIVERY_TIMEOUT_MILLIS = 250
10
10
  DEFAULT_METRICS_TIMEOUT_MILLIS = 3000
11
11
  DEFAULT_DELIVERY_ENDPOINT = "http://delivery.example.com"
12
12
  DEFAULT_METRICS_ENDPOINT = "http://metrics.example.com"
13
13
 
14
+ ##
15
+ # Client for working with Promoted's Metrics and Delivery APIs.
16
+ # See {Github}[https://github.com/promotedai/promoted-ruby-client] for more info.
14
17
  class PromotedClient
15
18
 
16
19
  class Error < StandardError; end
17
20
 
18
21
  attr_reader :perform_checks, :default_only_log, :delivery_timeout_millis, :metrics_timeout_millis, :should_apply_treatment_func,
19
- :default_request_headers
22
+ :default_request_headers, :http_client
20
23
 
24
+ ##
21
25
  # A common compact method implementation.
22
26
  def self.copy_and_remove_properties
23
27
  Proc.new do |insertion|
@@ -27,6 +31,8 @@ module Promoted
27
31
  end
28
32
  end
29
33
 
34
+ ##
35
+ # Create and configure a new Promoted client.
30
36
  def initialize (params={})
31
37
  @perform_checks = true
32
38
  if params[:perform_checks] != nil
@@ -36,7 +42,8 @@ module Promoted
36
42
  @logger = params[:logger] # Example: Logger.new(STDERR, :progname => "promotedai")
37
43
 
38
44
  @default_request_headers = params[:default_request_headers] || {}
39
- @default_request_headers['x-api-key'] = params[:api_key] || ''
45
+ @metrics_api_key = params[:metrics_api_key] || ''
46
+ @delivery_api_key = params[:delivery_api_key] || ''
40
47
 
41
48
  @default_only_log = params[:default_only_log] || false
42
49
  @should_apply_treatment_func = params[:should_apply_treatment_func]
@@ -57,31 +64,27 @@ module Promoted
57
64
  @metrics_timeout_millis = params[:metrics_timeout_millis] || DEFAULT_METRICS_TIMEOUT_MILLIS
58
65
 
59
66
  @http_client = FaradayHTTPClient.new
60
- @pool = Concurrent::CachedThreadPool.new
61
67
  @validator = Promoted::Ruby::Client::Validator.new
68
+
69
+ # Thread pool to process delivery of shadow traffic. Will silently drop excess requests beyond the queue
70
+ # size, and silently eat errors on the background threads.
71
+ @pool = Concurrent::ThreadPoolExecutor.new(
72
+ min_threads: 0,
73
+ max_threads: 10,
74
+ max_queue: 100,
75
+ fallback_policy: :discard
76
+ )
62
77
  end
63
78
 
79
+ ##
80
+ # Politely shut down a Promoted client.
64
81
  def close
65
82
  @pool.shutdown
66
83
  @pool.wait_for_termination
67
84
  end
68
85
 
69
- def send_request payload, endpoint, timeout_millis, headers=[], send_async=false
70
- use_headers = @default_request_headers.merge headers
71
-
72
- if send_async
73
- @pool.post do
74
- @http_client.send(endpoint, timeout_millis, payload, use_headers)
75
- end
76
- else
77
- begin
78
- @http_client.send(endpoint, timeout_millis, payload, use_headers)
79
- rescue Faraday::Error => err
80
- raise EndpointError.new(err)
81
- end
82
- end
83
- end
84
-
86
+ ##
87
+ # Make a delivery request.
85
88
  def deliver args, headers={}
86
89
  args = Promoted::Ruby::Client::Util.translate_args(args)
87
90
 
@@ -97,6 +100,7 @@ module Promoted
97
100
  insertions_from_promoted = false
98
101
 
99
102
  only_log = delivery_request_builder.only_log != nil ? delivery_request_builder.only_log : @default_only_log
103
+ deliver_err = false
100
104
  if !only_log
101
105
  cohort_membership_to_log = delivery_request_builder.new_cohort_membership_to_log
102
106
 
@@ -105,22 +109,22 @@ module Promoted
105
109
 
106
110
  # Call Delivery API
107
111
  begin
108
- response = send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, headers)
112
+ response = send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, @delivery_api_key, headers)
109
113
  rescue StandardError => err
110
114
  # Currently we don't propagate errors to the SDK caller, but rather default to returning
111
115
  # the request insertions.
116
+ deliver_err = true
112
117
  @logger.error("Error calling delivery: " + err.message) if @logger
113
118
  end
114
119
 
115
- if response != nil && response[:insertion] != nil
116
- response_insertions = delivery_request_builder.fill_details_from_response(response[:insertion])
117
- insertions_from_promoted = true;
118
- end
120
+ insertions_from_promoted = (response != nil && !deliver_err);
121
+ response_insertions = delivery_request_builder.fill_details_from_response(
122
+ response ? response[:insertion] : nil)
119
123
  end
120
124
  end
121
125
 
122
126
  request_to_log = nil
123
- if !insertions_from_promoted
127
+ if !insertions_from_promoted then
124
128
  request_to_log = delivery_request_builder.request
125
129
  size = delivery_request_builder.request.dig(:paging, :size)
126
130
  response_insertions = size != nil ? delivery_request_builder.full_insertion[0..size] : delivery_request_builder.full_insertion
@@ -163,6 +167,9 @@ module Promoted
163
167
  return client_response
164
168
  end
165
169
 
170
+ ##
171
+ # Generate a log request for a subsequent call to send_log_request
172
+ # or for logging via alternative means.
166
173
  def prepare_for_logging args, headers={}
167
174
  args = Promoted::Ruby::Client::Util.translate_args(args)
168
175
 
@@ -171,27 +178,30 @@ module Promoted
171
178
  # Note: This method expects as JSON (string keys) but internally, RequestBuilder
172
179
  # transforms and works with symbol keys.
173
180
  log_request_builder.set_request_params(args)
181
+ shadow_traffic_err = false
174
182
  if @perform_checks
175
183
  perform_common_checks! args
176
184
 
177
185
  if @shadow_traffic_delivery_percent > 0 && args[:insertion_page_type] != Promoted::Ruby::Client::INSERTION_PAGING_TYPE['UNPAGED'] then
178
- raise ShadowTrafficInsertionPageType
186
+ shadow_traffic_err = true
187
+ @logger.error(ShadowTrafficInsertionPageType.new) if @logger
179
188
  end
180
189
  end
181
190
 
182
191
  pre_delivery_fillin_fields log_request_builder
183
192
 
184
- if should_send_as_shadow_traffic?
193
+ if !shadow_traffic_err && should_send_as_shadow_traffic?
185
194
  deliver_shadow_traffic args, headers
186
195
  end
187
196
 
188
197
  log_request_builder.log_request_params
189
198
  end
190
199
 
200
+ ##
191
201
  # Sends a log request (previously created by a call to prepare_for_logging) to the metrics endpoint.
192
202
  def send_log_request log_request_params, headers={}
193
203
  begin
194
- send_request(log_request_params, @metrics_endpoint, @metrics_timeout_millis, headers)
204
+ send_request(log_request_params, @metrics_endpoint, @metrics_timeout_millis, @metrics_api_key, headers)
195
205
  rescue StandardError => err
196
206
  # Currently we don't propagate errors to the SDK caller.
197
207
  @logger.error("Error from metrics: " + err.message) if @logger
@@ -200,6 +210,24 @@ module Promoted
200
210
 
201
211
  private
202
212
 
213
+ def send_request payload, endpoint, timeout_millis, api_key, headers={}, send_async=false
214
+ headers["x-api-key"] = api_key
215
+ use_headers = @default_request_headers.merge headers
216
+
217
+ if send_async
218
+ @pool.post do
219
+ @http_client.send(endpoint, timeout_millis, payload, use_headers)
220
+ end
221
+ else
222
+ begin
223
+ @http_client.send(endpoint, timeout_millis, payload, use_headers)
224
+ rescue Faraday::Error => err
225
+ raise EndpointError.new(err)
226
+ end
227
+ end
228
+ end
229
+
230
+
203
231
  def add_missing_ids_on_insertions! request, insertions
204
232
  insertions.each do |insertion|
205
233
  insertion[:insertion_id] = SecureRandom.uuid if not insertion[:insertion_id]
@@ -223,7 +251,7 @@ module Promoted
223
251
  delivery_request_params[:client_info][:traffic_type] = Promoted::Ruby::Client::TRAFFIC_TYPE['SHADOW']
224
252
 
225
253
  # Call Delivery API async (fire and forget)
226
- send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, headers, true)
254
+ send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, @delivery_api_key, headers, true)
227
255
  end
228
256
 
229
257
  def perform_common_checks!(req)
@@ -238,7 +266,7 @@ module Promoted
238
266
 
239
267
  def should_apply_treatment(cohort_membership)
240
268
  if @should_apply_treatment_func != nil
241
- @should_apply_treatment_func
269
+ @should_apply_treatment_func.call(cohort_membership)
242
270
  else
243
271
  return true if !cohort_membership
244
272
  return true if !cohort_membership[:arm]
@@ -19,7 +19,7 @@ module Promoted
19
19
  response = @conn.post(endpoint) do |req|
20
20
  req.headers = req.headers.merge(additional_headers) if additional_headers
21
21
  req.headers['Content-Type'] = req.headers['Content-Type'] || 'application/json'
22
- req.options.timeout = timeout_millis / 1000
22
+ req.options.timeout = timeout_millis.to_f / 1000
23
23
  req.body = request.to_json
24
24
  end
25
25
 
@@ -61,6 +61,10 @@ module Promoted
61
61
  # Maps the response insertions to the full insertions and re-insert the properties bag
62
62
  # to the responses.
63
63
  def fill_details_from_response response_insertions
64
+ if !response_insertions then
65
+ response_insertions = full_insertion
66
+ end
67
+
64
68
  props = @full_insertion.each_with_object({}) do |insertion, hash|
65
69
  hash[insertion[:content_id]] = insertion[:properties]
66
70
  end
@@ -1,7 +1,7 @@
1
1
  module Promoted
2
2
  module Ruby
3
3
  module Client
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: promoted-ruby-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - scottmcmaster
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-29 00:00:00.000000000 Z
11
+ date: 2021-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler