leanplum_api 1.3.1
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 +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +133 -0
- data/Rakefile +11 -0
- data/lib/leanplum_api.rb +15 -0
- data/lib/leanplum_api/api.rb +251 -0
- data/lib/leanplum_api/configuration.rb +45 -0
- data/lib/leanplum_api/content_read_only.rb +15 -0
- data/lib/leanplum_api/data_export.rb +15 -0
- data/lib/leanplum_api/development.rb +14 -0
- data/lib/leanplum_api/exception.rb +4 -0
- data/lib/leanplum_api/http.rb +68 -0
- data/lib/leanplum_api/logger.rb +23 -0
- data/lib/leanplum_api/version.rb +3 -0
- data/spec/api_spec.rb +230 -0
- data/spec/configuration_spec.rb +26 -0
- data/spec/fixtures/vcr/export_data.yml +40 -0
- data/spec/fixtures/vcr/export_data_dates.yml +40 -0
- data/spec/fixtures/vcr/export_user.yml +40 -0
- data/spec/fixtures/vcr/get_ab_test.yml +40 -0
- data/spec/fixtures/vcr/get_ab_tests.yml +40 -0
- data/spec/fixtures/vcr/get_export_results.yml +40 -0
- data/spec/fixtures/vcr/get_messages.yml +41 -0
- data/spec/fixtures/vcr/get_vars.yml +40 -0
- data/spec/fixtures/vcr/reset_anomalous_user.yml +43 -0
- data/spec/fixtures/vcr/set_user_attributes.yml +43 -0
- data/spec/fixtures/vcr/track_events.yml +45 -0
- data/spec/fixtures/vcr/track_events_and_attributes.yml +46 -0
- data/spec/fixtures/vcr/track_events_anomaly_overrider.yml +86 -0
- data/spec/http_spec.rb +23 -0
- data/spec/spec_helper.rb +45 -0
- metadata +211 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module LeanplumApi
|
2
|
+
class << self
|
3
|
+
attr_accessor :configuration
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.configure
|
7
|
+
self.configuration ||= Configuration.new
|
8
|
+
yield(configuration) if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.reset!
|
12
|
+
self.configuration = Configuration.new
|
13
|
+
end
|
14
|
+
|
15
|
+
class Configuration
|
16
|
+
DEFAULT_LEANPLUM_API_VERSION = '1.0.6'
|
17
|
+
|
18
|
+
# Required IDs and access keys provided by leanplum
|
19
|
+
attr_accessor :app_id
|
20
|
+
attr_accessor :production_key
|
21
|
+
attr_accessor :content_read_only_key
|
22
|
+
attr_accessor :data_export_key
|
23
|
+
attr_accessor :development_key
|
24
|
+
|
25
|
+
# Optional
|
26
|
+
attr_accessor :api_version
|
27
|
+
attr_accessor :developer_mode
|
28
|
+
attr_accessor :log_path
|
29
|
+
attr_accessor :timeout_seconds
|
30
|
+
|
31
|
+
# Optional configuration for exporting raw data to S3.
|
32
|
+
# If s3_bucket_name is provided, s3_access_id and s3_access_key must also be provided.
|
33
|
+
attr_accessor :s3_bucket_name
|
34
|
+
attr_accessor :s3_access_id
|
35
|
+
attr_accessor :s3_access_key
|
36
|
+
attr_accessor :s3_object_prefix
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@log_path = 'log'
|
40
|
+
@api_version = DEFAULT_LEANPLUM_API_VERSION
|
41
|
+
@developer_mode = false
|
42
|
+
@timeout_seconds = 600
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'leanplum_api/http'
|
2
|
+
|
3
|
+
module LeanplumApi
|
4
|
+
class ContentReadOnly < HTTP
|
5
|
+
def initialize(options = {})
|
6
|
+
raise 'Content read only key not configured!' unless LeanplumApi.configuration.content_read_only_key
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
# Data export API requests need to use the Data Export key
|
11
|
+
def authentication_params
|
12
|
+
super.merge(clientKey: LeanplumApi.configuration.content_read_only_key)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'leanplum_api/http'
|
2
|
+
|
3
|
+
module LeanplumApi
|
4
|
+
class DataExport < HTTP
|
5
|
+
def initialize(options = {})
|
6
|
+
raise 'Data export key not configured' unless LeanplumApi.configuration.data_export_key
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
# Data export API requests need to use the Data Export key
|
11
|
+
def authentication_params
|
12
|
+
super.merge(clientKey: LeanplumApi.configuration.data_export_key)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'leanplum_api/http'
|
2
|
+
|
3
|
+
module LeanplumApi
|
4
|
+
class Development < HTTP
|
5
|
+
def initialize(options = {})
|
6
|
+
raise 'Development key not configured!' unless LeanplumApi.configuration.development_key
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def authentication_params
|
11
|
+
super.merge(clientKey: LeanplumApi.configuration.development_key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module LeanplumApi
|
6
|
+
class HTTP
|
7
|
+
LEANPLUM_API_PATH = '/api'
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@logger = options[:logger] || Logger.new(STDERR)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post(payload)
|
14
|
+
connection.post("#{LEANPLUM_API_PATH}?#{authed_multi_param_string}") do |request|
|
15
|
+
request.body = { data: payload }.to_json
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(query)
|
20
|
+
connection.get(LEANPLUM_API_PATH, query.merge(authentication_params))
|
21
|
+
end
|
22
|
+
|
23
|
+
def authentication_params
|
24
|
+
{
|
25
|
+
appId: LeanplumApi.configuration.app_id,
|
26
|
+
clientKey: LeanplumApi.configuration.production_key,
|
27
|
+
apiVersion: LeanplumApi.configuration.api_version,
|
28
|
+
devMode: LeanplumApi.configuration.developer_mode
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def connection
|
35
|
+
fail 'APP_ID not configured!' unless LeanplumApi.configuration.app_id
|
36
|
+
fail 'PRODUCTION_KEY not configured!' unless LeanplumApi.configuration.production_key
|
37
|
+
|
38
|
+
options = {
|
39
|
+
url: 'https://www.leanplum.com',
|
40
|
+
request: {
|
41
|
+
timeout: LeanplumApi.configuration.timeout_seconds,
|
42
|
+
open_timeout: LeanplumApi.configuration.timeout_seconds
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
@connection ||= Faraday.new(options) do |connection|
|
47
|
+
connection.request :json
|
48
|
+
|
49
|
+
connection.response :logger, @logger, bodies: true if api_debug?
|
50
|
+
connection.response :json, :content_type => /\bjson$/
|
51
|
+
|
52
|
+
connection.adapter Faraday.default_adapter
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def api_debug?
|
57
|
+
ENV['LEANPLUM_API_DEBUG'].to_s =~ /^(true|1)$/i
|
58
|
+
end
|
59
|
+
|
60
|
+
def authed_multi_param_string
|
61
|
+
if LeanplumApi.configuration.developer_mode
|
62
|
+
URI.encode_www_form(authentication_params.merge(action: 'multi', time: Time.now.utc.strftime('%s'), devMode: true))
|
63
|
+
else
|
64
|
+
URI.encode_www_form(authentication_params.merge(action: 'multi', time: Time.now.utc.strftime('%s')))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module LeanplumApi
|
4
|
+
class LeanplumApiLogger < Logger
|
5
|
+
def format_message(severity, timestamp, progname, msg)
|
6
|
+
@keys ||= [
|
7
|
+
LeanplumApi.configuration.production_key,
|
8
|
+
LeanplumApi.configuration.app_id,
|
9
|
+
LeanplumApi.configuration.data_export_key,
|
10
|
+
LeanplumApi.configuration.content_read_only_key,
|
11
|
+
LeanplumApi.configuration.development_key,
|
12
|
+
LeanplumApi.configuration.s3_access_key,
|
13
|
+
LeanplumApi.configuration.s3_access_id
|
14
|
+
].compact
|
15
|
+
|
16
|
+
if @keys.empty?
|
17
|
+
"#{timestamp.strftime('%Y-%m-%d %H:%M:%S')} #{severity} #{msg}\n"
|
18
|
+
else
|
19
|
+
"#{timestamp.strftime('%Y-%m-%d %H:%M:%S')} #{severity} #{msg.gsub(/#{@keys.map { |k| Regexp.quote(k) }.join('|')}/, '<HIDDEN_KEY>')}\n"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LeanplumApi::API do
|
4
|
+
let(:api) { LeanplumApi::API.new }
|
5
|
+
let(:users) do
|
6
|
+
[{
|
7
|
+
user_id: 123456,
|
8
|
+
first_name: 'Mike',
|
9
|
+
last_name: 'Jones',
|
10
|
+
gender: 'm',
|
11
|
+
email: 'still_tippin@test.com',
|
12
|
+
create_date: '2010-01-01'.to_date
|
13
|
+
}]
|
14
|
+
end
|
15
|
+
let(:first_user_id) { users.first[:user_id] }
|
16
|
+
|
17
|
+
context 'users' do
|
18
|
+
it 'build_user_attributes_hash' do
|
19
|
+
expect(api.send(:build_user_attributes_hash, users.first)).to eq({
|
20
|
+
'userId' => 123456,
|
21
|
+
'action' => 'setUserAttributes',
|
22
|
+
'userAttributes' => {
|
23
|
+
first_name: 'Mike',
|
24
|
+
last_name: 'Jones',
|
25
|
+
gender: 'm',
|
26
|
+
email: 'still_tippin@test.com',
|
27
|
+
create_date: '2010-01-01'
|
28
|
+
}
|
29
|
+
})
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'set_user_attributes' do
|
33
|
+
context 'valid request' do
|
34
|
+
it 'should successfully set user attributes' do
|
35
|
+
VCR.use_cassette('set_user_attributes') do
|
36
|
+
expect { api.set_user_attributes(users) }.to_not raise_error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'invalid request' do
|
42
|
+
let(:broken_users) { users + [{ first_name: 'Moe' }] }
|
43
|
+
|
44
|
+
it 'should raise an error' do
|
45
|
+
VCR.use_cassette('set_user_attributes_broken') do
|
46
|
+
expect{ api.set_user_attributes(broken_users) }.to raise_error(/No device_id or user_id in hash/)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'export_user' do
|
53
|
+
it 'should get user attributes for this user' do
|
54
|
+
VCR.use_cassette('export_user') do
|
55
|
+
user_info = api.export_user(first_user_id)
|
56
|
+
user_info.keys.each do |k|
|
57
|
+
if users.first[k.to_sym].is_a?(Date) || users.first[k.to_sym].is_a?(DateTime)
|
58
|
+
expect(user_info[k]).to eq(users.first[k.to_sym].strftime('%Y-%m-%d'))
|
59
|
+
else
|
60
|
+
expect(user_info[k]).to eq(users.first[k.to_sym])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'export_users' do
|
68
|
+
it 'should export users'
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'reset_anomalous_users' do
|
72
|
+
it 'should successfully call setUserAttributes with resetAnomalies' do
|
73
|
+
VCR.use_cassette('reset_anomalous_user') do
|
74
|
+
expect { api.reset_anomalous_users(first_user_id) }.to_not raise_error
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'events' do
|
81
|
+
let(:events) do
|
82
|
+
[
|
83
|
+
{
|
84
|
+
user_id: 12345,
|
85
|
+
event: 'purchase',
|
86
|
+
time: Time.now.utc,
|
87
|
+
params: {
|
88
|
+
'some_timestamp' => '2015-05-01 01:02:03'
|
89
|
+
}
|
90
|
+
},
|
91
|
+
{
|
92
|
+
user_id: 54321,
|
93
|
+
event: 'purchase_page_view',
|
94
|
+
time: Time.now.utc - 10.minutes,
|
95
|
+
}
|
96
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'build_event_attributes_hash' do
|
100
|
+
expect(api.send(:build_event_attributes_hash, events.first)).to eq({
|
101
|
+
'userId' => 12345,
|
102
|
+
'time' => Time.now.utc.strftime('%s'),
|
103
|
+
'action' => 'track',
|
104
|
+
'event' => 'purchase',
|
105
|
+
'params' => { params: { 'some_timestamp'=>'2015-05-01 01:02:03' } }
|
106
|
+
})
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'without user attributes' do
|
110
|
+
context 'valid request' do
|
111
|
+
it 'should successfully track events' do
|
112
|
+
VCR.use_cassette('track_events') do
|
113
|
+
expect { api.track_events(events) }.to_not raise_error
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'invalid request' do
|
119
|
+
let(:broken_events) { events + [{ event: 'no_user_id_event' }] }
|
120
|
+
|
121
|
+
it 'should raise an error' do
|
122
|
+
VCR.use_cassette('track_events_broken') do
|
123
|
+
expect { api.track_events(broken_events) }.to raise_error(/No device_id or user_id in hash/)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'anomalous data force_anomalous_override' do
|
129
|
+
it 'should successfully force the anomalous data override events' do
|
130
|
+
VCR.use_cassette('track_events_anomaly_overrider') do
|
131
|
+
expect { api.track_events(events, force_anomalous_override: true) }.to_not raise_error
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'along with user attributes' do
|
138
|
+
it 'should work' do
|
139
|
+
VCR.use_cassette('track_events_and_attributes') do
|
140
|
+
expect { api.track_multi(events, users) }.to_not raise_error
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Data export and content read only endpoints forbid use of devMode
|
147
|
+
context 'non devMode methods' do
|
148
|
+
around(:all) do |example|
|
149
|
+
LeanplumApi.configure { |c| c.developer_mode = false }
|
150
|
+
example.run
|
151
|
+
LeanplumApi.configure { |c| c.developer_mode = true }
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'data export methods' do
|
155
|
+
let(:s3_bucket_name) { 'bucket' }
|
156
|
+
let(:s3_access_key) { 's3_access_key' }
|
157
|
+
let(:s3_access_id) { 's3_access_id' }
|
158
|
+
|
159
|
+
around(:all) do |example|
|
160
|
+
LeanplumApi.configure do |c|
|
161
|
+
c.developer_mode = false
|
162
|
+
end
|
163
|
+
example.run
|
164
|
+
LeanplumApi.configure { |c| c.developer_mode = true }
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'export_data' do
|
168
|
+
it 'should request a data export job with a starttime' do
|
169
|
+
VCR.use_cassette('export_data') do
|
170
|
+
expect { api.export_data(Time.at(1438660800).utc) }.to raise_error(/No matching data found/)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should request a data export job with start and end dates' do
|
175
|
+
VCR.use_cassette('export_data_dates') do
|
176
|
+
expect { api.export_data(Date.new(2015, 9, 5), Date.new(2015, 9, 6)) }.to raise_error(/No matching data found/)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'get_export_results' do
|
182
|
+
it 'should get a status for a data export job' do
|
183
|
+
VCR.use_cassette('get_export_results') do
|
184
|
+
response = api.get_export_results('export_4727756026281984_2904941266315269120')
|
185
|
+
expect(response).to eq({
|
186
|
+
files: ['https://leanplum_export.storage.googleapis.com/export-4727756026281984-d5969d55-f242-48a6-85a3-165af08e2306-output-0'],
|
187
|
+
number_of_bytes: 36590,
|
188
|
+
number_of_sessions: 101,
|
189
|
+
state: LeanplumApi::API::EXPORT_FINISHED,
|
190
|
+
s3_copy_status: nil
|
191
|
+
})
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context 'content read only methods' do
|
198
|
+
it 'gets ab tests' do
|
199
|
+
VCR.use_cassette('get_ab_tests') do
|
200
|
+
expect(api.get_ab_tests).to eq([])
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'gets an ab test' do
|
205
|
+
VCR.use_cassette('get_ab_test') do
|
206
|
+
expect(api.get_ab_tests(1)).to eq([])
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'gets messages' do
|
211
|
+
VCR.use_cassette('get_messages') do
|
212
|
+
expect(api.get_messages).to eq([{
|
213
|
+
"id" => 5670583287676928,
|
214
|
+
"created" => 1440091595.799,
|
215
|
+
"name" => "New Message",
|
216
|
+
"active" => false,
|
217
|
+
"messageType" => "Push Notification"
|
218
|
+
}])
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'gets vars' do
|
223
|
+
VCR.use_cassette('get_vars') do
|
224
|
+
vars = api.get_vars(first_user_id)
|
225
|
+
expect(vars).to eq({'test_var' => 1})
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LeanplumApi do
|
4
|
+
context 'configuration' do
|
5
|
+
after(:each) do
|
6
|
+
LeanplumApi.reset!
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should have a default configuration' do
|
10
|
+
expect(LeanplumApi.configuration.log_path.is_a?(String)).to eq(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should allow configuration' do
|
14
|
+
LeanplumApi.configure do |config|
|
15
|
+
config.log_path = 'test/path'
|
16
|
+
config.production_key = 'new_client_key'
|
17
|
+
config.app_id = 'new_app_id'
|
18
|
+
end
|
19
|
+
|
20
|
+
expect(LeanplumApi.configuration.log_path).to eq('test/path')
|
21
|
+
expect(LeanplumApi.configuration.production_key).to eq('new_client_key')
|
22
|
+
expect(LeanplumApi.configuration.app_id).to eq('new_app_id')
|
23
|
+
expect(LeanplumApi.configuration.api_version).to eq(LeanplumApi::Configuration::DEFAULT_LEANPLUM_API_VERSION)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|