promoted-ruby-client 0.1.1 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0f6d46482500c45c759f6b2965ae13a76aea2b3b0d18c069e31055fe985146f
4
- data.tar.gz: 7527ed13e8306bc4f9b53fd1add1a12c478c261be30b0b0dc50d16e146f88f10
3
+ metadata.gz: 30ba0e172809a6aa76284b4b88980fa45dea5b2bf28cdda4fbe8e672b7510a83
4
+ data.tar.gz: e28586ed23f7db16b959bb9819072e502fc7af3805e68938f2bbf421fdc9e4d8
5
5
  SHA512:
6
- metadata.gz: fa5105d3d057f9e949dca0fa46a47c5d701e8dabec37f1d64619969827a580994245a6a83b5fbce399be06201f29d8758c2829e1d76b066bee8a4e8ea62fc892
7
- data.tar.gz: c6d29aeb9721ad09de063c21afd529495ff8c7eb3422886010124e85fc654fe711efeeb35d5b1a873f4f0ca4d56c064a119069f698f080a203daf797309ad8a9
6
+ metadata.gz: 4e6b42f73ff30726aa81af1c08b97a0cd0e0307685dec1f90dcce5d0d61d4009c3a2e6179af77a600038531101db187a83dc52f46a7c0ad4a99f7c6c2530b43e
7
+ data.tar.gz: f3d304f0a2aad7acc15229750f97b021da4c0bfe8deae40c6ac1110bc7c1621ab0db19635464ba835e4e9a9ec07b9cfa0c231a9163fa0e1dcffeeebfdc1e51fc
data/Gemfile CHANGED
@@ -9,7 +9,6 @@ gem 'faraday', '~> 1.4.1'
9
9
  gem 'faraday_middleware'
10
10
  gem 'faraday-net_http'
11
11
  gem 'concurrent-ruby', require: 'concurrent'
12
- gem 'byebug'
13
12
 
14
13
  group :development do
15
14
  gem 'ruby-debug-ide', group: :development
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- promoted-ruby-client (0.1.1)
4
+ promoted-ruby-client (0.1.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -9,7 +9,6 @@ GEM
9
9
  ast (2.4.2)
10
10
  backport (1.2.0)
11
11
  benchmark (0.1.1)
12
- byebug (11.1.3)
13
12
  concurrent-ruby (1.1.9)
14
13
  debase (0.2.5.beta2)
15
14
  debase-ruby_core_source (>= 0.10.12)
@@ -111,7 +110,6 @@ PLATFORMS
111
110
 
112
111
  DEPENDENCIES
113
112
  bundler (~> 1.17)
114
- byebug
115
113
  concurrent-ruby
116
114
  debase (>= 0.2.5.beta2)
117
115
  faraday (~> 1.4.1)
@@ -126,4 +124,4 @@ DEPENDENCIES
126
124
  solargraph
127
125
 
128
126
  BUNDLED WITH
129
- 1.17.2
127
+ 1.17.3
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.1)`
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,169 +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
239
+
240
+ ```rb
241
+ # (continuing from the above example for Metrics)
170
242
 
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
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
251
+ },
252
+ :properties => {
253
+ :struct => {
254
+ :active => true
178
255
  }
179
256
  }
180
257
  },
181
- "full_insertion"=>to_insertions(products)
258
+ :full_insertion => insertions,
259
+ :only_log => false
182
260
  }
183
- ```
184
261
 
185
- Or inlined full request.
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]
186
271
  ```
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
- }
221
- },
222
- {
223
- "contentId"=>"125",
224
- "properties"=>{
225
- "struct"=>{
226
- "product"=>{
227
- "type"=>"DRESS",
228
- "name"=>"Red dress",
229
- "totalSales"=>1200
230
- }
231
- }
232
- }
233
- }
234
- ]
235
- }
236
- ```
272
+
273
+ ### TODO Experimentation example
data/dev.md CHANGED
@@ -1,9 +1,8 @@
1
-
2
- ## Deploy
1
+ # Deploy
3
2
 
4
3
  1. Update version number.
5
4
  2. Get credentials for deployment from 1password.
6
5
  3. Modify `promoted-ruby-client.gemspec`'s push block.
7
6
  4. Run `gem build promoted-ruby-client.gemspec` to generate `gem`.
8
- 5. Run (using new output) `gem push promoted-ruby-client-0.1.1.gem`
7
+ 5. Run (using new output) `gem push promoted-ruby-client-0.1.6.gem`
9
8
  6. Update README with new version.
@@ -6,26 +6,44 @@ 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
+ ##
25
+ # A common compact method implementation.
26
+ def self.copy_and_remove_properties
27
+ Proc.new do |insertion|
28
+ insertion = Hash[insertion]
29
+ insertion.delete(:properties)
30
+ insertion
31
+ end
32
+ end
33
+
34
+ ##
35
+ # Create and configure a new Promoted client.
21
36
  def initialize (params={})
22
37
  @perform_checks = true
23
38
  if params[:perform_checks] != nil
24
39
  @perform_checks = params[:perform_checks]
25
40
  end
26
41
 
42
+ @logger = params[:logger] # Example: Logger.new(STDERR, :progname => "promotedai")
43
+
27
44
  @default_request_headers = params[:default_request_headers] || {}
28
- @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] || ''
29
47
 
30
48
  @default_only_log = params[:default_only_log] || false
31
49
  @should_apply_treatment_func = params[:should_apply_treatment_func]
@@ -46,35 +64,34 @@ module Promoted
46
64
  @metrics_timeout_millis = params[:metrics_timeout_millis] || DEFAULT_METRICS_TIMEOUT_MILLIS
47
65
 
48
66
  @http_client = FaradayHTTPClient.new
49
- @pool = Concurrent::CachedThreadPool.new
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
+ )
50
77
  end
51
78
 
79
+ ##
80
+ # Politely shut down a Promoted client.
52
81
  def close
53
82
  @pool.shutdown
54
83
  @pool.wait_for_termination
55
84
  end
56
85
 
57
- def send_request payload, endpoint, timeout_millis, headers=[], send_async=false
58
- use_headers = @default_request_headers.merge headers
59
-
60
- if send_async
61
- @pool.post do
62
- @http_client.send(endpoint, timeout_millis, payload, use_headers)
63
- end
64
- else
65
- @http_client.send(endpoint, timeout_millis, payload, use_headers)
66
- end
67
- end
68
-
86
+ ##
87
+ # Make a delivery request.
69
88
  def deliver args, headers={}
70
89
  args = Promoted::Ruby::Client::Util.translate_args(args)
71
90
 
72
91
  delivery_request_builder = RequestBuilder.new
73
92
  delivery_request_builder.set_request_params(args)
74
93
 
75
- if perform_checks?
76
- Promoted::Ruby::Client::Settings.check_that_log_ids_not_set!(args)
77
- end
94
+ perform_common_checks!(args) if @perform_checks
78
95
 
79
96
  pre_delivery_fillin_fields delivery_request_builder
80
97
 
@@ -83,22 +100,31 @@ module Promoted
83
100
  insertions_from_promoted = false
84
101
 
85
102
  only_log = delivery_request_builder.only_log != nil ? delivery_request_builder.only_log : @default_only_log
103
+ deliver_err = false
86
104
  if !only_log
87
105
  cohort_membership_to_log = delivery_request_builder.new_cohort_membership_to_log
88
- end
106
+
107
+ if should_apply_treatment(cohort_membership_to_log)
108
+ delivery_request_params = delivery_request_builder.delivery_request_params
89
109
 
90
- if should_apply_treatment(cohort_membership_to_log)
91
- delivery_request_params = delivery_request_builder.delivery_request_params
92
-
93
- # Call Delivery API
94
- response = send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, headers)
95
-
96
- response_insertions = delivery_request_builder.fill_details_from_response(response[:insertion])
97
- insertions_from_promoted = true;
110
+ # Call Delivery API
111
+ begin
112
+ response = send_request(delivery_request_params, @delivery_endpoint, @delivery_timeout_millis, @delivery_api_key, headers)
113
+ rescue StandardError => err
114
+ # Currently we don't propagate errors to the SDK caller, but rather default to returning
115
+ # the request insertions.
116
+ deliver_err = true
117
+ @logger.error("Error calling delivery: " + err.message) if @logger
118
+ end
119
+
120
+ insertions_from_promoted = (response != nil && !deliver_err);
121
+ response_insertions = delivery_request_builder.fill_details_from_response(
122
+ response ? response[:insertion] : [])
123
+ end
98
124
  end
99
125
 
100
126
  request_to_log = nil
101
- if !insertions_from_promoted
127
+ if !insertions_from_promoted then
102
128
  request_to_log = delivery_request_builder.request
103
129
  size = delivery_request_builder.request.dig(:paging, :size)
104
130
  response_insertions = size != nil ? delivery_request_builder.full_insertion[0..size] : delivery_request_builder.full_insertion
@@ -141,21 +167,73 @@ module Promoted
141
167
  return client_response
142
168
  end
143
169
 
144
- def add_missing_ids_on_insertions! request, insertions
145
- insertions.each do |insertion|
146
- insertion[:insertion_id] = SecureRandom.uuid if not insertion[:insertion_id]
147
- insertion[:session_id] = request[:session_id] if request[:session_id]
148
- insertion[:request_id] = request[:request_id] if request[:request_id]
170
+ ##
171
+ # Generate a log request for a subsequent call to send_log_request
172
+ # or for logging via alternative means.
173
+ def prepare_for_logging args, headers={}
174
+ args = Promoted::Ruby::Client::Util.translate_args(args)
175
+
176
+ log_request_builder = RequestBuilder.new
177
+
178
+ # Note: This method expects as JSON (string keys) but internally, RequestBuilder
179
+ # transforms and works with symbol keys.
180
+ log_request_builder.set_request_params(args)
181
+ shadow_traffic_err = false
182
+ if @perform_checks
183
+ perform_common_checks! args
184
+
185
+ if @shadow_traffic_delivery_percent > 0 && args[:insertion_page_type] != Promoted::Ruby::Client::INSERTION_PAGING_TYPE['UNPAGED'] then
186
+ shadow_traffic_err = true
187
+ @logger.error(ShadowTrafficInsertionPageType.new) if @logger
188
+ end
149
189
  end
150
- end
190
+
191
+ pre_delivery_fillin_fields log_request_builder
151
192
 
152
- def perform_checks?
153
- @perform_checks
193
+ if !shadow_traffic_err && should_send_as_shadow_traffic?
194
+ deliver_shadow_traffic args, headers
195
+ end
196
+
197
+ log_request_builder.log_request_params
154
198
  end
155
199
 
200
+ ##
156
201
  # Sends a log request (previously created by a call to prepare_for_logging) to the metrics endpoint.
157
202
  def send_log_request log_request_params, headers={}
158
- send_request(log_request_params, @metrics_endpoint, @metrics_timeout_millis, headers)
203
+ begin
204
+ send_request(log_request_params, @metrics_endpoint, @metrics_timeout_millis, @metrics_api_key, headers)
205
+ rescue StandardError => err
206
+ # Currently we don't propagate errors to the SDK caller.
207
+ @logger.error("Error from metrics: " + err.message) if @logger
208
+ end
209
+ end
210
+
211
+ private
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
+
231
+ def add_missing_ids_on_insertions! request, insertions
232
+ insertions.each do |insertion|
233
+ insertion[:insertion_id] = SecureRandom.uuid if not insertion[:insertion_id]
234
+ insertion[:session_id] = request[:session_id] if request[:session_id]
235
+ insertion[:request_id] = request[:request_id] if request[:request_id]
236
+ end
159
237
  end
160
238
 
161
239
  def should_send_as_shadow_traffic?
@@ -173,37 +251,22 @@ module Promoted
173
251
  delivery_request_params[:client_info][:traffic_type] = Promoted::Ruby::Client::TRAFFIC_TYPE['SHADOW']
174
252
 
175
253
  # Call Delivery API async (fire and forget)
176
- 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)
177
255
  end
178
256
 
179
- def prepare_for_logging args, headers={}
180
- args = Promoted::Ruby::Client::Util.translate_args(args)
181
-
182
- log_request_builder = RequestBuilder.new
183
-
184
- # Note: This method expects as JSON (string keys) but internally, RequestBuilder
185
- # transforms and works with symbol keys.
186
- log_request_builder.set_request_params(args)
187
- if perform_checks?
188
- Promoted::Ruby::Client::Settings.check_that_log_ids_not_set!(args)
189
-
190
- if @shadow_traffic_delivery_percent > 0 && args[:insertion_page_type] != Promoted::Ruby::Client::INSERTION_PAGING_TYPE['UNPAGED'] then
191
- raise ShadowTrafficInsertionPageType
192
- end
193
- end
194
-
195
- pre_delivery_fillin_fields log_request_builder
196
-
197
- if should_send_as_shadow_traffic?
198
- deliver_shadow_traffic args, headers
257
+ def perform_common_checks!(req)
258
+ begin
259
+ @validator.check_that_log_ids_not_set!(req)
260
+ @validator.validate_metrics_request!(req)
261
+ rescue StandardError => err
262
+ @logger.error(err) if @logger
263
+ raise
199
264
  end
200
-
201
- log_request_builder.log_request_params
202
265
  end
203
266
 
204
267
  def should_apply_treatment(cohort_membership)
205
268
  if @should_apply_treatment_func != nil
206
- @should_apply_treatment_func
269
+ @should_apply_treatment_func.call(cohort_membership)
207
270
  else
208
271
  return true if !cohort_membership
209
272
  return true if !cohort_membership[:arm]
@@ -217,15 +280,6 @@ module Promoted
217
280
  log_request_builder.timing[:client_log_timestamp] = Time.now.to_i
218
281
  end
219
282
  end
220
-
221
- # A common compact method implementation.
222
- def self.copy_and_remove_properties
223
- Proc.new do |insertion|
224
- insertion = Hash[insertion]
225
- insertion.delete(:properties)
226
- insertion
227
- end
228
- end
229
283
  end
230
284
  end
231
285
  end
@@ -234,7 +288,6 @@ end
234
288
  # dependent /libs
235
289
  require "promoted/ruby/client/request_builder"
236
290
  require "promoted/ruby/client/sampler"
237
- require "promoted/ruby/client/settings"
238
291
  require "promoted/ruby/client/util"
239
- require 'byebug'
292
+ require "promoted/ruby/client/validator"
240
293
  require 'securerandom'
@@ -1,40 +1,21 @@
1
1
  module Promoted
2
2
  module Ruby
3
3
  module Client
4
- class RequestError < StandardError
5
- def message
6
- 'Request.requestId should not be set'
7
- end
8
- end
9
-
10
- class RequestInsertionError < StandardError
11
- def message
12
- 'Do not set Request.insertion. Set full_insertion.'
13
- end
14
- end
15
-
16
- class InsertionRequestIdError < StandardError
17
- def message
18
- 'Insertion.requestId should not be set'
19
- end
20
- end
21
-
22
- class InsertionIdError < StandardError
4
+ class ShadowTrafficInsertionPageType < StandardError
23
5
  def message
24
- 'Insertion.insertionId should not be set'
6
+ 'Insertions must be unpaged when shadow traffic is on'
25
7
  end
26
8
  end
27
9
 
28
- class InsertionContentId < StandardError
29
- def message
30
- 'Insertion.contentId should be set'
10
+ class EndpointError < StandardError
11
+ attr_reader :cause
12
+ def initialize(cause)
13
+ @cause = cause
14
+ super('Error calling Promoted.ai endpoint')
31
15
  end
32
16
  end
33
17
 
34
- class ShadowTrafficInsertionPageType < StandardError
35
- def message
36
- 'Insertions must be unpaged when shadow traffic is on'
37
- end
18
+ class ValidationError < StandardError
38
19
  end
39
20
  end
40
21
  end
@@ -10,6 +10,7 @@ module Promoted
10
10
  @conn = Faraday.new do |f|
11
11
  f.request :json
12
12
  f.request :retry, max: 3
13
+ f.use Faraday::Response::RaiseError # raises on 4xx and 5xx responses
13
14
  f.adapter :net_http
14
15
  end
15
16
  end
@@ -18,12 +19,16 @@ module Promoted
18
19
  response = @conn.post(endpoint) do |req|
19
20
  req.headers = req.headers.merge(additional_headers) if additional_headers
20
21
  req.headers['Content-Type'] = req.headers['Content-Type'] || 'application/json'
21
- req.options.timeout = timeout_millis / 1000
22
+ req.options.timeout = timeout_millis.to_f / 1000
22
23
  req.body = request.to_json
23
24
  end
24
25
 
25
- # TODO: Check response code, rescue on ParserError, etc.
26
- JSON.parse(response.body, :symbolize_names => true)
26
+ norm_headers = response.headers.transform_keys(&:downcase)
27
+ if norm_headers["content-type"] != nil && norm_headers["content-type"].start_with?("application/json")
28
+ JSON.parse(response.body, :symbolize_names => true)
29
+ else
30
+ response.body
31
+ end
27
32
  end
28
33
  end
29
34
  end
@@ -14,6 +14,7 @@ module Promoted
14
14
  def set_request_params args = {}
15
15
  @request = args[:request] || {}
16
16
  @experiment = args[:experiment]
17
+ @only_log = args[:only_log]
17
18
  @session_id = request[:session_id]
18
19
  @platform_id = request[:platform_id]
19
20
  @client_info = request[:client_info] || {}
@@ -23,7 +24,6 @@ module Promoted
23
24
  @request_id = SecureRandom.uuid
24
25
  @user_info = request[:user_info] || { :user_id => nil, :log_user_id => nil}
25
26
  @timing = request[:timing] || { :client_log_timestamp => Time.now.to_i }
26
- @only_log = request[:only_log]
27
27
  @to_compact_metrics_insertion_func = args[:to_compact_metrics_insertion_func]
28
28
  @to_compact_delivery_insertion_func = args[:to_compact_delivery_insertion_func]
29
29
  end
@@ -48,10 +48,16 @@ module Promoted
48
48
  params = {
49
49
  user_info: user_info,
50
50
  timing: timing,
51
- cohort_membership: @experiment,
52
- client_info: @client_info.merge({ :client_type => Promoted::Ruby::Client::CLIENT_TYPE['PLATFORM_SERVER'] })
51
+ client_info: @client_info.merge({ :client_type => Promoted::Ruby::Client::CLIENT_TYPE['PLATFORM_SERVER'] }),
52
+ platform_id: @platform_id,
53
+ request_id: @request_id,
54
+ view_id: @view_id,
55
+ session_id: @session_id,
56
+ use_case: @use_case,
57
+ search_query: request[:search_query],
58
+ properties: request[:properties],
59
+ paging: request[:paging]
53
60
  }
54
- params[:request] = request
55
61
  params[:insertion] = should_compact ? compact_delivery_insertions : full_insertion
56
62
 
57
63
  params.clean!
@@ -61,6 +67,10 @@ module Promoted
61
67
  # Maps the response insertions to the full insertions and re-insert the properties bag
62
68
  # to the responses.
63
69
  def fill_details_from_response response_insertions
70
+ if !response_insertions then
71
+ response_insertions = full_insertion
72
+ end
73
+
64
74
  props = @full_insertion.each_with_object({}) do |insertion, hash|
65
75
  hash[insertion[:content_id]] = insertion[:properties]
66
76
  end
@@ -77,35 +87,6 @@ module Promoted
77
87
  filled_in_copy
78
88
  end
79
89
 
80
- def validate_request_params
81
- # TODO
82
- end
83
-
84
- def to_compact_metrics_insertion_func
85
- @to_compact_metrics_insertion_func
86
- end
87
-
88
- def to_compact_delivery_insertion_func
89
- @to_compact_delivery_insertion_func
90
- end
91
-
92
- # A list of the response Insertions. This client expects lists to be truncated
93
- # already to request.paging.size. If not truncated, this client will truncate
94
- # the list.
95
- def insertion
96
- @insertion
97
- end
98
-
99
- # A way to turn off logging. Defaults to true.
100
- def enabled?
101
- @enabled
102
- end
103
-
104
- # Default values to use on DeliveryRequests.
105
- def default_request_values
106
- @default_request_values
107
- end
108
-
109
90
  def log_request_params(include_insertions: true, include_request: true)
110
91
  params = {
111
92
  user_info: user_info,
@@ -154,6 +135,14 @@ module Promoted
154
135
  @insertion
155
136
  end
156
137
 
138
+ private
139
+
140
+ # A list of the response Insertions. This client expects lists to be truncated
141
+ # already to request.paging.size. If not truncated, this client will truncate
142
+ # the list.
143
+ def insertion
144
+ @insertion
145
+ end
157
146
  end
158
147
  end
159
148
  end
@@ -0,0 +1,159 @@
1
+ module Promoted
2
+ module Ruby
3
+ module Client
4
+ class Validator
5
+ def validate_user_info!(ui)
6
+ validate_fields!(
7
+ ui,
8
+ "user info",
9
+ [
10
+ {
11
+ :name => :user_id,
12
+ :type => String
13
+ },
14
+ {
15
+ :name => :log_user_id,
16
+ :type => String
17
+ }
18
+ ]
19
+ )
20
+ end
21
+
22
+ def validate_insertion!(ins)
23
+ validate_fields!(
24
+ ins,
25
+ "insertion",
26
+ [
27
+ {
28
+ :name => :platform_id,
29
+ :type => Integer
30
+ },
31
+ {
32
+ :name => :insertion_id,
33
+ :type => String
34
+ },
35
+ {
36
+ :name => :request_id,
37
+ :type => String
38
+ },
39
+ {
40
+ :name => :view_id,
41
+ :type => String
42
+ },
43
+ {
44
+ :name => :session_id,
45
+ :type => String
46
+ },
47
+ {
48
+ :name => :content_id,
49
+ :type => String
50
+ },
51
+ {
52
+ :name => :position,
53
+ :type => Integer
54
+ },
55
+ {
56
+ :name => :delivery_score,
57
+ :type => Integer
58
+ }
59
+ ]
60
+ )
61
+
62
+ if ins[:user_info] then
63
+ self.validate_user_info! ins[:user_info]
64
+ end
65
+ end
66
+
67
+ def validate_request!(req)
68
+ validate_fields!(
69
+ req,
70
+ "request",
71
+ [
72
+ {
73
+ :name => :platform_id,
74
+ :type => Integer
75
+ },
76
+ {
77
+ :name => :request_id,
78
+ :type => String
79
+ },
80
+ {
81
+ :name => :view_id,
82
+ :type => String
83
+ },
84
+ {
85
+ :name => :session_id,
86
+ :type => String
87
+ },
88
+ {
89
+ :name => :insertion,
90
+ :type => Array
91
+ }
92
+ ]
93
+ )
94
+
95
+ if req[:insertion] then
96
+ req[:insertion].each {|ins|
97
+ validate_insertion! ins
98
+ }
99
+ end
100
+
101
+ if req[:user_info] then
102
+ validate_user_info! req[:user_info]
103
+ end
104
+ end
105
+
106
+ def validate_metrics_request!(metrics_req)
107
+ validate_fields!(
108
+ metrics_req,
109
+ "metrics request",
110
+ [
111
+ {
112
+ :name => :request,
113
+ :required => true
114
+ },
115
+ {
116
+ :name => :full_insertion,
117
+ :required => true,
118
+ :type => Array
119
+ }
120
+ ]
121
+ )
122
+
123
+ validate_request!(metrics_req[:request])
124
+ metrics_req[:full_insertion].each {|ins|
125
+ validate_insertion! ins
126
+ }
127
+ end
128
+
129
+ def check_that_log_ids_not_set! req
130
+ raise ValidationError.new("Request.requestId should not be set") if req.dig(:request, :request_id)
131
+ raise ValidationError.new("Do not set Request.insertion. Set full_insertion.") if req[:insertion]
132
+
133
+ req[:full_insertion].each do |insertion_hash|
134
+ raise ValidationError.new("Insertion.requestId should not be set") if insertion_hash[:request_id]
135
+ raise ValidationError.new("'Insertion.insertionId should not be set") if insertion_hash[:insertion_id]
136
+ raise ValidationError.new("Insertion.deliveryScore should not be set") if insertion_hash[:delivery_score]
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def validate_fields!(obj, obj_name, fields)
143
+ fields.each {|field|
144
+ if field[:required] then
145
+ raise ValidationError.new(field[:name].to_s + " is required on " + obj_name) if !obj.has_key?(field[:name])
146
+ end
147
+
148
+ # If a field is provided as non-nil, it should be of the correct type.
149
+ if field[:type] && obj.has_key?(field[:name]) && obj[field[:name]] != nil then
150
+ raise ValidationError.new(field[:name].to_s + " should be a " + field[:type].to_s) if !obj[field[:name]].is_a?(field[:type])
151
+ end
152
+ }
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ require "promoted/ruby/client/errors"
@@ -1,7 +1,7 @@
1
1
  module Promoted
2
2
  module Ruby
3
3
  module Client
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.6"
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.1
4
+ version: 0.1.6
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-25 00:00:00.000000000 Z
11
+ date: 2021-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -77,8 +77,8 @@ files:
77
77
  - lib/promoted/ruby/client/faraday_http_client.rb
78
78
  - lib/promoted/ruby/client/request_builder.rb
79
79
  - lib/promoted/ruby/client/sampler.rb
80
- - lib/promoted/ruby/client/settings.rb
81
80
  - lib/promoted/ruby/client/util.rb
81
+ - lib/promoted/ruby/client/validator.rb
82
82
  - lib/promoted/ruby/client/version.rb
83
83
  - promoted-ruby-client.gemspec
84
84
  homepage: https://github.com/promotedai/promoted-ruby-client
@@ -1,21 +0,0 @@
1
- module Promoted
2
- module Ruby
3
- module Client
4
- class Settings
5
-
6
- def self.check_that_log_ids_not_set! options_hash
7
- raise RequestError if options_hash.dig(:request, :request_id)
8
- raise RequestInsertionError if options_hash[:insertion]
9
-
10
- options_hash[:full_insertion].each do |insertion_hash|
11
- raise InsertionRequestIdError if insertion_hash[:request_id]
12
- raise InsertionIdError if insertion_hash[:insertion_id]
13
- end
14
- true
15
- end
16
- end
17
- end
18
- end
19
- end
20
-
21
- require "promoted/ruby/client/errors"