sift 1.1.5 → 1.1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +9 -9
- data/README.rdoc +52 -7
- data/lib/sift.rb +7 -2
- data/lib/sift/client.rb +49 -17
- data/lib/sift/version.rb +2 -1
- data/sift.gemspec +5 -6
- data/spec/unit/client_spec.rb +67 -16
- metadata +11 -24
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YzY5Mjg2YTQ4ZjRmYWRlOTQ4NTk4NzFmNjNiZjFmYmE3NTg3MDM2MQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
7
|
-
|
6
|
+
ZGIzOTAwOWE4OTExMWQ0MzlkYTRiYjk4OTQzNmU0NzQ5YWVkMzg3OQ==
|
7
|
+
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NTM1ZTM3ZGU3ODI4ZmE5YzdiMTQ4ZWMxODUxZDhjZTAzZmZiYTAzMGZjMzZk
|
10
|
+
YzYxZjI5MDk1MjQ4ZWI1NDFhNWI1ODY2Y2MyYjk1MTI3YTA4ZjdmZjhjOTNl
|
11
|
+
NTc0ZGYzYzM5NmI0N2ZiM2JmMzgzMDg5M2ZhZWIwYjQwNWY1MGQ=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NzlhNGRiODU4M2UzNWQzZGRkM2U2OGM4MTk5MWVkZmE4ZjQ1YmJkYzcxZjI2
|
14
|
+
OWY1OGI5OWRjMzA4ODM1YWVkZGE4MGVhMzBhMjRkNDg3M2ZlMzNhY2M0YTQ4
|
15
|
+
OWJkNjkwN2EzNzIyOTRjMGNhOGNjNzIyMWYzNDdmN2Y3OWQ4ZTE=
|
data/README.rdoc
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
= Sift Science Ruby bindings {<img src="https://travis-ci.org/SiftScience/sift-ruby.png?branch=master" alt="Build Status" />}[https://travis-ci.org/SiftScience/sift-ruby]
|
2
2
|
|
3
|
+
== Requirements
|
4
|
+
|
5
|
+
* Ruby 1.8.7 or above. (Ruby 1.8.6 might work if you load ActiveSupport.)
|
6
|
+
* HTTParty, 0.11.0 or greater
|
7
|
+
* Multi Json, 1.0 or greater
|
8
|
+
|
9
|
+
For development only:
|
10
|
+
* bundler
|
11
|
+
* rspec, 2.14.1 or greater
|
12
|
+
* webmock, 1.16 or greater
|
13
|
+
* rake, any version
|
14
|
+
|
3
15
|
== Installation
|
4
16
|
|
5
17
|
If you want to build the gem from source:
|
@@ -9,16 +21,49 @@ If you want to build the gem from source:
|
|
9
21
|
Alternatively, you can install the gem from Rubyforge:
|
10
22
|
|
11
23
|
$ gem install sift
|
24
|
+
|
25
|
+
== Usage
|
26
|
+
require "sift"
|
12
27
|
|
13
|
-
|
28
|
+
Sift.api_key = '<your_api_key_here>'
|
29
|
+
client = Sift::Client.new()
|
30
|
+
|
31
|
+
# send a transaction event -- note this is blocking
|
32
|
+
event = "$transaction"
|
33
|
+
|
34
|
+
user_id = "23056" # User ID's may only contain a-z, A-Z, 0-9, =, ., -, _, +, @, :, &, ^, %, !, $
|
14
35
|
|
15
|
-
|
16
|
-
|
36
|
+
properties = {
|
37
|
+
"$user_id" => user_id,
|
38
|
+
"$user_email" => "buyer@gmail.com",
|
39
|
+
"$seller_user_id" => "2371",
|
40
|
+
"seller_user_email" => "seller@gmail.com",
|
41
|
+
"$transaction_id" => "573050",
|
42
|
+
"$payment_method" => {
|
43
|
+
"$payment_type" => "$credit_card",
|
44
|
+
"$payment_gateway" => "$braintree",
|
45
|
+
"$card_bin" => "542486",
|
46
|
+
"$card_last4" => "4444"
|
47
|
+
},
|
48
|
+
"$currency_code" => "USD",
|
49
|
+
"$amount" => 15230000,
|
50
|
+
}
|
51
|
+
|
52
|
+
response = client.track(event, properties)
|
53
|
+
|
54
|
+
response.ok? # returns true or false
|
55
|
+
|
56
|
+
response.http_status_code # HTTP response code, 200 is ok.
|
17
57
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
58
|
+
response.api_status # status field in the return body, Link to Error Codes
|
59
|
+
|
60
|
+
response.api_error_message # Error message associated with status Error Code
|
61
|
+
|
62
|
+
# Request a score forthe user with user_id 23056
|
63
|
+
response = client.score(user_id)
|
64
|
+
|
65
|
+
# Label the user with user_id 23056 as Bad with all optional fields
|
66
|
+
response = client.label(user_id,{ "$is_bad" => true, "$reasons" => ["$chargeback", ], "$description" => "Chargeback issued", "$source" => "Manual Review", "$analyst" => "analyst.name@your_domain.com"})
|
22
67
|
|
23
68
|
== Building
|
24
69
|
|
data/lib/sift.rb
CHANGED
@@ -5,12 +5,17 @@ module Sift
|
|
5
5
|
|
6
6
|
# Returns the path for the current API version
|
7
7
|
def self.current_rest_api_path
|
8
|
-
"/
|
8
|
+
"/v#{API_VERSION}/events"
|
9
9
|
end
|
10
10
|
|
11
11
|
def self.current_users_label_api_path(user_id)
|
12
12
|
# This API version is a minor version ahead of the /events API
|
13
|
-
"/
|
13
|
+
"/v#{API_VERSION}/users/#{URI.encode(user_id)}/labels"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Adding module scoped public API key
|
17
|
+
class << self
|
18
|
+
attr_accessor :api_key
|
14
19
|
end
|
15
20
|
|
16
21
|
# Sets the Output logger to use within the client. This can be left uninitializaed
|
data/lib/sift/client.rb
CHANGED
@@ -6,11 +6,11 @@ module Sift
|
|
6
6
|
# Represents the payload returned from a call through the track API
|
7
7
|
#
|
8
8
|
class Response
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :body
|
10
10
|
attr_reader :http_status_code
|
11
11
|
attr_reader :api_status
|
12
12
|
attr_reader :api_error_message
|
13
|
-
attr_reader :
|
13
|
+
attr_reader :request
|
14
14
|
|
15
15
|
# Constructor
|
16
16
|
#
|
@@ -21,11 +21,11 @@ module Sift
|
|
21
21
|
# sections.
|
22
22
|
#
|
23
23
|
def initialize(http_response, http_response_code)
|
24
|
-
@
|
25
|
-
@
|
24
|
+
@body = MultiJson.load(http_response)
|
25
|
+
@request = MultiJson.load(@body["request"].to_s) if @body["request"]
|
26
26
|
@http_status_code = http_response_code
|
27
|
-
@api_status = @
|
28
|
-
@api_error_message = @
|
27
|
+
@api_status = @body["status"].to_i
|
28
|
+
@api_error_message = @body["error_message"].to_s
|
29
29
|
end
|
30
30
|
|
31
31
|
# Helper method returns true if and only if the response from the API call was
|
@@ -36,6 +36,19 @@ module Sift
|
|
36
36
|
def ok?
|
37
37
|
0 == @api_status.to_i
|
38
38
|
end
|
39
|
+
|
40
|
+
|
41
|
+
# DEPRECIATED
|
42
|
+
# Getter method for depreciated 'json' member variable.
|
43
|
+
def json
|
44
|
+
@body
|
45
|
+
end
|
46
|
+
|
47
|
+
# DEPRECIATED
|
48
|
+
# Getter method for depreciated 'original_request' member variable.
|
49
|
+
def original_request
|
50
|
+
@request
|
51
|
+
end
|
39
52
|
end
|
40
53
|
|
41
54
|
# This class wraps accesses through the API
|
@@ -46,7 +59,6 @@ module Sift
|
|
46
59
|
|
47
60
|
include HTTParty
|
48
61
|
base_uri API_ENDPOINT
|
49
|
-
default_timeout API_TIMEOUT
|
50
62
|
|
51
63
|
# Constructor
|
52
64
|
#
|
@@ -57,10 +69,22 @@ module Sift
|
|
57
69
|
# path
|
58
70
|
# The path to the event API, e.g., "/v201/events"
|
59
71
|
#
|
60
|
-
def initialize(api_key, path = Sift.current_rest_api_path)
|
61
|
-
|
72
|
+
def initialize(api_key = Sift.api_key, path = Sift.current_rest_api_path, timeout = API_TIMEOUT)
|
73
|
+
raise(RuntimeError, "api_key must be a non-empty string") if (!api_key.is_a? String) || api_key.empty?
|
74
|
+
raise(RuntimeError, "path must be a non-empty string") if (!path.is_a? String) || path.empty?
|
75
|
+
@api_key = api_key
|
62
76
|
@path = path
|
63
|
-
|
77
|
+
@timeout = timeout
|
78
|
+
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def api_key
|
83
|
+
@api_key
|
84
|
+
end
|
85
|
+
|
86
|
+
def user_agent
|
87
|
+
"SiftScience/v#{API_VERSION} sift-ruby/#{VERSION}"
|
64
88
|
end
|
65
89
|
|
66
90
|
# Tracks an event and associated properties through the Sift Science API. This call
|
@@ -94,13 +118,17 @@ module Sift
|
|
94
118
|
# result, though.
|
95
119
|
#
|
96
120
|
def track(event, properties = {}, timeout = nil, path = nil, return_score = false)
|
97
|
-
raise(RuntimeError, "event must be a string") if event.
|
121
|
+
raise(RuntimeError, "event must be a non-empty string") if (!event.is_a? String) || event.empty?
|
98
122
|
raise(RuntimeError, "properties cannot be empty") if properties.empty?
|
99
123
|
path ||= @path
|
100
|
-
|
124
|
+
timeout ||= @timeout
|
125
|
+
if return_score
|
126
|
+
path = path + "?return_score=true"
|
127
|
+
end
|
101
128
|
options = {
|
102
129
|
:body => MultiJson.dump(delete_nils(properties).merge({"$type" => event,
|
103
|
-
"$api_key" => @api_key}))
|
130
|
+
"$api_key" => @api_key})),
|
131
|
+
:headers => {"User-Agent" => user_agent}
|
104
132
|
}
|
105
133
|
options.merge!(:timeout => timeout) unless timeout.nil?
|
106
134
|
begin
|
@@ -125,11 +153,15 @@ module Sift
|
|
125
153
|
# A Response object is returned and captures the status message and
|
126
154
|
# status code. In general, you can ignore the returned result, though.
|
127
155
|
#
|
128
|
-
def score(user_id)
|
156
|
+
def score(user_id, timeout = nil)
|
157
|
+
|
158
|
+
raise(RuntimeError, "user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
|
159
|
+
timetout ||= @timeout
|
129
160
|
|
130
|
-
|
161
|
+
options = { :headers => {"User-Agent" => user_agent} }
|
162
|
+
options.merge!(:timeout => timeout) unless timeout.nil?
|
131
163
|
|
132
|
-
response = self.class.get("/
|
164
|
+
response = self.class.get("/v#{API_VERSION}/score/#{user_id}/?api_key=#{@api_key}", options)
|
133
165
|
Response.new(response.body, response.code)
|
134
166
|
|
135
167
|
end
|
@@ -155,7 +187,7 @@ module Sift
|
|
155
187
|
#
|
156
188
|
def label(user_id, properties = {}, timeout = nil)
|
157
189
|
|
158
|
-
raise(RuntimeError, "user_id must be a string") if user_id.
|
190
|
+
raise(RuntimeError, "user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
|
159
191
|
|
160
192
|
path = Sift.current_users_label_api_path(user_id)
|
161
193
|
track("$label", delete_nils(properties), timeout, path)
|
data/lib/sift/version.rb
CHANGED
data/sift.gemspec
CHANGED
@@ -6,8 +6,8 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "sift"
|
7
7
|
s.version = Sift::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.authors = ["Fred Sadaghiani"]
|
10
|
-
s.email = ["
|
9
|
+
s.authors = ["Fred Sadaghiani", "Yoav Schatzberg"]
|
10
|
+
s.email = ["support@siftscience.com"]
|
11
11
|
s.homepage = "http://siftscience.com"
|
12
12
|
s.summary = %q{Sift Science Ruby API Gem}
|
13
13
|
s.description = %q{Sift Science Ruby API. Please see http://siftscience.com for more details.}
|
@@ -20,13 +20,12 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
22
|
# Gems that must be intalled for sift to compile and build
|
23
|
-
s.add_development_dependency "rspec", "
|
24
|
-
s.add_development_dependency "webmock", "
|
23
|
+
s.add_development_dependency "rspec", ">=2.14.1"
|
24
|
+
s.add_development_dependency "webmock", ">= 1.16.0"
|
25
25
|
|
26
26
|
# Gems that must be intalled for sift to work
|
27
27
|
s.add_dependency "httparty", ">= 0.11.0"
|
28
|
-
s.add_dependency "multi_json", "
|
28
|
+
s.add_dependency "multi_json", ">= 1.0"
|
29
29
|
|
30
|
-
s.add_development_dependency("rspec", ">= 2.0")
|
31
30
|
s.add_development_dependency("rake")
|
32
31
|
end
|
data/spec/unit/client_spec.rb
CHANGED
@@ -2,6 +2,10 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
|
|
2
2
|
|
3
3
|
describe Sift::Client do
|
4
4
|
|
5
|
+
before :each do
|
6
|
+
Sift.api_key = nil
|
7
|
+
end
|
8
|
+
|
5
9
|
def valid_transaction_properties
|
6
10
|
{
|
7
11
|
:$buyer_user_id => "123456",
|
@@ -22,13 +26,53 @@ describe Sift::Client do
|
|
22
26
|
}
|
23
27
|
end
|
24
28
|
|
29
|
+
def score_response_json
|
30
|
+
{
|
31
|
+
:user_id => "247019",
|
32
|
+
:score => 0.93,
|
33
|
+
:reasons => [{
|
34
|
+
:name => "UsersPerDevice",
|
35
|
+
:value => 4,
|
36
|
+
:details => {
|
37
|
+
:users => "a, b, c, d"
|
38
|
+
}
|
39
|
+
}],
|
40
|
+
:status => 0,
|
41
|
+
:error_message => "OK"
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
25
45
|
def fully_qualified_api_endpoint
|
26
46
|
Sift::Client::API_ENDPOINT + Sift.current_rest_api_path
|
27
47
|
end
|
28
48
|
|
29
|
-
it "
|
49
|
+
it "Can instantiate client with blank api key if Sift.api_key set" do
|
50
|
+
Sift.api_key = "test_global_api_key"
|
51
|
+
Sift::Client.new().api_key.should eq(Sift.api_key)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "Parameter passed api key takes precedence over Sift.api_key" do
|
55
|
+
Sift.api_key = "test_global_api_key"
|
56
|
+
api_key = "test_local_api_key"
|
57
|
+
Sift::Client.new(api_key).api_key.should eq(api_key)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "Cannot instantiate client with nil, empty, non-string, or blank api key" do
|
30
61
|
lambda { Sift::Client.new(nil) }.should raise_error
|
31
62
|
lambda { Sift::Client.new("") }.should raise_error
|
63
|
+
lambda { Sift::Client.new(123456) }.should raise_error
|
64
|
+
lambda { Sift::Client.new() }.should raise_error
|
65
|
+
end
|
66
|
+
|
67
|
+
it "Cannot instantiate client with nil, empty, non-string, or blank path" do
|
68
|
+
api_key = "test_local_api_key"
|
69
|
+
lambda { Sift::Client.new(api_key, nil) }.should raise_error
|
70
|
+
lambda { Sift::Client.new(api_key, "") }.should raise_error
|
71
|
+
lambda { Sift::Client.new(api_key, 123456) }.should raise_error
|
72
|
+
end
|
73
|
+
|
74
|
+
it "Can instantiate client with non-default timeout" do
|
75
|
+
lambda { Sift::Client.new("test_local_api_key", Sift.current_rest_api_path, 4) }.should_not raise_error
|
32
76
|
end
|
33
77
|
|
34
78
|
it "Track call must specify an event name" do
|
@@ -126,31 +170,38 @@ describe Sift::Client do
|
|
126
170
|
it "Successfully fetches a score" do
|
127
171
|
|
128
172
|
api_key = "foobar"
|
129
|
-
|
173
|
+
response_json = score_response_json
|
130
174
|
|
175
|
+
stub_request(:get, "https://api.siftscience.com/v203/score/247019/?api_key=foobar").
|
176
|
+
to_return(:status => 200, :body => MultiJson.dump(response_json), :headers => {})
|
177
|
+
|
178
|
+
response = Sift::Client.new(api_key).score(score_response_json[:user_id])
|
179
|
+
response.ok?.should eq(true)
|
180
|
+
response.api_status.should eq(0)
|
181
|
+
response.api_error_message.should eq("OK")
|
182
|
+
|
183
|
+
response.body["score"].should eq(0.93)
|
184
|
+
end
|
185
|
+
|
186
|
+
it "Successfuly make a sync score request" do
|
187
|
+
|
188
|
+
api_key = "foobar"
|
131
189
|
response_json = {
|
132
|
-
:user_id => user_id,
|
133
|
-
:score => 0.93,
|
134
|
-
:reasons => [{
|
135
|
-
:name => "UsersPerDevice",
|
136
|
-
:value => 4,
|
137
|
-
:details => {
|
138
|
-
:users => "a, b, c, d"
|
139
|
-
}
|
140
|
-
}],
|
141
190
|
:status => 0,
|
142
|
-
:error_message => "OK"
|
191
|
+
:error_message => "OK",
|
192
|
+
:score_response => score_response_json
|
143
193
|
}
|
144
194
|
|
145
|
-
stub_request(:
|
195
|
+
stub_request(:post, "https://api.siftscience.com/v203/events?return_score=true").
|
146
196
|
to_return(:status => 200, :body => MultiJson.dump(response_json), :headers => {})
|
147
197
|
|
148
|
-
|
198
|
+
event = "$transaction"
|
199
|
+
properties = valid_transaction_properties
|
200
|
+
response = Sift::Client.new(api_key).track(event, properties, nil, nil, true)
|
149
201
|
response.ok?.should eq(true)
|
150
202
|
response.api_status.should eq(0)
|
151
203
|
response.api_error_message.should eq("OK")
|
152
|
-
|
153
|
-
response.json["score"].should eq(0.93)
|
204
|
+
response.body["score_response"]["score"].should eq(0.93)
|
154
205
|
end
|
155
206
|
|
156
207
|
end
|
metadata
CHANGED
@@ -1,41 +1,42 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sift
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fred Sadaghiani
|
8
|
+
- Yoav Schatzberg
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2014-
|
12
|
+
date: 2014-09-03 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rspec
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
|
-
- -
|
18
|
+
- - ! '>='
|
18
19
|
- !ruby/object:Gem::Version
|
19
20
|
version: 2.14.1
|
20
21
|
type: :development
|
21
22
|
prerelease: false
|
22
23
|
version_requirements: !ruby/object:Gem::Requirement
|
23
24
|
requirements:
|
24
|
-
- -
|
25
|
+
- - ! '>='
|
25
26
|
- !ruby/object:Gem::Version
|
26
27
|
version: 2.14.1
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
29
|
name: webmock
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
30
31
|
requirements:
|
31
|
-
- -
|
32
|
+
- - ! '>='
|
32
33
|
- !ruby/object:Gem::Version
|
33
34
|
version: 1.16.0
|
34
35
|
type: :development
|
35
36
|
prerelease: false
|
36
37
|
version_requirements: !ruby/object:Gem::Requirement
|
37
38
|
requirements:
|
38
|
-
- -
|
39
|
+
- - ! '>='
|
39
40
|
- !ruby/object:Gem::Version
|
40
41
|
version: 1.16.0
|
41
42
|
- !ruby/object:Gem::Dependency
|
@@ -56,30 +57,16 @@ dependencies:
|
|
56
57
|
name: multi_json
|
57
58
|
requirement: !ruby/object:Gem::Requirement
|
58
59
|
requirements:
|
59
|
-
- -
|
60
|
+
- - ! '>='
|
60
61
|
- !ruby/object:Gem::Version
|
61
62
|
version: '1.0'
|
62
63
|
type: :runtime
|
63
64
|
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ~>
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '1.0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rspec
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ! '>='
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '2.0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
65
|
version_requirements: !ruby/object:Gem::Requirement
|
79
66
|
requirements:
|
80
67
|
- - ! '>='
|
81
68
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
69
|
+
version: '1.0'
|
83
70
|
- !ruby/object:Gem::Dependency
|
84
71
|
name: rake
|
85
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,7 +83,7 @@ dependencies:
|
|
96
83
|
version: '0'
|
97
84
|
description: Sift Science Ruby API. Please see http://siftscience.com for more details.
|
98
85
|
email:
|
99
|
-
-
|
86
|
+
- support@siftscience.com
|
100
87
|
executables: []
|
101
88
|
extensions: []
|
102
89
|
extra_rdoc_files: []
|
@@ -137,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
124
|
version: '0'
|
138
125
|
requirements: []
|
139
126
|
rubyforge_project: sift
|
140
|
-
rubygems_version: 2.
|
127
|
+
rubygems_version: 2.2.2
|
141
128
|
signing_key:
|
142
129
|
specification_version: 4
|
143
130
|
summary: Sift Science Ruby API Gem
|