leanplum_api 3.1.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +63 -32
- data/lib/leanplum_api/api.rb +107 -156
- data/lib/leanplum_api/configuration.rb +9 -1
- data/lib/leanplum_api/connection.rb +2 -2
- data/lib/leanplum_api/data_export_api.rb +78 -0
- data/lib/leanplum_api/faraday_middleware/response_validation.rb +23 -19
- data/lib/leanplum_api/version.rb +1 -1
- data/spec/api_spec.rb +142 -98
- data/spec/configuration_spec.rb +2 -2
- data/spec/data_export_api_spec.rb +57 -0
- data/spec/fixtures/vcr/delete_user.yml +129 -0
- data/spec/fixtures/vcr/export_data.yml +5 -5
- data/spec/fixtures/vcr/export_data_dates.yml +6 -6
- data/spec/fixtures/vcr/export_user.yml +7 -7
- data/spec/fixtures/vcr/export_users.yml +44 -0
- data/spec/fixtures/vcr/get_ab_test.yml +5 -5
- data/spec/fixtures/vcr/get_ab_tests.yml +5 -5
- data/spec/fixtures/vcr/get_export_results.yml +5 -5
- data/spec/fixtures/vcr/get_messages.yml +5 -5
- data/spec/fixtures/vcr/get_vars.yml +5 -5
- data/spec/fixtures/vcr/missing_message.yml +4 -4
- data/spec/fixtures/vcr/reset_anomalous_user.yml +6 -6
- data/spec/fixtures/vcr/set_device_attributes.yml +46 -0
- data/spec/fixtures/vcr/set_user_attributes.yml +7 -7
- data/spec/fixtures/vcr/set_user_attributes_with_devices.yml +46 -0
- data/spec/fixtures/vcr/set_user_attributes_with_devices_and_events.yml +46 -0
- data/spec/fixtures/vcr/set_user_attributes_with_events.yml +46 -0
- data/spec/fixtures/vcr/track_events.yml +8 -8
- data/spec/fixtures/vcr/track_events_and_attributes.yml +9 -9
- data/spec/fixtures/vcr/track_events_anomaly_overrider.yml +20 -19
- data/spec/fixtures/vcr/track_offline_events.yml +8 -8
- data/spec/http_spec.rb +6 -5
- data/spec/spec_helper.rb +11 -8
- metadata +40 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba21ebe73260c66ae36b3c901abfe6824d238b26
|
4
|
+
data.tar.gz: '009e58d6f0537a717bd1a66ddf6f39d5601ab328'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e56f3fea79360b0820803e7edb1d153f19691d7adf644699feba9c7abe00edd58593852c8e62988dd202f3471ad7818ed8de9c55225adf6fb10b657c4e420c5
|
7
|
+
data.tar.gz: 9404b64c448032f32cde14db3082cdb4fc5fa4e2b4d7e27de1ede0ea5f4dc653fd4a8bdf8d3c01b9983df5a75687deb62c922c6d4d6fae05b2035f3f518e88ae
|
data/README.md
CHANGED
@@ -8,11 +8,11 @@ Leanplum calls it a REST API but it is not very RESTful.
|
|
8
8
|
|
9
9
|
Leanplum also likes to change and break stuff in their API without changing the version number, so buyer beware.
|
10
10
|
|
11
|
-
The gem uses the
|
11
|
+
The gem uses the `multi` method with a POST for all event tracking and user attribute updating requests. Check Leanplum's docs for more information on `multi`.
|
12
12
|
|
13
13
|
Tested with Leanplum API version 1.0.6 - which is actually totally meaningless because the version is always 1.0.6, even when they make major revisions to how the API works.
|
14
14
|
|
15
|
-
`required_ruby_version` is set to
|
15
|
+
`required_ruby_version` is set to 2.0 but this code has only been tested with Ruby 2.1.5 and up!
|
16
16
|
|
17
17
|
## Configuration
|
18
18
|
|
@@ -44,6 +44,9 @@ LeanplumApi.configure do |config|
|
|
44
44
|
# Set this to true to send events and user attributes to the test environment.
|
45
45
|
# Defaults to false. See "Debugging" below for more info.
|
46
46
|
config.developer_mode = true
|
47
|
+
|
48
|
+
# Override validations for Leanplum responses. True by default and you should probably leave it that way.
|
49
|
+
config.validate_response = true
|
47
50
|
end
|
48
51
|
```
|
49
52
|
|
@@ -54,9 +57,16 @@ end
|
|
54
57
|
```ruby
|
55
58
|
api = LeanplumApi::API.new
|
56
59
|
|
60
|
+
# Setting device attributes requires a device_id.
|
61
|
+
device_attributes = {
|
62
|
+
device_id: 'big_boss_belt_buckler',
|
63
|
+
device_model: 'four_vogues'
|
64
|
+
}
|
65
|
+
api.set_device_attributes(device_attributes)
|
66
|
+
|
57
67
|
# You must provide either :user_id or :device_id for requests involving
|
58
68
|
# attribute updates or event tracking.
|
59
|
-
|
69
|
+
user_attributes = {
|
60
70
|
user_id: 12345,
|
61
71
|
first_name: 'Mike',
|
62
72
|
last_name: 'Jones',
|
@@ -64,23 +74,26 @@ attribute_hash = {
|
|
64
74
|
email: 'still_tippin@test.com',
|
65
75
|
birthday: Date.today # Dates/times will be converted to ISO8601 format
|
66
76
|
}
|
67
|
-
api.set_user_attributes(
|
68
|
-
|
69
|
-
# In 2017, Leanplum implemented the ability to set various first and last timestamps for event
|
70
|
-
# counts for that event in their API
|
71
|
-
#
|
72
|
-
|
77
|
+
api.set_user_attributes(user_attributes)
|
78
|
+
|
79
|
+
# In 2017, Leanplum implemented the ability to set various first and last timestamps for event
|
80
|
+
# occurrences, as well as counts for that event in their setUserAttributes API.
|
81
|
+
# They also added the ability to set devices for that user.
|
82
|
+
# This is what it would look like to push data about an event that happened 5 times between
|
83
|
+
# 2015-02-01 and today along with a set of devices for that user.
|
84
|
+
user_attributes = {
|
73
85
|
user_id: 12345,
|
86
|
+
devices: [device_attributes],
|
74
87
|
events: {
|
75
88
|
my_event_name: {
|
76
89
|
count: 5,
|
77
90
|
value: 'woodgrain',
|
78
|
-
firstTime: '2015-02-01'.to_time,
|
79
|
-
lastTime: Time.now.utc
|
91
|
+
firstTime: '2015-02-01'.to_time, # Dates/times will be converted to epoch seconds
|
92
|
+
lastTime: Time.now.utc # Dates/times will be converted to epoch seconds
|
80
93
|
}
|
81
94
|
}
|
82
95
|
}
|
83
|
-
api.set_user_attributes(
|
96
|
+
api.set_user_attributes(user_attributes)
|
84
97
|
|
85
98
|
# You must also provide the :event property for event tracking.
|
86
99
|
## :info is an optional property for an extra string.
|
@@ -89,63 +102,79 @@ api.set_user_attributes(attribute_hash)
|
|
89
102
|
event = {
|
90
103
|
user_id: 12345,
|
91
104
|
event: 'purchase',
|
105
|
+
time: Time.now.utc, # Event timestamps will be converted to epoch seconds
|
92
106
|
info: 'reallybigpurchase',
|
93
|
-
time: Time.now.utc, # Event timestamps will be converted to epoch seconds by the gem.
|
94
107
|
some_event_property: 'boss_hog_on_candy'
|
95
108
|
}
|
96
109
|
api.track_events(event)
|
97
110
|
# Events tracked like that will be made part of a session; for independent events use :allow_offline
|
98
|
-
# Ed. note 2017-09-12 - looks like Leanplum changed their API and everything is considered offline now
|
111
|
+
# Ed. note 2017-09-12 - looks like Leanplum changed their API and everything is considered offline now.
|
99
112
|
api.track_events(event, allow_offline: true)
|
100
113
|
|
101
|
-
# You can also track events and
|
102
|
-
api.track_multi(
|
114
|
+
# You can also track events, user attributes, and device attributes at the same time. Magic!
|
115
|
+
api.track_multi(
|
116
|
+
events: event,
|
117
|
+
user_attributes: user_attributes,
|
118
|
+
device_attributes: device_attributes,
|
119
|
+
options: { force_anomalous_override: true }
|
120
|
+
)
|
103
121
|
|
104
122
|
# If your event is sufficiently far in the past, leanplum will mark your user as "Anomalous"
|
105
|
-
# To force a reset of this flag, either call the method directly
|
123
|
+
# To force a reset of this flag, either call the method directly.
|
106
124
|
api.reset_anomalous_users([12345, 23456])
|
107
|
-
# Or use the :force_anomalous_override option when calling track_events or track_multi
|
125
|
+
# Or use the :force_anomalous_override option when calling track_events or track_multi.
|
108
126
|
api.track_events(event, force_anomalous_override: true)
|
109
127
|
```
|
110
128
|
|
111
129
|
### API based data export:
|
112
130
|
|
113
131
|
```ruby
|
114
|
-
|
115
|
-
|
116
|
-
|
132
|
+
data_export_api = LeanplumApi::DataExportAPI.new
|
133
|
+
|
134
|
+
# Bulk data export
|
135
|
+
job_id = data_export_api.export_data(start_time, end_time)
|
136
|
+
response = data_export_api.wait_for_export_job(job_id)
|
137
|
+
|
138
|
+
# User export
|
139
|
+
job_id = data_export_api.export_users(ab_test_id, segment)
|
140
|
+
response = data_export_api.wait_for_export_job(job_id)
|
117
141
|
```
|
118
142
|
|
119
143
|
**Note well that Leanplum now officially recommends use of the automated S3 export instead of API based export.** According to a Leanplum engineer these two data export methodologies are completely independent data paths and in our experience we have found API based data export to be missing 10-15% of the data that is eventually returned by the automated export.
|
120
144
|
|
121
145
|
### Other Available Methods
|
122
|
-
|
123
|
-
|
146
|
+
These are mostly simple wrappers around Leanplum's API methods. See their documentation for details.
|
147
|
+
|
148
|
+
* `api.export_user(user_id)`
|
149
|
+
* `api.user_attributes(user_id)` (gives you the attributes section of `exportUser`)
|
150
|
+
* `api.user_events(user_id)` (gives you the events section of `exportUser`)
|
124
151
|
* `api.get_ab_tests(only_recent)`
|
125
152
|
* `api.get_ab_test(ab_test_id)`
|
126
153
|
* `api.get_messages(only_recent)`
|
127
154
|
* `api.get_message(message_id)`
|
128
155
|
* `api.get_variant(variant_id)`
|
129
156
|
* `api.get_vars(user_id)`
|
157
|
+
* `api.delete_user(user_id)`
|
158
|
+
|
130
159
|
|
131
160
|
## Specs
|
132
161
|
|
133
162
|
`bundle exec rspec` should work fine at running existing specs.
|
134
163
|
|
135
|
-
To write _new_ specs (or regenerate one of [VCR](https://github.com/vcr/vcr)'s YAML files), you must set the `LEANPLUM_PRODUCTION_KEY`, `LEANPLUM_APP_ID`, `LEANPLUM_CONTENT_READ_ONLY_KEY`, `LEANPLUM_DEVELOPMENT_KEY`, and `LEANPLUM_DATA_EXPORT_KEY` environment variables (preferably to some development only keys)
|
136
|
-
|
137
|
-
> BE AWARE THAT IF YOU WRITE A NEW SPEC OR DELETE A VCR FILE, IT'S POSSIBLE THAT REAL DATA WILL BE WRITTEN TO THE `LEANPLUM_APP_ID` YOU CONFIGURE! Certainly a real request will be made to rebuild the VCR file, and while specs run with ```devMode=true```, it's usually a good idea to create a fake app for testing/running specs against.
|
164
|
+
To write _new_ specs (or regenerate one of [VCR](https://github.com/vcr/vcr)'s YAML files), you must set the `LEANPLUM_PRODUCTION_KEY`, `LEANPLUM_APP_ID`, `LEANPLUM_CONTENT_READ_ONLY_KEY`, `LEANPLUM_DEVELOPMENT_KEY`, and `LEANPLUM_DATA_EXPORT_KEY` environment variables (preferably to some development only keys). The easiest way to do this is to create a `.env` file based on the [.env.example](.env.example) file in the repo and then fill in the blanks; the `dotenv` gem will handle loading them into the environment when you run `bundle exec rspec`.
|
138
165
|
|
139
166
|
```bash
|
140
|
-
|
141
|
-
|
142
|
-
export LEANPLUM_DATA_EXPORT_KEY=data_something_3238mmmX
|
143
|
-
export LEANPLUM_CONTENT_READ_ONLY_KEY=sometingsome23xx9
|
144
|
-
export LEANPLUM_DEVELOPMENT_KEY=sometingsome23xx923n23i
|
145
|
-
|
167
|
+
cp .env.example .env
|
168
|
+
vi .env # open in your favorite text editor; edit it and fill in the various keys
|
146
169
|
bundle exec rspec
|
147
170
|
```
|
148
171
|
|
172
|
+
> BE AWARE THAT IF YOU WRITE A NEW SPEC OR DELETE A VCR FILE, IT'S POSSIBLE THAT REAL DATA WILL BE WRITTEN TO THE `LEANPLUM_APP_ID` YOU CONFIGURE! Certainly a real request will be made to rebuild the VCR file, and while specs run with ```devMode=true```, it's usually a good idea to create a fake app for testing/running specs against.
|
173
|
+
|
174
|
+
VCR will create fixture data based on your requests, masking your actual keys so that it's safe to commit the file.
|
175
|
+
|
176
|
+
One gotcha is that the `Timecop` gem is used to freeze the specs at a particular point in time. You may need to update the `Timecop` config in [spec_helper.rb](spec/spec_helper.rb) and regenerate all the fixtures should you want to record any new interactions. If you see warnings about "Device skew" in the fixtures, this is how you fix it.
|
177
|
+
|
149
178
|
## Debugging
|
150
179
|
|
151
180
|
The `LEANPLUM_API_DEBUG` environment variable will trigger full printouts of Faraday's debug output to STDERR and to the configured logger.
|
@@ -164,6 +193,8 @@ LeanplumApi.configure do |config|
|
|
164
193
|
end
|
165
194
|
```
|
166
195
|
|
196
|
+
This also works when running specs.
|
197
|
+
|
167
198
|
### Developer Mode
|
168
199
|
|
169
200
|
You can also configure "developer mode". This will use the `devMode=true` parameter on some requests, which seems to sends them to a separate queue which might not count towards Leanplum's usage billing.
|
data/lib/leanplum_api/api.rb
CHANGED
@@ -1,146 +1,51 @@
|
|
1
1
|
module LeanplumApi
|
2
2
|
class API
|
3
|
-
|
3
|
+
# API Command Constants
|
4
|
+
SET_USER_ATTRIBUTES = 'setUserAttributes'.freeze
|
5
|
+
SET_DEVICE_ATTRIBUTES = 'setDeviceAttributes'.freeze
|
6
|
+
TRACK = 'track'.freeze
|
4
7
|
|
5
|
-
|
8
|
+
# Data export related constants
|
9
|
+
EXPORT_PENDING = 'PENDING'.freeze
|
10
|
+
EXPORT_RUNNING = 'RUNNING'.freeze
|
11
|
+
EXPORT_FINISHED = 'FINISHED'.freeze
|
6
12
|
|
7
|
-
|
8
|
-
EXPORT_RUNNING = 'RUNNING'
|
9
|
-
EXPORT_FINISHED = 'FINISHED'
|
10
|
-
|
11
|
-
def initialize(options = {})
|
13
|
+
def initialize
|
12
14
|
fail 'LeanplumApi not configured yet!' unless LeanplumApi.configuration
|
13
15
|
end
|
14
16
|
|
15
17
|
def set_user_attributes(user_attributes, options = {})
|
16
|
-
track_multi(
|
18
|
+
track_multi(user_attributes: user_attributes, options: options)
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
20
|
-
track_multi(
|
21
|
-
end
|
22
|
-
|
23
|
-
# This method is for tracking events and/or updating user attributes at the same time, batched together like
|
24
|
-
# leanplum recommends.
|
25
|
-
# Set force_anomalous_override: true to catch warnings from leanplum about anomalous events and force them to not
|
26
|
-
# be considered anomalous
|
27
|
-
def track_multi(events = nil, user_attributes = nil, options = {})
|
28
|
-
request_data = Array.wrap(user_attributes).map { |h| build_user_attributes_hash(h) } +
|
29
|
-
Array.wrap(events).map { |h| build_event_attributes_hash(h, options) }
|
30
|
-
response = production_connection.multi(request_data).body['response']
|
31
|
-
|
32
|
-
if options[:force_anomalous_override]
|
33
|
-
user_ids_to_reset = []
|
34
|
-
response.each_with_index do |indicator, i|
|
35
|
-
# Leanplum's engineering team likes to break their API and or change stuff without warning (often)
|
36
|
-
# and has no idea what "versioning" actually means, so we just reset everyone all the time.
|
37
|
-
# This condition should be:
|
38
|
-
# if indicator['warning'] && indicator['warning']['message'] =~ /Past event detected/i
|
39
|
-
#
|
40
|
-
# but it has to be:
|
41
|
-
if indicator['warning']
|
42
|
-
# Leanplum does not return their warnings in order!!! So we just have
|
43
|
-
# to reset everyone who had any events. This is what the code should be:
|
44
|
-
# user_ids_to_reset << request_data[i]['userId']
|
45
|
-
|
46
|
-
# This is what it has to be:
|
47
|
-
user_ids_to_reset = events.map { |e| e[:user_id] }.uniq
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
unless user_ids_to_reset.empty?
|
52
|
-
LeanplumApi.configuration.logger.debug("Resetting anomalous user ids: #{user_ids_to_reset}")
|
53
|
-
reset_anomalous_users(user_ids_to_reset)
|
54
|
-
end
|
55
|
-
end
|
21
|
+
def set_device_attributes(device_attributes, options = {})
|
22
|
+
track_multi(device_attributes: device_attributes, options: options)
|
56
23
|
end
|
57
24
|
|
58
|
-
|
59
|
-
|
60
|
-
# leads to sort of unprocessed information that can be incomplete.
|
61
|
-
# They recommend using the automatic export to S3 if possible.
|
62
|
-
def export_data(start_time, end_time = nil)
|
63
|
-
LeanplumApi.configuration.logger.warn("You should probably use the direct S3 export instead of exportData")
|
64
|
-
fail "Start time #{start_time} after end time #{end_time}" if end_time && start_time > end_time
|
65
|
-
LeanplumApi.configuration.logger.info("Requesting data export from #{start_time} to #{end_time}...")
|
66
|
-
|
67
|
-
# Because of open questions about how startTime and endTime work (or don't work, as the case may be), we
|
68
|
-
# only want to pass the dates unless start and end times are specifically requested.
|
69
|
-
params = { action: 'exportData', startDate: start_time.strftime('%Y%m%d') }
|
70
|
-
params[:startTime] = start_time.strftime('%s') if start_time.is_a?(DateTime) || start_time.is_a?(Time)
|
71
|
-
if end_time
|
72
|
-
params[:endDate] = end_time.strftime('%Y%m%d')
|
73
|
-
params[:endTime] = end_time.strftime('%s') if end_time.is_a?(DateTime) || end_time.is_a?(Time)
|
74
|
-
end
|
75
|
-
|
76
|
-
# Handle optional S3 export params
|
77
|
-
if LeanplumApi.configuration.s3_bucket_name
|
78
|
-
fail 's3_bucket_name set but s3_access_id not configured!' unless LeanplumApi.configuration.s3_access_id
|
79
|
-
fail 's3_bucket_name set but s3_access_key not configured!' unless LeanplumApi.configuration.s3_access_key
|
80
|
-
|
81
|
-
params.merge!(
|
82
|
-
s3BucketName: LeanplumApi.configuration.s3_bucket_name,
|
83
|
-
s3AccessId: LeanplumApi.configuration.s3_access_id,
|
84
|
-
s3AccessKey: LeanplumApi.configuration.s3_access_key
|
85
|
-
)
|
86
|
-
params.merge!(s3ObjectPrefix: LeanplumApi.configuration.s3_object_prefix) if LeanplumApi.configuration.s3_object_prefix
|
87
|
-
end
|
88
|
-
|
89
|
-
data_export_connection.get(params).body['response'].first['jobId']
|
25
|
+
def track_events(events, options = {})
|
26
|
+
track_multi(events: events, options: options)
|
90
27
|
end
|
91
28
|
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
|
96
|
-
|
97
|
-
|
29
|
+
# This method is for tracking events and/or updating user and/or device attributes
|
30
|
+
# at the same time, batched together like leanplum recommends.
|
31
|
+
# Set the :force_anomalous_override option to catch warnings from leanplum
|
32
|
+
# about anomalous events and force them to not be considered anomalous.
|
33
|
+
def track_multi(events: nil, user_attributes: nil, device_attributes: nil, options: {})
|
34
|
+
events = Array.wrap(events)
|
98
35
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
if response['state'] == EXPORT_FINISHED
|
103
|
-
LeanplumApi.configuration.logger.info("Export finished.")
|
104
|
-
LeanplumApi.configuration.logger.debug(" Response: #{response}")
|
105
|
-
{
|
106
|
-
files: response['files'],
|
107
|
-
number_of_sessions: response['numSessions'],
|
108
|
-
number_of_bytes: response['numBytes'],
|
109
|
-
state: response['state'],
|
110
|
-
s3_copy_status: response['s3CopyStatus']
|
111
|
-
}
|
112
|
-
else
|
113
|
-
{ state: response['state'] }
|
114
|
-
end
|
115
|
-
end
|
36
|
+
request_data = events.map { |h| build_event_attributes_hash(h.dup, options) } +
|
37
|
+
Array.wrap(user_attributes).map { |h| build_user_attributes_hash(h.dup) } +
|
38
|
+
Array.wrap(device_attributes).map { |h| build_device_attributes_hash(h.dup) }
|
116
39
|
|
117
|
-
|
118
|
-
|
119
|
-
LeanplumApi.configuration.logger.debug("Polling job #{job_id}: #{get_export_results(job_id)}")
|
120
|
-
sleep(polling_interval)
|
121
|
-
end
|
122
|
-
get_export_results(job_id)
|
123
|
-
end
|
40
|
+
response = production_connection.multi(request_data)
|
41
|
+
force_anomalous_override(response, events) if options[:force_anomalous_override]
|
124
42
|
|
125
|
-
|
126
|
-
def wait_for_job(job_id, polling_interval = 60)
|
127
|
-
wait_for_export_job(job_id, polling_interval)
|
43
|
+
response
|
128
44
|
end
|
129
|
-
deprecate :wait_for_job, 'wait_for_export_job', 2018, 6
|
130
45
|
|
131
46
|
def user_attributes(user_id)
|
132
|
-
|
133
|
-
|
134
|
-
if v == 'True'
|
135
|
-
attrs[k] = true
|
136
|
-
elsif v == 'False'
|
137
|
-
attrs[k] = false
|
138
|
-
else
|
139
|
-
attrs[k] = v
|
140
|
-
end
|
141
|
-
|
142
|
-
attrs
|
143
|
-
end
|
47
|
+
# Leanplum returns strings instead of booleans
|
48
|
+
Hash[export_user(user_id)['userAttributes'].map { |k, v| [k, v.to_s =~ /\Atrue|false\z/i ? eval(v.downcase) : v] }]
|
144
49
|
end
|
145
50
|
|
146
51
|
def user_events(user_id)
|
@@ -148,102 +53,114 @@ module LeanplumApi
|
|
148
53
|
end
|
149
54
|
|
150
55
|
def export_user(user_id)
|
151
|
-
data_export_connection.get(action: 'exportUser', userId: user_id).
|
56
|
+
response = data_export_connection.get(action: 'exportUser', userId: user_id).first
|
57
|
+
fail ResourceNotFoundError, "User #{user_id} not found" unless response['events'] || response['userAttributes']
|
58
|
+
response
|
152
59
|
end
|
153
60
|
|
154
61
|
def get_ab_tests(only_recent = false)
|
155
|
-
content_read_only_connection.get(action: 'getAbTests', recent: only_recent).
|
62
|
+
content_read_only_connection.get(action: 'getAbTests', recent: only_recent).first['abTests']
|
156
63
|
end
|
157
64
|
|
158
65
|
def get_ab_test(ab_test_id)
|
159
|
-
content_read_only_connection.get(action: 'getAbTest', id: ab_test_id).
|
66
|
+
content_read_only_connection.get(action: 'getAbTest', id: ab_test_id).first['abTest']
|
160
67
|
end
|
161
68
|
|
162
69
|
def get_variant(variant_id)
|
163
|
-
content_read_only_connection.get(action: 'getVariant', id: variant_id).
|
70
|
+
content_read_only_connection.get(action: 'getVariant', id: variant_id).first['variant']
|
164
71
|
end
|
165
72
|
|
166
73
|
def get_messages(only_recent = false)
|
167
|
-
content_read_only_connection.get(action: 'getMessages', recent: only_recent).
|
74
|
+
content_read_only_connection.get(action: 'getMessages', recent: only_recent).first['messages']
|
168
75
|
end
|
169
76
|
|
170
77
|
def get_message(message_id)
|
171
|
-
content_read_only_connection.get(action: 'getMessage', id: message_id).
|
78
|
+
content_read_only_connection.get(action: 'getMessage', id: message_id).first['message']
|
172
79
|
end
|
173
80
|
|
174
81
|
def get_vars(user_id)
|
175
|
-
production_connection.get(action: 'getVars', userId: user_id).
|
82
|
+
production_connection.get(action: 'getVars', userId: user_id).first['vars']
|
83
|
+
end
|
84
|
+
|
85
|
+
def delete_user(user_id)
|
86
|
+
development_connection.get(action: 'deleteUser', userId: user_id).first['vars']
|
176
87
|
end
|
177
88
|
|
178
|
-
# If you pass old events OR users with old date attributes (
|
179
|
-
# them 'anomalous' and exclude them from your data set.
|
180
|
-
# Calling this method after you pass old events will fix that for all events for the specified user_id
|
181
|
-
# For some reason this API feature requires the developer key
|
89
|
+
# If you pass old events OR users with old date attributes (e.g. create_date for an old user), Leanplum
|
90
|
+
# wil mark them 'anomalous' and exclude them from your data set.
|
91
|
+
# Calling this method after you pass old events will fix that for all events for the specified user_id.
|
182
92
|
def reset_anomalous_users(user_ids)
|
183
93
|
user_ids = Array.wrap(user_ids)
|
184
|
-
request_data = user_ids.map { |user_id| { action:
|
94
|
+
request_data = user_ids.map { |user_id| { action: SET_USER_ATTRIBUTES, resetAnomalies: true, userId: user_id } }
|
185
95
|
development_connection.multi(request_data)
|
186
96
|
end
|
187
97
|
|
188
98
|
private
|
189
99
|
|
190
100
|
def production_connection
|
191
|
-
fail
|
101
|
+
fail 'production_key not configured!' unless LeanplumApi.configuration.production_key
|
192
102
|
@production ||= Connection.new(LeanplumApi.configuration.production_key)
|
193
103
|
end
|
194
104
|
|
195
105
|
# Only instantiated for data export endpoint calls
|
196
106
|
def data_export_connection
|
197
|
-
fail
|
107
|
+
fail 'data_export_key not configured!' unless LeanplumApi.configuration.data_export_key
|
198
108
|
@data_export ||= Connection.new(LeanplumApi.configuration.data_export_key)
|
199
109
|
end
|
200
110
|
|
201
111
|
# Only instantiated for ContentReadOnly calls (AB tests)
|
202
112
|
def content_read_only_connection
|
203
|
-
fail
|
113
|
+
fail 'content_read_only_key not configured!' unless LeanplumApi.configuration.content_read_only_key
|
204
114
|
@content_read_only ||= Connection.new(LeanplumApi.configuration.content_read_only_key)
|
205
115
|
end
|
206
116
|
|
207
117
|
def development_connection
|
208
|
-
fail
|
118
|
+
fail 'development_key not configured!' unless LeanplumApi.configuration.development_key
|
209
119
|
@development ||= Connection.new(LeanplumApi.configuration.development_key)
|
210
120
|
end
|
211
121
|
|
212
122
|
# Deletes the user_id and device_id key/value pairs from the hash parameter.
|
213
123
|
def extract_user_id_or_device_id_hash!(hash)
|
214
|
-
user_id = hash.delete(:user_id)
|
215
|
-
device_id = hash.delete(:device_id)
|
124
|
+
user_id = hash.delete(:user_id) || hash.delete(:userId)
|
125
|
+
device_id = hash.delete(:device_id) || hash.delete(:deviceId)
|
216
126
|
fail "No device_id or user_id in hash #{hash}" unless user_id || device_id
|
217
127
|
|
218
128
|
user_id ? { userId: user_id } : { deviceId: device_id }
|
219
129
|
end
|
220
130
|
|
221
|
-
#
|
222
|
-
#
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
131
|
+
# build a user attributes hash
|
132
|
+
# @param [Hash] user_hash user attributes to set into LP user
|
133
|
+
def build_user_attributes_hash(user_hash)
|
134
|
+
user_attr_hash = extract_user_id_or_device_id_hash!(user_hash)
|
135
|
+
user_attr_hash[:action] = SET_USER_ATTRIBUTES
|
136
|
+
user_attr_hash[:devices] = user_hash.delete(:devices) if user_hash.key?(:devices)
|
227
137
|
|
228
|
-
if
|
229
|
-
events
|
230
|
-
|
231
|
-
end
|
138
|
+
if user_hash.key?(:events)
|
139
|
+
user_attr_hash[:events] = user_hash.delete(:events)
|
140
|
+
user_attr_hash[:events].each { |k, v| user_attr_hash[:events][k] = fix_seconds_since_epoch(v) }
|
232
141
|
end
|
233
142
|
|
234
|
-
|
235
|
-
|
236
|
-
|
143
|
+
user_attr_hash[:userAttributes] = fix_iso8601(user_hash)
|
144
|
+
user_attr_hash
|
145
|
+
end
|
146
|
+
|
147
|
+
# build a user attributes hash
|
148
|
+
# @param [Hash] device_hash device attributes to set into LP device
|
149
|
+
def build_device_attributes_hash(device_hash)
|
150
|
+
device_hash = fix_iso8601(device_hash)
|
151
|
+
extract_user_id_or_device_id_hash!(device_hash).merge(
|
152
|
+
action: SET_DEVICE_ATTRIBUTES,
|
153
|
+
deviceAttributes: device_hash
|
154
|
+
)
|
237
155
|
end
|
238
156
|
|
239
157
|
# Events have a :user_id or :device id, a name (:event) and an optional time (:time)
|
240
158
|
# Use the :allow_offline option to send events without creating a new session
|
241
159
|
def build_event_attributes_hash(event_hash, options = {})
|
242
|
-
event_hash = HashWithIndifferentAccess.new(event_hash)
|
243
160
|
event_name = event_hash.delete(:event)
|
244
161
|
fail ":event key not present in #{event_hash}" unless event_name
|
245
162
|
|
246
|
-
event = { action:
|
163
|
+
event = { action: TRACK, event: event_name }.merge(extract_user_id_or_device_id_hash!(event_hash))
|
247
164
|
event.merge!(time: event_hash.delete(:time).strftime('%s').to_i) if event_hash[:time]
|
248
165
|
event.merge!(info: event_hash.delete(:info)) if event_hash[:info]
|
249
166
|
event.merge!(allowOffline: true) if options[:allow_offline]
|
@@ -251,6 +168,40 @@ module LeanplumApi
|
|
251
168
|
event_hash.keys.size > 0 ? event.merge(params: event_hash.symbolize_keys ) : event
|
252
169
|
end
|
253
170
|
|
171
|
+
# Leanplum's engineering team likes to break their API and or change stuff without warning (often)
|
172
|
+
# and has no idea what "versioning" actually means, so we just reset everyone on any type of warning.
|
173
|
+
def force_anomalous_override(responses, events)
|
174
|
+
user_ids_to_reset = []
|
175
|
+
|
176
|
+
responses.each_with_index do |indicator, i|
|
177
|
+
# This condition should be:
|
178
|
+
# if indicator['warning'] && indicator['warning']['message'] =~ /Past event detected/i
|
179
|
+
# but it has to be:
|
180
|
+
if indicator['warning']
|
181
|
+
# Leanplum does not return their warnings in order!!! So we just have
|
182
|
+
# to reset everyone who had any events. This is what the code should be:
|
183
|
+
# user_ids_to_reset << request_data[i]['userId']
|
184
|
+
|
185
|
+
# This is what it has to be:
|
186
|
+
user_ids_to_reset = events.map { |e| e[:user_id] }.uniq
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
unless user_ids_to_reset.empty?
|
191
|
+
LeanplumApi.configuration.logger.debug("Resetting anomalous user ids: #{user_ids_to_reset}")
|
192
|
+
reset_anomalous_users(user_ids_to_reset)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# As of 2015-10 Leanplum supports ISO8601 date & time strings as user attributes.
|
197
|
+
def fix_iso8601(attr_hash)
|
198
|
+
Hash[attr_hash.map { |k, v| [k, (is_date_or_time?(v) ? v.iso8601 : v)] }]
|
199
|
+
end
|
200
|
+
|
201
|
+
def fix_seconds_since_epoch(attr_hash)
|
202
|
+
Hash[attr_hash.map { |k, v| [k, (is_date_or_time?(v) ? v.strftime('%s').to_i : v)] }]
|
203
|
+
end
|
204
|
+
|
254
205
|
def is_date_or_time?(obj)
|
255
206
|
obj.is_a?(Date) || obj.is_a?(Time) || obj.is_a?(DateTime)
|
256
207
|
end
|