sift 2.2.1 → 4.1.0

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
- SHA1:
3
- metadata.gz: e4c2e3042d8ff485909a45e093bfe1f1145e8f28
4
- data.tar.gz: 57c0c4e3bb6cfdfe26cb5907717581d4ba8d3e25
2
+ SHA256:
3
+ metadata.gz: 4b20b0230176dc6340e5ea4939e51b6e1ed2642e788c25fa2f7f4e829b5c61ff
4
+ data.tar.gz: 28932a0fddd658fe868c6c6b6b408ab1d8529060cfbe0766ffeef76e2398e43e
5
5
  SHA512:
6
- metadata.gz: 8f81535b358bab94f1156197e54612f3e8c81210dbbe38a45e68309eb3327bfc9c6ac164921fcece99059577ded9b6431f65caae9016513c00dfb512e4e76d3a
7
- data.tar.gz: 063f7691c9f6f99f241db47f837135dd0e32fcb1d65d5c35aea1aece78effb4fa6fdfbdb97d758d2b9d6fb8905837f030bfd7b56b5c1ab942f7787bb1ef73a9f
6
+ metadata.gz: b905f6002c28777a4cf82cc6b0e59b2c79c300fdf1f4b4b3bf8b0bff8691946325d83794da812d5abc4b344b5e35d24f72b720aae1ddc0783b7e935117f35588
7
+ data.tar.gz: e161d32f1c210f3fc4562b985537ceb6e2f304330a3fb14ddbce142cd105b0a0c567f6e177dd3c29c99e6c50133e3a7eb40c5ba922071914d9cf56b540c8f427
@@ -0,0 +1,43 @@
1
+ version: 2
2
+
3
+ jobs:
4
+ build:
5
+ parallelism: 3 # run three instances of this job in parallel
6
+ docker:
7
+ - image: circleci/ruby:2.4.2-jessie-node
8
+ environment:
9
+ BUNDLE_JOBS: 3
10
+ BUNDLE_RETRY: 3
11
+ BUNDLE_PATH: vendor/bundle
12
+ steps:
13
+ - checkout
14
+
15
+ - run:
16
+ name: Which bundler?
17
+ command: bundle -v
18
+
19
+ - restore_cache:
20
+ keys:
21
+ - sift-bundle-{{ checksum "sift.gemspec" }}
22
+ - sift-bundle-
23
+
24
+ - run:
25
+ name: Bundle Install
26
+ command: bundle check || bundle install
27
+
28
+ - save_cache:
29
+ key: sift-bundle-{{ checksum "sift.gemspec" }}
30
+ paths:
31
+ - vendor/bundle
32
+
33
+ - run:
34
+ name: Run rspec in parallel
35
+ command: |
36
+ bundle exec rspec --profile 10 \
37
+ --format RspecJunitFormatter \
38
+ --out test_results/rspec.xml \
39
+ --format progress \
40
+ $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
41
+
42
+ - store_test_results:
43
+ path: test_results
data/HISTORY CHANGED
@@ -1,3 +1,34 @@
1
+ === 4.1.0 2022-06-22
2
+ - Add return_route_info query param
3
+
4
+ === 4.0.0 2019-05-15
5
+ - Breaking change: Propagate exception in Client.track() – previously we were silently swallowing exceptions and now we are bubbling them up. You will need to add exception handling code.
6
+ - Fix URL encoding
7
+ - Add Circle build
8
+
9
+ === 3.3.0 2018-07-31
10
+ - Add support for rescore_user and get_user_score APIs
11
+
12
+ === 3.2.0 2018-07-05
13
+ - Add new query parameter force_workflow_run
14
+
15
+ === 3.1.0 2018-06-04
16
+ - Adds support for get session decisions to [Decisions API](https://siftscience.com/developers/docs/curl/decisions-api)
17
+
18
+ === 3.0.1 2018-04-06
19
+ - Improved documentation on HISTORY and README.MD
20
+
21
+ === 3.0.0 2018-03-05
22
+ - Adds support for Sift Science API Version 205, including new [`$create_content`](https://siftscience.com/developers/docs/curl/events-api/reserved-events/create-content) and [`$update_content`](https://siftscience.com/developers/docs/curl/events-api/reserved-events/update-content) formats
23
+ - V205 APIs are now called -- **this is an incompatible change**
24
+ - (use `:version => 204` to call the previous API version)
25
+ - Adds support for content decisions to [Decisions API](https://siftscience.com/developers/docs/curl/decisions-api)
26
+
27
+ INCOMPATIBLE CHANGES INTRODUCED IN API V205:
28
+ - `$create_content` and `$update_content` have significantly changed, and the old format will be rejected
29
+ - `$send_message` and `$submit_review` events are no longer valid
30
+ - V205 improves server-side event data validation. In V204 and earlier, server-side validation accepted some events that did not conform to the published APIs in our [developer documentation](https://siftscience.com/developers/docs/curl/events-api). V205 does not modify existing event APIs other than those mentioned above, but may reject invalid event data that were previously accepted. **Please test your integration on V205 in sandbox before using in production.**
31
+
1
32
  === 2.2.1.0 2018-02-12
2
33
  * Add session level decisions in Apply Decisions APIs.
3
34
 
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # Sift Science Ruby bindings [![Build Status](https://travis-ci.org/SiftScience/sift-ruby.png?branch=master)](https://travis-ci.org/SiftScience/sift-ruby)
1
+ # sift-ruby
2
+ [![CircleCI](https://circleci.com/gh/SiftScience/sift-ruby.svg?style=svg)](https://circleci.com/gh/SiftScience/sift-ruby)
3
+
4
+ The official Ruby bindings for the latest version (v205) of the [Sift API](https://sift.com/developers/docs/java/apis-overview).
2
5
 
3
6
  ## Requirements
4
7
 
5
- * Ruby 1.9.3 or above.
8
+ * Ruby 2.0.0 or above.
6
9
 
7
10
 
8
11
  ## Installation
@@ -32,7 +35,7 @@ client = Sift::Client.new()
32
35
 
33
36
  ##### OR
34
37
 
35
- client = Sift::Client.new(api_key: '<your_api_key_here>', account_id: '<your_account_id_here>'
38
+ client = Sift::Client.new(api_key: '<your_api_key_here>', account_id: '<your_account_id_here>')
36
39
 
37
40
  ```
38
41
 
@@ -73,14 +76,14 @@ response = client.score(user_id)
73
76
 
74
77
  ## Decisions
75
78
 
76
- To learn more about the decisions endpoint visit our [developer docs](https://siftscience.com/developers/docs/ruby/decisions-api/get-decisions).
79
+ To learn more about the decisions endpoint visit our [developer docs](https://sift.com/developers/docs/ruby/decisions-api/get-decisions).
77
80
 
78
81
  ### List of Configured Decisions
79
82
 
80
83
  Get a list of your decisions.
81
84
 
82
85
  **Optional Params**
83
- - `entity_type`: `:user` or `:order` or `:session`
86
+ - `entity_type`: `:user` or `:order` or `:session` or `:content`
84
87
  - `abuse_types`: `["payment_abuse", "content_abuse", "content_abuse",
85
88
  "account_abuse", "legacy", "account_takeover"]`
86
89
 
@@ -117,7 +120,7 @@ end
117
120
 
118
121
  ### Apply a decision
119
122
 
120
- Applies a decision to an entity. Visit our [developer docs](http://siftscience.com/developers/docs/ruby/decisions-api/apply-decision) for more information.
123
+ Applies a decision to an entity. Visit our [developer docs](http://sift.com/developers/docs/ruby/decisions-api/apply-decision) for more information.
121
124
 
122
125
  **Required Params:**
123
126
  - `decision_id`, `source`, `user_id`
@@ -140,7 +143,7 @@ response = client.apply_decision({
140
143
  user_id: "john@example.com"
141
144
  })
142
145
 
143
- # apply decision to "bob@example.com"'s order
146
+ # apply decision to "john@example.com"'s order
144
147
  response = client.apply_decision({
145
148
  decision_id: "block_bad_order",
146
149
  source: "manual_review",
@@ -149,7 +152,7 @@ response = client.apply_decision({
149
152
  order_id: "ORDER_1234"
150
153
  })
151
154
 
152
- # apply decision to "bob@example.com"'s session
155
+ # apply decision to "john@example.com"'s session
153
156
  response = client.apply_decision({
154
157
  decision_id: "block_bad_session",
155
158
  source: "manual_review",
@@ -158,6 +161,16 @@ response = client.apply_decision({
158
161
  session_id: "SESSION_ID_1234"
159
162
  })
160
163
 
164
+ # apply decision to "john@example.com"'s content
165
+ response = client.apply_decision({
166
+ decision_id: "block_bad_session",
167
+ source: "manual_review",
168
+ analyst: "bob@your_company.com",
169
+ user_id: "john@example.com",
170
+ content_id: "CONTENT_ID_1234"
171
+ })
172
+
173
+
161
174
  # Make sure you handle the response after applying a decision:
162
175
 
163
176
  if response.ok?
@@ -196,6 +209,12 @@ response = client.get_user_decisions('example_user_id')
196
209
 
197
210
  # Get the latest decisions for an order
198
211
  response = client.get_order_decisions('example_order_id')
212
+
213
+ # Get the latest decisions for a session
214
+ response = client.get_session_decisions('example_user_id', 'example_session_id')
215
+
216
+ # Get the latest decisions for an content
217
+ response = client.get_content_decisions('example_user_id', 'example_order_id')
199
218
  ```
200
219
 
201
220
  ## Response Object
@@ -15,6 +15,7 @@ module Sift
15
15
  description
16
16
  order_id
17
17
  session_id
18
+ content_id
18
19
  user_id
19
20
  account_id
20
21
  time
@@ -93,11 +94,17 @@ module Sift
93
94
  configs.has_key?("session_id") || configs.has_key?(:session_id)
94
95
  end
95
96
 
97
+ def applying_to_content?
98
+ configs.has_key?("content_id") || configs.has_key?(:content_id)
99
+ end
100
+
96
101
  def path
97
102
  if applying_to_order?
98
103
  "#{user_path}/orders/#{CGI.escape(order_id)}/decisions"
99
104
  elsif applying_to_session?
100
105
  "#{user_path}/sessions/#{CGI.escape(session_id)}/decisions"
106
+ elsif applying_to_content?
107
+ "#{user_path}/content/#{CGI.escape(content_id)}/decisions"
101
108
  else
102
109
  "#{user_path}/decisions"
103
110
  end
data/lib/sift/client.rb CHANGED
@@ -34,15 +34,25 @@ module Sift
34
34
 
35
35
  # only set these variables if a message-body is expected.
36
36
  if not @http_raw_response.kind_of? Net::HTTPNoContent
37
- @body = MultiJson.load(http_response) unless http_response.nil?
38
- @request = MultiJson.load(@body["request"].to_s) if @body["request"]
39
- @api_status = @body["status"].to_i if @body["status"]
40
- @api_error_message = @body["error_message"]
41
-
42
- if @body["error"]
43
- @api_error_message = @body["error"]
44
- @api_error_description = @body["description"]
45
- @api_error_issues = @body["issues"] || {}
37
+
38
+ begin
39
+ @body = MultiJson.load(http_response) unless http_response.nil?
40
+ rescue
41
+ if @http_status_code == 200
42
+ raise TypeError.new
43
+ end
44
+ end
45
+
46
+ if not @body.nil?
47
+ @request = MultiJson.load(@body["request"].to_s) if @body["request"]
48
+ @api_status = @body["status"].to_i if @body["status"]
49
+ @api_error_message = @body["error_message"]
50
+
51
+ if @body["error"]
52
+ @api_error_message = @body["error"]
53
+ @api_error_description = @body["description"]
54
+ @api_error_issues = @body["issues"] || {}
55
+ end
46
56
  end
47
57
  end
48
58
  end
@@ -118,7 +128,7 @@ module Sift
118
128
  #
119
129
  # :version::
120
130
  # The version of the Events API, Score API, and Labels API to call.
121
- # By default, version 204.
131
+ # By default, version 205.
122
132
  #
123
133
  # :path::
124
134
  # The URL path to use for Events API path. By default, the
@@ -132,7 +142,7 @@ module Sift
132
142
  @timeout = opts[:timeout] || 2 # 2-second timeout by default
133
143
  @path = opts[:path] || Sift.rest_api_path(@version)
134
144
 
135
- raise("api_key must be a non-empty string") if !@api_key.is_a?(String) || @api_key.empty?
145
+ raise("api_key") if !@api_key.is_a?(String) || @api_key.empty?
136
146
  raise("path must be a non-empty string") if !@path.is_a?(String) || @path.empty?
137
147
  end
138
148
 
@@ -194,8 +204,8 @@ module Sift
194
204
  #
195
205
  # ==== Returns:
196
206
  #
197
- # In the case of a connection error (timeout, broken connection,
198
- # etc.), this method returns nil; otherwise, a Response object is
207
+ # In the case of a network error (timeout, broken connection, etc.),
208
+ # this method propagates the exception, otherwise, a Response object is
199
209
  # returned that captures the status message and status code.
200
210
  #
201
211
  def track(event, properties = {}, opts = {})
@@ -206,6 +216,8 @@ module Sift
206
216
  return_score = opts[:return_score]
207
217
  return_action = opts[:return_action]
208
218
  return_workflow_status = opts[:return_workflow_status]
219
+ return_route_info = opts[:return_route_info]
220
+ force_workflow_run = opts[:force_workflow_run]
209
221
  abuse_types = opts[:abuse_types]
210
222
 
211
223
  raise("event must be a non-empty string") if (!event.is_a? String) || event.empty?
@@ -216,6 +228,8 @@ module Sift
216
228
  query["return_score"] = "true" if return_score
217
229
  query["return_action"] = "true" if return_action
218
230
  query["return_workflow_status"] = "true" if return_workflow_status
231
+ query["return_route_info"] = "true" if return_route_info
232
+ query["force_workflow_run"] = "true" if force_workflow_run
219
233
  query["abuse_types"] = abuse_types.join(",") if abuse_types
220
234
 
221
235
  options = {
@@ -226,14 +240,8 @@ module Sift
226
240
  }
227
241
  options.merge!(:timeout => timeout) unless timeout.nil?
228
242
 
229
- begin
230
- response = self.class.post(path, options)
231
- Response.new(response.body, response.code, response.response)
232
- rescue StandardError => e
233
- Sift.warn("Failed to track event: " + e.to_s)
234
- Sift.warn(e.backtrace)
235
- nil
236
- end
243
+ response = self.class.post(path, options)
244
+ Response.new(response.body, response.code, response.response)
237
245
  end
238
246
 
239
247
 
@@ -293,6 +301,114 @@ module Sift
293
301
  end
294
302
 
295
303
 
304
+ # Fetches the latest score(s) computed for the specified user and abuse types.
305
+ #
306
+ # As opposed to client.score() and client.rescore_user(), this *does not* compute
307
+ # a new score for the user; it simply fetches the latest score(s) which have computed.
308
+ # These scores may be arbitrarily old.
309
+ #
310
+ # See https://siftscience.com/developers/docs/ruby/score-api/get-score for more details.
311
+ #
312
+ # ==== Parameters:
313
+ #
314
+ # user_id::
315
+ # A user's id. This id should be the same as the user_id used in
316
+ # event calls.
317
+ #
318
+ # opts (optional)::
319
+ # A Hash of optional parameters for the request --
320
+ #
321
+ # :abuse_types::
322
+ # List of abuse types, specifying for which abuse types a
323
+ # score should be returned. By default, a score is returned
324
+ # for every abuse type to which you are subscribed.
325
+ #
326
+ # :api_key::
327
+ # Overrides the API key for this call.
328
+ #
329
+ # :timeout::
330
+ # Overrides the timeout (in seconds) for this call.
331
+ #
332
+ # ==== Returns:
333
+ #
334
+ # A Response object containing a status code, status message, and,
335
+ # if successful, the user's score(s).
336
+ #
337
+ def get_user_score(user_id, opts = {})
338
+ abuse_types = opts[:abuse_types]
339
+ api_key = opts[:api_key] || @api_key
340
+ timeout = opts[:timeout] || @timeout
341
+
342
+ raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
343
+ raise("Bad api_key parameter") if api_key.empty?
344
+
345
+ query = {}
346
+ query["api_key"] = api_key
347
+ query["abuse_types"] = abuse_types.join(",") if abuse_types
348
+
349
+ options = {
350
+ :headers => {"User-Agent" => user_agent},
351
+ :query => query
352
+ }
353
+ options.merge!(:timeout => timeout) unless timeout.nil?
354
+
355
+ response = self.class.get(Sift.user_score_api_path(user_id, @version), options)
356
+ Response.new(response.body, response.code, response.response)
357
+ end
358
+
359
+
360
+ # Rescores the specified user for the specified abuse types and returns the resulting score(s).
361
+ #
362
+ # See https://siftscience.com/developers/docs/ruby/score-api/rescore for more details.
363
+ #
364
+ # ==== Parameters:
365
+ #
366
+ # user_id::
367
+ # A user's id. This id should be the same as the user_id used in
368
+ # event calls.
369
+ #
370
+ # opts (optional)::
371
+ # A Hash of optional parameters for the request --
372
+ #
373
+ # :abuse_types::
374
+ # List of abuse types, specifying for which abuse types a
375
+ # score should be returned. By default, a score is returned
376
+ # for every abuse type to which you are subscribed.
377
+ #
378
+ # :api_key::
379
+ # Overrides the API key for this call.
380
+ #
381
+ # :timeout::
382
+ # Overrides the timeout (in seconds) for this call.
383
+ #
384
+ # ==== Returns:
385
+ #
386
+ # A Response object containing a status code, status message, and,
387
+ # if successful, the user's score(s).
388
+ #
389
+ def rescore_user(user_id, opts = {})
390
+ abuse_types = opts[:abuse_types]
391
+ api_key = opts[:api_key] || @api_key
392
+ timeout = opts[:timeout] || @timeout
393
+
394
+ raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
395
+ raise("Bad api_key parameter") if api_key.empty?
396
+
397
+ query = {}
398
+ query["api_key"] = api_key
399
+ query["abuse_types"] = abuse_types.join(",") if abuse_types
400
+
401
+ options = {
402
+ :headers => {"User-Agent" => user_agent},
403
+ :query => query
404
+ }
405
+ options.merge!(:timeout => timeout) unless timeout.nil?
406
+
407
+ response = self.class.post(Sift.user_score_api_path(user_id, @version), options)
408
+ Response.new(response.body, response.code, response.response)
409
+ end
410
+
411
+
296
412
  # Labels a user.
297
413
  #
298
414
  # See https://siftscience.com/developers/docs/ruby/labels-api/label-user .
@@ -504,6 +620,86 @@ module Sift
504
620
  Response.new(response.body, response.code, response.response)
505
621
  end
506
622
 
623
+ # Gets the decision status of a session.
624
+ #
625
+ # See https://siftscience.com/developers/docs/ruby/decisions-api/decision-status .
626
+ #
627
+ # ==== Parameters
628
+ #
629
+ # user_id::
630
+ # The ID of the user in the session.
631
+ #
632
+ # session_id::
633
+ # The ID of a session.
634
+ #
635
+ # opts (optional)::
636
+ # A Hash of optional parameters for this request --
637
+ #
638
+ # :account_id::
639
+ # Overrides the account id for this call.
640
+ #
641
+ # :api_key::
642
+ # Overrides the API key for this call.
643
+ #
644
+ # :timeout::
645
+ # Overrides the timeout (in seconds) for this call.
646
+ #
647
+ def get_session_decisions(user_id, session_id, opts = {})
648
+ account_id = opts[:account_id] || @account_id
649
+ api_key = opts[:api_key] || @api_key
650
+ timeout = opts[:timeout] || @timeout
651
+
652
+ options = {
653
+ :headers => { "User-Agent" => user_agent },
654
+ :basic_auth => { :username => api_key, :password => "" }
655
+ }
656
+ options.merge!(:timeout => timeout) unless timeout.nil?
657
+
658
+ uri = API3_ENDPOINT + Sift.session_decisions_api_path(account_id, user_id, session_id)
659
+ response = self.class.get(uri, options)
660
+ Response.new(response.body, response.code, response.response)
661
+ end
662
+
663
+ # Gets the decision status of a piece of content.
664
+ #
665
+ # See https://siftscience.com/developers/docs/ruby/decisions-api/decision-status .
666
+ #
667
+ # ==== Parameters
668
+ #
669
+ # user_id::
670
+ # The ID of the owner of the content.
671
+ #
672
+ # content_id::
673
+ # The ID of a piece of content.
674
+ #
675
+ # opts (optional)::
676
+ # A Hash of optional parameters for this request --
677
+ #
678
+ # :account_id::
679
+ # Overrides the API key for this call.
680
+ #
681
+ # :api_key::
682
+ # Overrides the API key for this call.
683
+ #
684
+ # :timeout::
685
+ # Overrides the timeout (in seconds) for this call.
686
+ #
687
+ def get_content_decisions(user_id, content_id, opts = {})
688
+ account_id = opts[:account_id] || @account_id
689
+ api_key = opts[:api_key] || @api_key
690
+ timeout = opts[:timeout] || @timeout
691
+
692
+ options = {
693
+ :headers => { "User-Agent" => user_agent },
694
+ :basic_auth => { :username => api_key, :password => "" }
695
+ }
696
+ options.merge!(:timeout => timeout) unless timeout.nil?
697
+
698
+ uri = API3_ENDPOINT + Sift.content_decisions_api_path(account_id, user_id, content_id)
699
+ response = self.class.get(uri, options)
700
+ Response.new(response.body, response.code, response.response)
701
+ end
702
+
507
703
  def decisions(opts = {})
508
704
  decision_instance.list(opts)
509
705
  end
@@ -20,6 +20,12 @@ module Sift
20
20
  validate_key(:non_empty_string, :user_id, :session_id)
21
21
  end
22
22
  end
23
+
24
+ def valid_content?
25
+ run do
26
+ validate_key(:non_empty_string, :user_id, :content_id)
27
+ end
28
+ end
23
29
 
24
30
  def valid_user?
25
31
  run do
data/lib/sift/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Sift
2
- VERSION = "2.2.1"
3
- API_VERSION = "204"
2
+ VERSION = "4.1.0"
3
+ API_VERSION = "205"
4
4
  end
data/lib/sift.rb CHANGED
@@ -1,5 +1,6 @@
1
- require_relative "./sift/version"
2
- require_relative "./sift/client"
1
+ require_relative './sift/version'
2
+ require_relative './sift/client'
3
+ require 'erb'
3
4
 
4
5
  module Sift
5
6
 
@@ -10,27 +11,49 @@ module Sift
10
11
 
11
12
  # Returns the Score API path for the specified user ID and API version
12
13
  def self.score_api_path(user_id, version=API_VERSION)
13
- "/v#{version}/score/#{URI.encode(user_id)}/"
14
+ "/v#{version}/score/#{ERB::Util.url_encode(user_id)}/"
15
+ end
16
+
17
+ # Returns the User Score API path for the specified user ID and API version
18
+ def self.user_score_api_path(user_id, version=API_VERSION)
19
+ "/v#{version}/users/#{ERB::Util.url_encode(user_id)}/score"
14
20
  end
15
21
 
16
22
  # Returns the users API path for the specified user ID and API version
17
23
  def self.users_label_api_path(user_id, version=API_VERSION)
18
- "/v#{version}/users/#{URI.encode(user_id)}/labels"
24
+ "/v#{version}/users/#{ERB::Util.url_encode(user_id)}/labels"
19
25
  end
20
26
 
21
27
  # Returns the path for the Workflow Status API
22
28
  def self.workflow_status_path(account_id, run_id)
23
- "/v3/accounts/#{account_id}/workflows/runs/#{run_id}"
29
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
30
+ "/workflows/runs/#{ERB::Util.url_encode(run_id)}"
24
31
  end
25
32
 
26
33
  # Returns the path for User Decisions API
27
34
  def self.user_decisions_api_path(account_id, user_id)
28
- "/v3/accounts/#{account_id}/users/#{user_id}/decisions"
35
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
36
+ "/users/#{ERB::Util.url_encode(user_id)}/decisions"
29
37
  end
30
38
 
31
39
  # Returns the path for Orders Decisions API
32
40
  def self.order_decisions_api_path(account_id, order_id)
33
- "/v3/accounts/#{account_id}/orders/#{order_id}/decisions"
41
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
42
+ "/orders/#{ERB::Util.url_encode(order_id)}/decisions"
43
+ end
44
+
45
+ # Returns the path for Session Decisions API
46
+ def self.session_decisions_api_path(account_id, user_id, session_id)
47
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
48
+ "/users/#{ERB::Util.url_encode(user_id)}" \
49
+ "/sessions/#{ERB::Util.url_encode(session_id)}/decisions"
50
+ end
51
+
52
+ # Returns the path for Content Decisions API
53
+ def self.content_decisions_api_path(account_id, user_id, content_id)
54
+ "/v3/accounts/#{ERB::Util.url_encode(account_id)}" \
55
+ "/users/#{ERB::Util.url_encode(user_id)}" \
56
+ "/content/#{ERB::Util.url_encode(content_id)}/decisions"
34
57
  end
35
58
 
36
59
  # Module-scoped public API key
@@ -64,5 +87,4 @@ module Sift
64
87
  def self.fatal(msg)
65
88
  @logger.fatal(msg) if @logger
66
89
  end
67
-
68
90
  end
data/sift.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
 
22
22
  # Gems that must be intalled for sift to compile and build
23
23
  s.add_development_dependency "rspec", "~> 3.5"
24
+ s.add_development_dependency "rspec_junit_formatter"
24
25
  s.add_development_dependency "pry"
25
26
  s.add_development_dependency "webmock", ">= 1.16.0", "< 2"
26
27
 
@@ -25,7 +25,7 @@ describe Sift::Client do
25
25
  response_json = { :status => 0, :error_message => "OK" }
26
26
  user_id = "frodo_baggins"
27
27
 
28
- stub_request(:post, "https://api.siftscience.com/v204/users/frodo_baggins/labels")
28
+ stub_request(:post, "https://api.siftscience.com/v205/users/frodo_baggins/labels")
29
29
  .with(:body => ('{"$abuse_type":"content_abuse","$is_bad":true,"$description":"Listed a fake item","$type":"$label","$api_key":"foobar"}'))
30
30
  .to_return(:body => MultiJson.dump(response_json), :status => 200,
31
31
  :headers => {"content-type"=>"application/json; charset=UTF-8",
@@ -45,7 +45,7 @@ describe Sift::Client do
45
45
  user_id = "frodo_baggins"
46
46
 
47
47
  stub_request(:delete,
48
- "https://api.siftscience.com/v204/users/frodo_baggins/labels?api_key=foobar&abuse_type=payment_abuse")
48
+ "https://api.siftscience.com/v205/users/frodo_baggins/labels?api_key=foobar&abuse_type=payment_abuse")
49
49
  .to_return(:status => 204)
50
50
 
51
51
  api_key = "foobar"
@@ -43,6 +43,38 @@ describe Sift::Client do
43
43
  }
44
44
  end
45
45
 
46
+ def user_score_response_json
47
+ {
48
+ :entity_type => "user",
49
+ :entity_id => "247019",
50
+ :scores => {
51
+ :payment_abuse => {
52
+ :score => 0.78
53
+ },
54
+ :content_abuse => {
55
+ :score => 0.11
56
+ }
57
+ },
58
+ :latest_decisions => {
59
+ :payment_abuse => {
60
+ :id => "user_looks_bad_payment_abuse",
61
+ :category => "block",
62
+ :source => "AUTOMATED_RULE",
63
+ :time => 1352201880,
64
+ :description => "Bad Fraudster"
65
+ }
66
+ },
67
+ :latest_labels => {
68
+ :payment_abuse => {
69
+ :is_bad => true,
70
+ :time => 1352201880
71
+ }
72
+ },
73
+ :status => 0,
74
+ :error_message => "OK"
75
+ }
76
+ end
77
+
46
78
  def action_response_json
47
79
  {
48
80
  :user_id => "247019",
@@ -140,36 +172,59 @@ describe Sift::Client do
140
172
  end
141
173
 
142
174
 
143
- it "Doesn't raise an exception on Net/HTTP errors" do
175
+ it "Handles parse exceptions for 500 status" do
144
176
  api_key = "foobar"
145
177
  event = "$transaction"
146
178
  properties = valid_transaction_properties
179
+ res = nil
180
+
181
+ stub_request(:any, /.*/).to_return(:body => "{123", :status => 500)
182
+
183
+ expect { res = Sift::Client.new(:api_key => api_key).track(event, properties) }.not_to raise_error
184
+ expect(res.http_status_code).to eq(500)
185
+ end
186
+
187
+
188
+ it "Handles parse exceptions for 200 status" do
189
+ api_key = "foobar"
190
+ event = "$transaction"
191
+ properties = valid_transaction_properties
192
+ res = nil
193
+
194
+ stub_request(:any, /.*/).to_return(:body => "{123", :status => 200)
195
+
196
+ expect { res = Sift::Client.new(:api_key => api_key).track(event, properties) }.to raise_error(TypeError)
197
+ end
198
+
199
+
200
+ it "Preserves response on HTTP errors but does not raise an exception" do
201
+ api_key = "foobar"
202
+ event = "$transaction"
203
+ properties = valid_transaction_properties
204
+ res = nil
147
205
 
148
206
  stub_request(:any, /.*/).to_return(:status => 401)
149
207
 
150
- # This method should just return nil -- the track call failed because
151
- # of an HTTP error
152
- expect(Sift::Client.new(:api_key => api_key).track(event, properties)).to eq(nil)
208
+ expect { res = Sift::Client.new(:api_key => api_key).track(event, properties) }.not_to raise_error
209
+ expect(res.http_status_code).to eq(401)
153
210
  end
154
211
 
155
212
 
156
- it "Returns nil when a StandardError occurs within the request" do
213
+ it "Propagates exception when a StandardError occurs within the request" do
157
214
  api_key = "foobar"
158
215
  event = "$transaction"
159
216
  properties = valid_transaction_properties
160
217
 
161
218
  stub_request(:any, /.*/).to_raise(StandardError)
162
219
 
163
- # This method should just return nil -- the track call failed because
164
- # a StandardError exception was thrown
165
- expect(Sift::Client.new(:api_key => api_key).track(event, properties)).to eq(nil)
220
+ expect { Sift::Client.new(:api_key => api_key).track(event, properties) }.to raise_error(StandardError)
166
221
  end
167
222
 
168
223
 
169
224
  it "Successfuly handles an event and returns OK" do
170
225
  response_json = { :status => 0, :error_message => "OK" }
171
226
 
172
- stub_request(:post, "https://api.siftscience.com/v204/events").
227
+ stub_request(:post, "https://api.siftscience.com/v205/events").
173
228
  with { |request|
174
229
  parsed_body = JSON.parse(request.body)
175
230
  expect(parsed_body).to include("$buyer_user_id" => "123456")
@@ -190,7 +245,7 @@ describe Sift::Client do
190
245
 
191
246
  it "Successfully submits event with overridden key" do
192
247
  response_json = { :status => 0, :error_message => "OK"}
193
- stub_request(:post, "https://api.siftscience.com/v204/events").
248
+ stub_request(:post, "https://api.siftscience.com/v205/events").
194
249
  with { | request|
195
250
  parsed_body = JSON.parse(request.body)
196
251
  expect(parsed_body).to include("$buyer_user_id" => "123456")
@@ -212,7 +267,7 @@ describe Sift::Client do
212
267
  it "Successfully scrubs nils" do
213
268
  response_json = { :status => 0, :error_message => "OK" }
214
269
 
215
- stub_request(:post, "https://api.siftscience.com/v204/events")
270
+ stub_request(:post, "https://api.siftscience.com/v205/events")
216
271
  .with { |request|
217
272
  parsed_body = JSON.parse(request.body)
218
273
  expect(parsed_body).not_to include("fake_property")
@@ -241,7 +296,7 @@ describe Sift::Client do
241
296
  api_key = "foobar"
242
297
  response_json = score_response_json
243
298
 
244
- stub_request(:get, "https://api.siftscience.com/v204/score/247019/?api_key=foobar")
299
+ stub_request(:get, "https://api.siftscience.com/v205/score/247019/?api_key=foobar")
245
300
  .to_return(:status => 200, :body => MultiJson.dump(response_json),
246
301
  :headers => {"content-type"=>"application/json; charset=UTF-8",
247
302
  "content-length"=> "74"})
@@ -259,7 +314,7 @@ describe Sift::Client do
259
314
  api_key = "foobar"
260
315
  response_json = score_response_json
261
316
 
262
- stub_request(:get, "https://api.siftscience.com/v204/score/247019/?api_key=overridden")
317
+ stub_request(:get, "https://api.siftscience.com/v205/score/247019/?api_key=overridden")
263
318
  .to_return(:status => 200, :body => MultiJson.dump(response_json), :headers => {})
264
319
 
265
320
  response = Sift::Client.new(:api_key => api_key)
@@ -272,6 +327,88 @@ describe Sift::Client do
272
327
  end
273
328
 
274
329
 
330
+ it "Successfully executes client.get_user_score()" do
331
+ api_key = "foobar"
332
+ response_json = user_score_response_json
333
+
334
+ stub_request(:get, "https://api.siftscience.com/v205/users/247019/score?api_key=foobar")
335
+ .to_return(:status => 200, :body => MultiJson.dump(response_json),
336
+ :headers => {"content-type"=>"application/json; charset=UTF-8",
337
+ "content-length"=> "74"})
338
+
339
+ response = Sift::Client.new(:api_key => api_key).get_user_score(user_score_response_json[:entity_id])
340
+ expect(response.ok?).to eq(true)
341
+ expect(response.api_status).to eq(0)
342
+ expect(response.api_error_message).to eq("OK")
343
+
344
+ expect(response.body["entity_id"]).to eq("247019")
345
+ expect(response.body["scores"]["payment_abuse"]["score"]).to eq(0.78)
346
+ end
347
+
348
+
349
+ it "Successfully executes client.get_user_score() with abuse types specified" do
350
+ api_key = "foobar"
351
+ response_json = user_score_response_json
352
+
353
+ stub_request(:get, "https://api.siftscience.com/v205/users/247019/score" +
354
+ "?api_key=foobar&abuse_types=content_abuse,payment_abuse")
355
+ .to_return(:status => 200, :body => MultiJson.dump(response_json),
356
+ :headers => {"content-type"=>"application/json; charset=UTF-8",
357
+ "content-length"=> "74"})
358
+
359
+ response = Sift::Client.new(:api_key => api_key).get_user_score(user_score_response_json[:entity_id],
360
+ :abuse_types => ["content_abuse",
361
+ "payment_abuse"])
362
+ expect(response.ok?).to eq(true)
363
+ expect(response.api_status).to eq(0)
364
+ expect(response.api_error_message).to eq("OK")
365
+
366
+ expect(response.body["entity_id"]).to eq("247019")
367
+ expect(response.body["scores"]["payment_abuse"]["score"]).to eq(0.78)
368
+ end
369
+
370
+
371
+ it "Successfully executes client.rescore_user()" do
372
+ api_key = "foobar"
373
+ response_json = user_score_response_json
374
+
375
+ stub_request(:post, "https://api.siftscience.com/v205/users/247019/score?api_key=foobar")
376
+ .to_return(:status => 200, :body => MultiJson.dump(response_json),
377
+ :headers => {"content-type"=>"application/json; charset=UTF-8",
378
+ "content-length"=> "74"})
379
+
380
+ response = Sift::Client.new(:api_key => api_key).rescore_user(user_score_response_json[:entity_id])
381
+ expect(response.ok?).to eq(true)
382
+ expect(response.api_status).to eq(0)
383
+ expect(response.api_error_message).to eq("OK")
384
+
385
+ expect(response.body["entity_id"]).to eq("247019")
386
+ expect(response.body["scores"]["payment_abuse"]["score"]).to eq(0.78)
387
+ end
388
+
389
+
390
+ it "Successfully executes client.rescore_user() with abuse types specified" do
391
+ api_key = "foobar"
392
+ response_json = user_score_response_json
393
+
394
+ stub_request(:post, "https://api.siftscience.com/v205/users/247019/score" +
395
+ "?api_key=foobar&abuse_types=content_abuse,payment_abuse")
396
+ .to_return(:status => 200, :body => MultiJson.dump(response_json),
397
+ :headers => {"content-type"=>"application/json; charset=UTF-8",
398
+ "content-length"=> "74"})
399
+
400
+ response = Sift::Client.new(:api_key => api_key).rescore_user(user_score_response_json[:entity_id],
401
+ :abuse_types => ["content_abuse",
402
+ "payment_abuse"])
403
+ expect(response.ok?).to eq(true)
404
+ expect(response.api_status).to eq(0)
405
+ expect(response.api_error_message).to eq("OK")
406
+
407
+ expect(response.body["entity_id"]).to eq("247019")
408
+ expect(response.body["scores"]["payment_abuse"]["score"]).to eq(0.78)
409
+ end
410
+
411
+
275
412
  it "Successfully make a sync score request" do
276
413
  api_key = "foobar"
277
414
  response_json = {
@@ -280,7 +417,7 @@ describe Sift::Client do
280
417
  :score_response => score_response_json
281
418
  }
282
419
 
283
- stub_request(:post, "https://api.siftscience.com/v204/events?return_score=true")
420
+ stub_request(:post, "https://api.siftscience.com/v205/events?return_score=true")
284
421
  .to_return(:status => 200, :body => MultiJson.dump(response_json),
285
422
  :headers => {"content-type"=>"application/json; charset=UTF-8",
286
423
  "content-length"=> "74"})
@@ -304,7 +441,7 @@ describe Sift::Client do
304
441
  :score_response => action_response_json
305
442
  }
306
443
 
307
- stub_request(:post, "https://api.siftscience.com/v204/events?return_action=true")
444
+ stub_request(:post, "https://api.siftscience.com/v205/events?return_action=true")
308
445
  .to_return(:status => 200, :body => MultiJson.dump(response_json),
309
446
  :headers => {"content-type"=>"application/json; charset=UTF-8",
310
447
  "content-length"=> "74"})
@@ -332,7 +469,7 @@ describe Sift::Client do
332
469
  }
333
470
 
334
471
  stub_request(:post,
335
- "https://api.siftscience.com/v204/events?return_workflow_status=true&abuse_types=legacy,payment_abuse")
472
+ "https://api.siftscience.com/v205/events?return_workflow_status=true&return_route_info=true&abuse_types=legacy,payment_abuse")
336
473
  .to_return(:status => 200, :body => MultiJson.dump(response_json),
337
474
  :headers => {"content-type"=>"application/json; charset=UTF-8",
338
475
  "content-length"=> "74"})
@@ -341,7 +478,8 @@ describe Sift::Client do
341
478
  properties = valid_transaction_properties
342
479
  response = Sift::Client.new(:api_key => api_key)
343
480
  .track(event, properties,
344
- :return_workflow_status => true, :abuse_types => ['legacy', 'payment_abuse'])
481
+ :return_workflow_status => true, :return_route_info => true,
482
+ :abuse_types => ['legacy', 'payment_abuse'])
345
483
  expect(response.ok?).to eq(true)
346
484
  expect(response.api_status).to eq(0)
347
485
  expect(response.api_error_message).to eq("OK")
@@ -363,7 +501,7 @@ describe Sift::Client do
363
501
  end
364
502
 
365
503
 
366
- it "Successfully make a user decisions request" do
504
+ it "Successfully make a user decisions request" do
367
505
  response_text = '{"decisions":{"content_abuse":{"decision":{"id":"user_decision"},"time":1468707128659,"webhook_succeeded":false}}}'
368
506
 
369
507
  stub_request(:get, "https://foobar:@api3.siftscience.com/v3/accounts/ACCT/users/example_user/decisions")
@@ -390,4 +528,30 @@ describe Sift::Client do
390
528
  expect(response.body["decisions"]["payment_abuse"]["decision"]["id"]).to eq("decision7")
391
529
  end
392
530
 
531
+ it "Successfully make a session decisions request" do
532
+ response_text = '{"decisions":{"account_takeover":{"decision":{"id":"session_decision"},"time":1468707128659,"webhook_succeeded":false}}}'
533
+
534
+ stub_request(:get, "https://foobar:@api3.siftscience.com/v3/accounts/ACCT/users/example_user/sessions/example_session/decisions")
535
+ .to_return(:status => 200, :body => response_text, :headers => {})
536
+
537
+ client = Sift::Client.new(:api_key => "foobar", :account_id => "ACCT")
538
+ response = client.get_session_decisions("example_user", "example_session")
539
+
540
+ expect(response.ok?).to eq(true)
541
+ expect(response.body["decisions"]["account_takeover"]["decision"]["id"]).to eq("session_decision")
542
+ end
543
+
544
+ it "Successfully make an content decisions request" do
545
+ response_text = '{"decisions":{"content_abuse":{"decision":{"id":"decision7"},"time":1468599638005,"webhook_succeeded":false}}}'
546
+
547
+ stub_request(:get, "https://foobar:@api3.siftscience.com/v3/accounts/ACCT/users/USER/content/example_content/decisions")
548
+ .to_return(:status => 200, :body => response_text, :headers => {})
549
+
550
+ client = Sift::Client.new(:api_key => "foobar", :account_id => "ACCT")
551
+ response = client.get_content_decisions("USER", "example_content", :timeout => 3)
552
+
553
+ expect(response.ok?).to eq(true)
554
+ expect(response.body["decisions"]["content_abuse"]["decision"]["id"]).to eq("decision7")
555
+ end
556
+
393
557
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sift
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fred Sadaghiani
8
8
  - Yoav Schatzberg
9
9
  - Jacob Burnim
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-02-15 00:00:00.000000000 Z
13
+ date: 2022-07-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rspec
@@ -26,6 +26,20 @@ dependencies:
26
26
  - - "~>"
27
27
  - !ruby/object:Gem::Version
28
28
  version: '3.5'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rspec_junit_formatter
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
29
43
  - !ruby/object:Gem::Dependency
30
44
  name: pry
31
45
  requirement: !ruby/object:Gem::Requirement
@@ -109,8 +123,8 @@ executables: []
109
123
  extensions: []
110
124
  extra_rdoc_files: []
111
125
  files:
126
+ - ".circleci/config.yml"
112
127
  - ".gitignore"
113
- - ".travis.yml"
114
128
  - Gemfile
115
129
  - HISTORY
116
130
  - LICENSE
@@ -142,7 +156,7 @@ files:
142
156
  homepage: http://siftscience.com
143
157
  licenses: []
144
158
  metadata: {}
145
- post_install_message:
159
+ post_install_message:
146
160
  rdoc_options: []
147
161
  require_paths:
148
162
  - lib
@@ -157,9 +171,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
171
  - !ruby/object:Gem::Version
158
172
  version: '0'
159
173
  requirements: []
160
- rubyforge_project: sift
161
- rubygems_version: 2.4.6
162
- signing_key:
174
+ rubygems_version: 3.1.4
175
+ signing_key:
163
176
  specification_version: 4
164
177
  summary: Sift Science Ruby API Gem
165
178
  test_files:
data/.travis.yml DELETED
@@ -1,18 +0,0 @@
1
- language: ruby
2
- script: "bundle exec rake spec"
3
- rvm:
4
- - 1.9.3
5
- - 2.0.0
6
- - 2.1.5
7
- - 2.2.1
8
- before_install:
9
- - gem install bundler
10
- - bundle --version
11
- env:
12
- # None for now
13
- gemfile:
14
- - Gemfile
15
- services:
16
- # None for now
17
- notifications:
18
- # None for now