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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +63 -32
  3. data/lib/leanplum_api/api.rb +107 -156
  4. data/lib/leanplum_api/configuration.rb +9 -1
  5. data/lib/leanplum_api/connection.rb +2 -2
  6. data/lib/leanplum_api/data_export_api.rb +78 -0
  7. data/lib/leanplum_api/faraday_middleware/response_validation.rb +23 -19
  8. data/lib/leanplum_api/version.rb +1 -1
  9. data/spec/api_spec.rb +142 -98
  10. data/spec/configuration_spec.rb +2 -2
  11. data/spec/data_export_api_spec.rb +57 -0
  12. data/spec/fixtures/vcr/delete_user.yml +129 -0
  13. data/spec/fixtures/vcr/export_data.yml +5 -5
  14. data/spec/fixtures/vcr/export_data_dates.yml +6 -6
  15. data/spec/fixtures/vcr/export_user.yml +7 -7
  16. data/spec/fixtures/vcr/export_users.yml +44 -0
  17. data/spec/fixtures/vcr/get_ab_test.yml +5 -5
  18. data/spec/fixtures/vcr/get_ab_tests.yml +5 -5
  19. data/spec/fixtures/vcr/get_export_results.yml +5 -5
  20. data/spec/fixtures/vcr/get_messages.yml +5 -5
  21. data/spec/fixtures/vcr/get_vars.yml +5 -5
  22. data/spec/fixtures/vcr/missing_message.yml +4 -4
  23. data/spec/fixtures/vcr/reset_anomalous_user.yml +6 -6
  24. data/spec/fixtures/vcr/set_device_attributes.yml +46 -0
  25. data/spec/fixtures/vcr/set_user_attributes.yml +7 -7
  26. data/spec/fixtures/vcr/set_user_attributes_with_devices.yml +46 -0
  27. data/spec/fixtures/vcr/set_user_attributes_with_devices_and_events.yml +46 -0
  28. data/spec/fixtures/vcr/set_user_attributes_with_events.yml +46 -0
  29. data/spec/fixtures/vcr/track_events.yml +8 -8
  30. data/spec/fixtures/vcr/track_events_and_attributes.yml +9 -9
  31. data/spec/fixtures/vcr/track_events_anomaly_overrider.yml +20 -19
  32. data/spec/fixtures/vcr/track_offline_events.yml +8 -8
  33. data/spec/http_spec.rb +6 -5
  34. data/spec/spec_helper.rb +11 -8
  35. metadata +40 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2908e4c232266ec051f099699765d0605a270e2b
4
- data.tar.gz: 82b980405f5c55a3096e4101cb2c7b503796f18b
3
+ metadata.gz: ba21ebe73260c66ae36b3c901abfe6824d238b26
4
+ data.tar.gz: '009e58d6f0537a717bd1a66ddf6f39d5601ab328'
5
5
  SHA512:
6
- metadata.gz: 21525c8ab65ad7b2226b06bb3717df28b131aba26e114625b20aa32c6f5b4e828655f3d485b3d85f5f80d7dda6165d9f8926f533109cdf03e065cd38a0b67ae2
7
- data.tar.gz: 07f5ebb8514bedcd977f74d173127b074fb2dae7efd36bcc0fc91181e8d2865647ca96dbb8e61cf32b3836fd94b259da2b0ee7e4e2447d523c91b8d593d331ec
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 ```multi``` method with a POST for all event tracking and user attribute updating requests. Check Leanplum's docs for more information on ```multi```.
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 1.9 but this code has only been tested with Ruby 2.1.5 and up!
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
- attribute_hash = {
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(attribute_hash)
68
-
69
- # In 2017, Leanplum implemented the ability to set various first and last timestamps for event occurrences, as well as
70
- # counts for that event in their API at the same time as you set various attributes for that user.
71
- # This is what it would look like to push data about an event that happened 5 times between 2015-02-01 and today.
72
- attribute_hash = {
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(attribute_hash)
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 user attributes at the same time
102
- api.track_multi(event, attribute_hash)
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
- api = LeanplumApi::API.new
115
- job_id = api.export_data(start_time, end_time)
116
- response = wait_for_export_job(job_id)
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
- * `api.user_attributes(user_id)`
123
- * `api.user_events(user_id)`
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) to something and then run rspec. VCR will create fixture data based on your requests, masking your actual keys so that it's safe to commit the file.
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
- export LEANPLUM_APP_ID=app_somethingsomething2039410238
141
- export LEANPLUM_PRODUCTION_KEY=dev_somethingsomeg123456
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.
@@ -1,146 +1,51 @@
1
1
  module LeanplumApi
2
2
  class API
3
- extend Gem::Deprecate
3
+ # API Command Constants
4
+ SET_USER_ATTRIBUTES = 'setUserAttributes'.freeze
5
+ SET_DEVICE_ATTRIBUTES = 'setDeviceAttributes'.freeze
6
+ TRACK = 'track'.freeze
4
7
 
5
- class LeanplumValidationException < RuntimeError; end
8
+ # Data export related constants
9
+ EXPORT_PENDING = 'PENDING'.freeze
10
+ EXPORT_RUNNING = 'RUNNING'.freeze
11
+ EXPORT_FINISHED = 'FINISHED'.freeze
6
12
 
7
- EXPORT_PENDING = 'PENDING'
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(nil, user_attributes, options)
18
+ track_multi(user_attributes: user_attributes, options: options)
17
19
  end
18
20
 
19
- def track_events(events, options = {})
20
- track_multi(events, nil, options)
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
- # Returns the jobId
59
- # Leanplum has confirmed that using startTime and endTime, especially trying to be relatively up to the minute,
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
- # See leanplum docs.
93
- # The segment syntax is identical to that produced by the "Insert Value" feature on the dashboard.
94
- # Examples: 'Country = "US"', '{Country = "US"} and {App version = 1}'.
95
- def export_users(segment, ab_test_id)
96
- data_export_connection.get(action: 'exportUsers', segment: segment, ab_test_id: ab_test_id)
97
- end
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
- def get_export_results(job_id)
100
- response = data_export_connection.get(action: 'getExportResults', jobId: job_id).body['response'].first
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
- def wait_for_export_job(job_id, polling_interval = 60)
118
- while get_export_results(job_id)[:state] != EXPORT_FINISHED
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
- # Remove in version 4.x
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
- export_user(user_id)['userAttributes'].inject({}) do |attrs, (k, v)|
133
- # Leanplum doesn't use true JSON for booleans...
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).body['response'].first
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).body['response'].first['abTests']
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).body['response'].first['abTest']
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).body['response'].first['variant']
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).body['response'].first['messages']
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).body['response'].first['message']
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).body['response'].first['vars']
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 (i.e. create_date for an old users), leanplum will mark
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: 'setUserAttributes', resetAnomalies: true, userId: user_id } }
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 "production_key not configured!" unless LeanplumApi.configuration.production_key
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 "data_export_key not configured!" unless LeanplumApi.configuration.data_export_key
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 "content_read_only_key not configured!" unless LeanplumApi.configuration.content_read_only_key
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 "development_key not configured!" unless LeanplumApi.configuration.development_key
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
- # Action can be any command that takes a userAttributes param. "start" (a session) is the other command that most
222
- # obviously takes userAttributes.
223
- # As of 2015-10 Leanplum supports ISO8601 date & time strings as user attributes.
224
- def build_user_attributes_hash(user_hash, action = 'setUserAttributes')
225
- user_hash = HashWithIndifferentAccess.new(user_hash)
226
- user_hash.each { |k, v| user_hash[k] = v.iso8601 if is_date_or_time?(v) }
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 (events = user_hash.delete(:events))
229
- events.each do |event_name, event_props|
230
- event_props.each { |k, v| event_props[k] = v.strftime('%s').to_i if is_date_or_time?(v) }
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
- user_attributes = extract_user_id_or_device_id_hash!(user_hash).merge(action: action, userAttributes: user_hash)
235
- user_attributes[:events] = events if events
236
- user_attributes
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: 'track', event: event_name }.merge(extract_user_id_or_device_id_hash!(event_hash))
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