sift 4.3.0 → 4.5.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
2
  SHA256:
3
- metadata.gz: e538e799471ba36d0c73d8b6bdea81f56e5928ef018a01a8aa0d1d376151ba97
4
- data.tar.gz: 7ba73b5ea722594550ce5fb2b9259c8728b6658ac393206f991ac182e6216fd1
3
+ metadata.gz: 71be696ff04ffb1503d22a724241e5087809d2ffa2ab29f08666939ecd6bb98b
4
+ data.tar.gz: e2e841d8259fbea8c247b1b2bd3cdf208bd53ded613a765a75f595714a2e1e8d
5
5
  SHA512:
6
- metadata.gz: d6a650d190c215fa627b364e134e51d35012edd1b40682c8d24e8ea446e7bbbcfc2121189d9ef1335a5f67ee5aeb4472c42823e99737bd5879ea14a51c4f0e2c
7
- data.tar.gz: 1569ecf84bf4bfee1991a7664922293a24ab6993ac56766b28a10183cd8cc593641ff301c3e68fbdc1e8f2f51b3425b1ad64131332c12c4ded51489e54703eb3
6
+ metadata.gz: 9b578f6bccf6323d4ef53947cea5a517678920d882439da1d1df262cf5eb39e45064aab3a1dee702da01f41f29a522df0c5e7b850c68e1ac302fa0f7d9830e29
7
+ data.tar.gz: 3c4565ffc5f98626ac67a07b378adfd80aaef787e47ffc50d84780da4b62788bdfb43e1b2e4ed90ca59cb4ee0fe23ec727dad8d1c0dd9413c137d7ec1de95510
data/.circleci/config.yml CHANGED
@@ -81,8 +81,25 @@ jobs:
81
81
  - slack/notify:
82
82
  <<: *slack_notify
83
83
 
84
+ run_integration_tests:
85
+ docker:
86
+ - image: circleci/ruby:2.4.2-jessie-node
87
+ steps:
88
+ - checkout
89
+ - run:
90
+ name: Install bundle and run the tests
91
+ command: |
92
+ bundle check || bundle install
93
+ bundle exec ruby test_integration_app/main.rb
94
+
84
95
  workflows:
85
96
  ruby-test:
86
97
  jobs:
87
98
  - build:
88
99
  context: *context
100
+ ruby-integration-tests:
101
+ jobs:
102
+ - run_integration_tests:
103
+ filters:
104
+ branches:
105
+ only: master
@@ -12,18 +12,14 @@ jobs:
12
12
  steps:
13
13
  - name: Checkout code
14
14
  uses: actions/checkout@v3
15
-
16
-
17
15
  - name: Set up Ruby
18
16
  uses: ruby/setup-ruby@v1
19
17
  with:
20
18
  ruby-version: 2.7
21
-
22
19
  - name: Install Bundler
23
20
  run: |
24
21
  sudo gem install bundler
25
22
  bundle install
26
-
27
23
  - name: Build and push gem
28
24
  run: |
29
25
  mkdir -p $HOME/.gem
@@ -37,14 +33,6 @@ jobs:
37
33
  gem build sift.gemspec
38
34
  gem push sift-$version.gem
39
35
  rm -rf $HOME/.gem
40
- ls -la $HOME/
41
36
  else
42
37
  echo "Gem version $version exists on RubyGems"
43
38
  fi
44
-
45
- - name: Run Rake tasks
46
- run: |
47
- bundle exec rake -T
48
- bundle exec rake build
49
- bundle exec rake install
50
- bundle exec rake release
@@ -0,0 +1,103 @@
1
+ // Load Jenkins shared library
2
+ jenkinsBranch = 'v0.37.0'
3
+ sharedLib = library("shared-lib@${jenkinsBranch}")
4
+
5
+ def siftRubyWorkflow = sharedLib.com.sift.ci.SiftRubyWorkflow.new()
6
+ def ciUtil = sharedLib.com.sift.ci.CIUtil.new()
7
+ def stackdriver = sharedLib.com.sift.ci.StackDriverMetrics.new()
8
+
9
+ // Default GitHub status context for automatically triggered builds
10
+ def defaultStatusContext = 'Jenkins:auto'
11
+
12
+ // Pod template file for Jenkins agent pod
13
+ // Pod template yaml file is defined in https://github.com/SiftScience/jenkins/tree/master/resources/jenkins-k8s-pod-templates
14
+ def ruby2PodTemplateFile = 'ruby-2-4-2-pod-template.yaml'
15
+ def ruby2PodLabel = "ruby2-${BUILD_TAG}"
16
+
17
+
18
+ // GitHub repo name
19
+ def repoName = 'sift-ruby'
20
+
21
+ pipeline {
22
+ agent none
23
+ options {
24
+ timestamps()
25
+ skipDefaultCheckout()
26
+ disableConcurrentBuilds()
27
+ disableRestartFromStage()
28
+ parallelsAlwaysFailFast()
29
+ buildDiscarder logRotator(artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '30', numToKeepStr: '')
30
+ timeout(time: 1, unit: 'HOURS')
31
+ }
32
+ environment {
33
+ GIT_BRANCH = "${env.CHANGE_BRANCH != null? env.CHANGE_BRANCH : env.BRANCH_NAME}"
34
+ }
35
+ stages {
36
+ stage('Initialize') {
37
+ steps {
38
+ script {
39
+ statusContext = defaultStatusContext
40
+ // Get the commit sha for the build
41
+ commitSha = ciUtil.commitHashForBuild()
42
+ ciUtil.updateGithubCommitStatus(repoName, statusContext, 'Started', 'pending', commitSha)
43
+ }
44
+ }
45
+ }
46
+ stage ('Build and Test Workflows') {
47
+ steps {
48
+ script {
49
+ def workflows = [:]
50
+ def stage1 = 'Run Integration Tests - ruby'
51
+ workflows[stage1] = {
52
+ stage(stage1) {
53
+ if (env.GIT_BRANCH.equals('master')) {
54
+ ciUtil.updateGithubCommitStatus(repoName, stage1, 'Started', 'pending', commitSha)
55
+ try {
56
+ siftRubyWorkflow.runSiftRubyIntegration(ruby2PodTemplateFile, ruby2PodLabel)
57
+ ciUtil.updateGithubCommitStatus(repoName, stage1, 'SUCCESS', 'success', commitSha)
58
+ } catch (Exception e) {
59
+ ciUtil.updateGithubCommitStatus(repoName, stage1, 'FAILURE', 'failure', commitSha)
60
+ print("${stage1} failed")
61
+ throw e
62
+ }
63
+ }
64
+ }
65
+ }
66
+ def stage2 = 'Test - ruby'
67
+ workflows[stage2] = {
68
+ stage(stage2) {
69
+ ciUtil.updateGithubCommitStatus(repoName, stage2, 'Started', 'pending', commitSha)
70
+ try {
71
+ siftRubyWorkflow.runSiftRubyTest(ruby2PodTemplateFile, ruby2PodLabel)
72
+ ciUtil.updateGithubCommitStatus(repoName, stage2, 'SUCCESS', 'success', commitSha)
73
+ } catch (Exception e) {
74
+ ciUtil.updateGithubCommitStatus(repoName, stage2, 'FAILURE', 'failure', commitSha)
75
+ print("${stage2} failed")
76
+ throw e
77
+ }
78
+ }
79
+ }
80
+ parallel workflows
81
+ }
82
+ }
83
+ }
84
+ }
85
+ post {
86
+ success {
87
+ script {
88
+ ciUtil.updateGithubCommitStatus(repoName, statusContext, currentBuild.currentResult, 'success', commitSha)
89
+ }
90
+ }
91
+ unsuccessful {
92
+ script {
93
+ ciUtil.updateGithubCommitStatus(repoName, statusContext, currentBuild.currentResult, 'failure', commitSha)
94
+ ciUtil.notifySlack(repoName, commitSha)
95
+ }
96
+ }
97
+ always {
98
+ script {
99
+ stackdriver.updatePipelineStatistics(this)
100
+ }
101
+ }
102
+ }
103
+ }
data/HISTORY CHANGED
@@ -1,3 +1,9 @@
1
+ === 4.5.0 2024-05-16
2
+ - Support for warnings in Events API
3
+
4
+ === 4.4.0 2023-10-05
5
+ - Score percentiles in Score API
6
+
1
7
  === 4.3.0 2023-08-21
2
8
  - PSP Merchant Management API
3
9
 
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  # sift-ruby
2
- [![CircleCI](https://circleci.com/gh/SiftScience/sift-ruby.svg?style=svg)](https://circleci.com/gh/SiftScience/sift-ruby)
3
2
 
4
3
  The official Ruby bindings for the latest version (v205) of the [Sift API](https://sift.com/developers/docs/java/apis-overview).
5
4
 
@@ -39,8 +38,22 @@ client = Sift::Client.new(api_key: '<your_api_key_here>', account_id: '<your_acc
39
38
 
40
39
  ```
41
40
 
42
- ### Sending a transaction event
41
+ ### Sending an event
42
+ Send event to Sift.
43
+ To learn more about the Events API visit our [developer docs](https://developers.sift.com/docs/ruby/events-api/overview).
43
44
 
45
+
46
+ **Optional Params**
47
+ - `return_score`: `:true` or `:false`
48
+ - `return_action`: `:true` or `:false`
49
+ - `return_workflow_status`: `:true` or `:false`
50
+ - `return_route_info`: `:true` or `:false`
51
+ - `force_workflow_run`: `:true` or `:false`
52
+ - `include_score_percentiles`: `:true` or `:false`
53
+ - `warnings`: `:true` or `:false`
54
+ - `abuse_types`: `["payment_abuse", "content_abuse", "content_abuse", "account_abuse", "legacy", "account_takeover"]`
55
+
56
+ **Example:**
44
57
  ```ruby
45
58
  event = "$transaction"
46
59
 
@@ -318,3 +331,21 @@ To run the various tests use the rake command as follows:
318
331
  ```ruby
319
332
  $ rake spec
320
333
  ```
334
+
335
+ ## Integration testing app
336
+
337
+ For testing the app with real calls it is possible to run the integration testing app,
338
+ it makes calls to almost all our public endpoints to make sure the library integrates
339
+ well. At the moment, the app is run on every merge to master
340
+
341
+ #### How to run it locally
342
+
343
+ 1. Add env variable `ACCOUNT_ID` with the valid account id
344
+ 2. Add env variable `API_KEY` with the valid Api Key associated from the account
345
+ 3. Run the following under the project root folder
346
+ ```
347
+ # Install the budle locally
348
+ bundle check || bundle install
349
+ # Run the app
350
+ bundle exec ruby test_integration_app/main.rb
351
+ ```
data/lib/sift/client.rb CHANGED
@@ -201,6 +201,12 @@ module Sift
201
201
  #
202
202
  # :path::
203
203
  # Overrides the URI path for this API call.
204
+ #
205
+ # :include_score_percentiles::
206
+ # include_score_percentiles(optional) : Whether to add new parameter in the query parameter.
207
+ #
208
+ # :warnings::
209
+ # warnings(optional) : Whether to add list of warnings (if any) to response.
204
210
  #
205
211
  # ==== Returns:
206
212
  #
@@ -220,6 +226,7 @@ module Sift
220
226
  force_workflow_run = opts[:force_workflow_run]
221
227
  abuse_types = opts[:abuse_types]
222
228
  include_score_percentiles = opts[:include_score_percentiles]
229
+ warnings = opts[:warnings]
223
230
 
224
231
  raise("event must be a non-empty string") if (!event.is_a? String) || event.empty?
225
232
  raise("properties cannot be empty") if properties.empty?
@@ -232,8 +239,12 @@ module Sift
232
239
  query["return_route_info"] = "true" if return_route_info
233
240
  query["force_workflow_run"] = "true" if force_workflow_run
234
241
  query["abuse_types"] = abuse_types.join(",") if abuse_types
235
- if include_score_percentiles == "true"
236
- query["fields"] = "SCORE_PERCENTILES"
242
+
243
+ if include_score_percentiles == "true" || warnings == "true"
244
+ fields = []
245
+ fields << "SCORE_PERCENTILES" if include_score_percentiles == "true"
246
+ fields << "WARNINGS" if warnings == "true"
247
+ query["fields"] = fields.join(",")
237
248
  end
238
249
 
239
250
  options = {
@@ -275,6 +286,9 @@ module Sift
275
286
  #
276
287
  # :version::
277
288
  # Overrides the version of the Events API to call.
289
+ #
290
+ # :include_score_percentiles::
291
+ # include_score_percentiles(optional) : Whether to add new parameter in the query parameter.
278
292
  #
279
293
  # ==== Returns:
280
294
  #
@@ -286,6 +300,7 @@ module Sift
286
300
  api_key = opts[:api_key] || @api_key
287
301
  timeout = opts[:timeout] || @timeout
288
302
  version = opts[:version] || @version
303
+ include_score_percentiles = opts[:include_score_percentiles]
289
304
 
290
305
  raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
291
306
  raise("Bad api_key parameter") if api_key.empty?
@@ -293,6 +308,9 @@ module Sift
293
308
  query = {}
294
309
  query["api_key"] = api_key
295
310
  query["abuse_types"] = abuse_types.join(",") if abuse_types
311
+ if include_score_percentiles == "true"
312
+ query["fields"] = "SCORE_PERCENTILES"
313
+ end
296
314
 
297
315
  options = {
298
316
  :headers => {"User-Agent" => user_agent},
@@ -332,6 +350,9 @@ module Sift
332
350
  #
333
351
  # :timeout::
334
352
  # Overrides the timeout (in seconds) for this call.
353
+ #
354
+ # :include_score_percentiles::
355
+ # include_score_percentiles(optional) : Whether to add new parameter in the query parameter.
335
356
  #
336
357
  # ==== Returns:
337
358
  #
@@ -342,6 +363,7 @@ module Sift
342
363
  abuse_types = opts[:abuse_types]
343
364
  api_key = opts[:api_key] || @api_key
344
365
  timeout = opts[:timeout] || @timeout
366
+ include_score_percentiles = opts[:include_score_percentiles]
345
367
 
346
368
  raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
347
369
  raise("Bad api_key parameter") if api_key.empty?
@@ -349,6 +371,9 @@ module Sift
349
371
  query = {}
350
372
  query["api_key"] = api_key
351
373
  query["abuse_types"] = abuse_types.join(",") if abuse_types
374
+ if include_score_percentiles == "true"
375
+ query["fields"] = "SCORE_PERCENTILES"
376
+ end
352
377
 
353
378
  options = {
354
379
  :headers => {"User-Agent" => user_agent},
data/lib/sift/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Sift
2
- VERSION = "4.3.0"
2
+ VERSION = "4.5.0"
3
3
  API_VERSION = "205"
4
4
  end
@@ -110,6 +110,69 @@ describe Sift::Client do
110
110
  }
111
111
  end
112
112
 
113
+ def warnings
114
+ {
115
+ :count => 1,
116
+ :items => [{
117
+ :message => 'Invalid currency'
118
+ }]
119
+ }
120
+ end
121
+
122
+ def percentile_response_json
123
+ {
124
+ :user_id => 'billy_jones_301',
125
+ :latest_labels => {},
126
+ :workflow_statuses => [],
127
+ :scores => {
128
+ :account_abuse => {
129
+ :score => 0.32787917675535705,
130
+ :reasons => [{
131
+ :name => 'Latest item product title',
132
+ :value => 'The Slanket Blanket-Texas Tea'
133
+ }],
134
+ :percentiles => {
135
+ :last_7_days => -1.0, :last_1_days => -1.0, :last_10_days => -1.0, :last_5_days => -1.0
136
+ }
137
+ },
138
+ :acontent_abuse => {
139
+ :score => 0.28056292905897995,
140
+ :reasons => [{
141
+ :name => 'timeSinceFirstEvent',
142
+ :value => '13.15 minutes'
143
+ }],
144
+ :percentiles => {
145
+ :last_7_days => -1.0, :last_1_days => -1.0, :last_10_days => -1.0, :last_5_days => -1.0
146
+ }
147
+ },
148
+ :payment_abuse => {
149
+ :score => 0.28610507028376797,
150
+ :reasons => [{
151
+ :name => 'Latest item currency code',
152
+ :value => 'USD'
153
+ }, {
154
+ :name => 'Latest item item ID',
155
+ :value => 'B004834GQO'
156
+ }, {
157
+ :name => 'Latest item product title',
158
+ :value => 'The Slanket Blanket-Texas Tea'
159
+ }],
160
+ :percentiles => {
161
+ :last_7_days => -1.0, :last_1_days => -1.0, :last_10_days => -1.0, :last_5_days => -1.0
162
+ }
163
+ },
164
+ :promotion_abuse => {
165
+ :score => 0.05731508921450917,
166
+ :percentiles => {
167
+ :last_7_days => -1.0, :last_1_days => -1.0, :last_10_days => -1.0, :last_5_days => -1.0
168
+ }
169
+ }
170
+ },
171
+ :status => 0,
172
+ :error_message => 'OK'
173
+ }
174
+ end
175
+
113
176
  def fully_qualified_api_endpoint
114
177
  Sift::Client::API_ENDPOINT + Sift.rest_api_path
115
178
  end
@@ -554,4 +617,106 @@ describe Sift::Client do
554
617
  expect(response.body["decisions"]["content_abuse"]["decision"]["id"]).to eq("decision7")
555
618
  end
556
619
 
620
+ it "Successfully submits a v205 event with SCORE_PERCENTILES" do
621
+ response_json =
622
+ { :status => 0, :error_message => "OK", :score_response => percentile_response_json}
623
+ stub_request(:post, "https://api.siftscience.com/v205/events?fields=SCORE_PERCENTILES&return_score=true").
624
+ with { | request|
625
+ parsed_body = JSON.parse(request.body)
626
+ expect(parsed_body).to include("$api_key" => "overridden")
627
+ }.to_return(:status => 200, :body => MultiJson.dump(response_json), :headers => {})
628
+
629
+ api_key = "foobar"
630
+ event = "$transaction"
631
+ properties = valid_transaction_properties
632
+
633
+ response = Sift::Client.new(:api_key => api_key, :version => "205")
634
+ .track(event, properties, :api_key => "overridden", :include_score_percentiles => "true", :return_score => "true")
635
+ expect(response.ok?).to eq(true)
636
+ expect(response.api_status).to eq(0)
637
+ expect(response.api_error_message).to eq("OK")
638
+ expect(response.body["score_response"]["scores"]["account_abuse"]["percentiles"]["last_7_days"]).to eq(-1.0)
639
+ end
640
+
641
+ it "Successfully submits a v205 event with SCORE_PERCENTILES" do
642
+ response_json =
643
+ { :status => 0, :error_message => "OK", :score_response => percentile_response_json}
644
+ stub_request(:post, "https://api.siftscience.com/v205/events?fields=SCORE_PERCENTILES&return_score=true").
645
+ with { | request|
646
+ parsed_body = JSON.parse(request.body)
647
+ expect(parsed_body).to include("$api_key" => "overridden")
648
+ }.to_return(:status => 200, :body => MultiJson.dump(response_json), :headers => {})
649
+
650
+ api_key = "foobar"
651
+ event = "$transaction"
652
+ properties = valid_transaction_properties
653
+
654
+ response = Sift::Client.new(:api_key => api_key, :version => "205")
655
+ .track(event, properties, :api_key => "overridden", :include_score_percentiles => "true", :return_score => "true")
656
+ expect(response.ok?).to eq(true)
657
+ expect(response.api_status).to eq(0)
658
+ expect(response.api_error_message).to eq("OK")
659
+ expect(response.body["score_response"]["scores"]["account_abuse"]["percentiles"]["last_7_days"]).to eq(-1.0)
660
+ end
661
+
662
+ it "Successfully fetches a v205 score with SCORE_PERCENTILES" do
663
+
664
+ api_key = "foobar"
665
+ response_json = score_response_json
666
+
667
+ stub_request(:get, "https://api.siftscience.com/v205/score/247019/?api_key=foobar&fields=SCORE_PERCENTILES")
668
+ .to_return(:status => 200, :body => MultiJson.dump(response_json),
669
+ :headers => {"content-type"=>"application/json; charset=UTF-8",
670
+ "content-length"=> "74"})
671
+
672
+ response = Sift::Client.new(:api_key => api_key)
673
+ .score(score_response_json[:user_id], :version => 205, :include_score_percentiles => "true")
674
+ expect(response.ok?).to eq(true)
675
+ expect(response.api_status).to eq(0)
676
+ expect(response.api_error_message).to eq("OK")
677
+
678
+ expect(response.body["score"]).to eq(0.93)
679
+ end
680
+
681
+ it "Successfully executes client.get_user_score() with SCORE_PERCENTILES" do
682
+
683
+ api_key = "foobar"
684
+ response_json = user_score_response_json
685
+
686
+ stub_request(:get, "https://api.siftscience.com/v205/users/247019/score?api_key=foobar&fields=SCORE_PERCENTILES")
687
+ .to_return(:status => 200, :body => MultiJson.dump(response_json),
688
+ :headers => {"content-type"=>"application/json; charset=UTF-8",
689
+ "content-length"=> "74"})
690
+
691
+ response = Sift::Client.new(:api_key => api_key)
692
+ .get_user_score(user_score_response_json[:entity_id], :include_score_percentiles => "true")
693
+ expect(response.ok?).to eq(true)
694
+ expect(response.api_status).to eq(0)
695
+ expect(response.api_error_message).to eq("OK")
696
+
697
+ expect(response.body["entity_id"]).to eq("247019")
698
+ expect(response.body["scores"]["payment_abuse"]["score"]).to eq(0.78)
699
+ end
700
+
701
+ it "Successfully submits a v205 event with WARNINGS" do
702
+ response_json =
703
+ { :status => 0, :error_message => "OK", :warnings => warnings}
704
+ stub_request(:post, "https://api.siftscience.com/v205/events?fields=WARNINGS").
705
+ with { | request|
706
+ parsed_body = JSON.parse(request.body)
707
+ expect(parsed_body).to include("$api_key" => "overridden")
708
+ }.to_return(:status => 200, :body => MultiJson.dump(response_json), :headers => {})
709
+
710
+ api_key = "foobar"
711
+ event = "$transaction"
712
+ properties = valid_transaction_properties
713
+
714
+ response = Sift::Client.new(:api_key => api_key, :version => "205")
715
+ .track(event, properties, :api_key => "overridden",:warnings => "true")
716
+ expect(response.ok?).to eq(true)
717
+ expect(response.api_status).to eq(0)
718
+ expect(response.api_error_message).to eq("OK")
719
+ expect(response.body["warnings"]["count"]).to eq(1)
720
+ expect(response.body["warnings"]["items"][0]["message"]).to eq("Invalid currency")
721
+ end
557
722
  end
@@ -0,0 +1,31 @@
1
+ require "sift"
2
+
3
+ class DecisionAPI
4
+
5
+ @@client = Sift::Client.new(:api_key => ENV["API_KEY"], :account_id => ENV["ACCOUNT_ID"])
6
+
7
+ def apply_user_decision()
8
+ properties = {
9
+ "decision_id": "integration_app_watch_account_abuse",
10
+ "description": "User linked to three other payment abusers and ordering high value items",
11
+ "source": "manual_review",
12
+ "analyst": "analyst@example.com",
13
+ "user_id": "userId"
14
+ }
15
+
16
+ return @@client.apply_decision(properties)
17
+ end
18
+
19
+ def apply_order_decision()
20
+ properties = {
21
+ "decision_id": "block_order_payment_abuse",
22
+ "description": "applied via the high priority queue, queued user because their risk score exceeded 85",
23
+ "source": "AUTOMATED_RULE",
24
+ "user_id": "userId",
25
+ "order_id": "orderId"
26
+ }
27
+
28
+ return @@client.apply_decision(properties)
29
+ end
30
+
31
+ end