sift 4.3.0 → 4.5.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
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