sift 2.2.1 → 4.1.0

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