splitclient-rb 7.1.4.pre.rc3-java → 7.1.4.pre.rc8-java

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 44698d45c21a3a79f58c7af6f640486c276967f0
4
- data.tar.gz: 1dd25d70d076f9651e79eb139a9d96fe7df85063
3
+ metadata.gz: 587fa9e98277f3a3f2551240e421d5a734ed821e
4
+ data.tar.gz: 9ba4dd4290b4b61e18e29c3390a901bdd9e9aebd
5
5
  SHA512:
6
- metadata.gz: 134683e7f2dad59fe226b23589ed6bf42f10e5e0bc6519a182e96c5028352c99466ba9c02488b12fa16221111d74b17eee10432a6c2bd5ea4860ea55ffc37470
7
- data.tar.gz: 6a93079c8aa707a4c9c0548048c122b93311a9822240bd3ffdb4b47a708640ad95d75d1e4ef2ebf51c82aecc3b8d86dc8c91b04e6c9d9877ef95f24aa26e939c
6
+ metadata.gz: 737188f30254868feebdf799ddc9e4a54ef840db885aac1c1155b1e4bc988e112e15ec7879d89bc31911abb334b08379211b7cf93d5735124b330d2f93bf7704
7
+ data.tar.gz: e96eca60d3dc2c53c2667f0a4eee50ed960865af99d6108788598589f8d16cc5b2d33d61a9ae9616f2446888c9a527b8019eecd459ade92ecce66aedac9c2224
@@ -1,7 +1,7 @@
1
1
  CHANGES
2
2
 
3
3
  7.1.3 (Jul 31, 2020)
4
- - Updated rake development dependency to ~> 12.3.3
4
+ - Updated rake development dependency to ~> 12.3.3.
5
5
 
6
6
  7.1.2 (Jun 15, 2020)
7
7
  - Fixed uninitialized constant LocalhostSplitStore::YAML for console apps.
data/Rakefile CHANGED
@@ -11,14 +11,19 @@ RSpec::Core::RakeTask.new(:spec)
11
11
  RuboCop::RakeTask.new(:rubocop)
12
12
 
13
13
  task spec: :compile
14
-
15
- if RUBY_PLATFORM == 'java'
14
+ case RUBY_PLATFORM
15
+ when 'java'
16
16
  require 'rake/javaextensiontask'
17
17
  Rake::JavaExtensionTask.new 'murmurhash' do |ext|
18
18
  ext.lib_dir = 'lib/murmurhash'
19
19
  ext.target_version = '1.7'
20
20
  ext.source_version = '1.7'
21
21
  end
22
+ else
23
+ require 'rake/extensiontask'
24
+ Rake::ExtensionTask.new 'murmurhash' do |ext|
25
+ ext.lib_dir = 'lib/murmurhash'
26
+ end
22
27
  end
23
28
 
24
29
  if !ENV['APPRAISAL_INITIALIZED']
@@ -159,4 +159,143 @@ public final class MurmurHash3 {
159
159
 
160
160
  return h1 & 0xFFFFFFFFL;
161
161
  }
162
+
163
+ // The following set of methods and constants are borrowed from:
164
+ // `This method is borrowed from `org.apache.commons.codec.digest.MurmurHash3`
165
+
166
+ // Constants for 128-bit variant
167
+ private static final long C1 = 0x87c37b91114253d5L;
168
+ private static final long C2 = 0x4cf5ad432745937fL;
169
+ private static final int R1 = 31;
170
+ private static final int R2 = 27;
171
+ private static final int R3 = 33;
172
+ private static final int M = 5;
173
+ private static final int N1 = 0x52dce729;
174
+ private static final int N2 = 0x38495ab5;
175
+
176
+ /**
177
+ * Gets the little-endian long from 8 bytes starting at the specified index.
178
+ *
179
+ * @param data The data
180
+ * @param index The index
181
+ * @return The little-endian long
182
+ */
183
+ private static long getLittleEndianLong(final byte[] data, final int index) {
184
+ return (((long) data[index ] & 0xff) ) |
185
+ (((long) data[index + 1] & 0xff) << 8) |
186
+ (((long) data[index + 2] & 0xff) << 16) |
187
+ (((long) data[index + 3] & 0xff) << 24) |
188
+ (((long) data[index + 4] & 0xff) << 32) |
189
+ (((long) data[index + 5] & 0xff) << 40) |
190
+ (((long) data[index + 6] & 0xff) << 48) |
191
+ (((long) data[index + 7] & 0xff) << 56);
192
+ }
193
+
194
+ public static long[] hash128x64(final byte[] data) {
195
+ return hash128x64(data, 0, data.length, 0);
196
+ }
197
+
198
+ /**
199
+ * Generates 128-bit hash from the byte array with the given offset, length and seed.
200
+ *
201
+ * <p>This is an implementation of the 128-bit hash function {@code MurmurHash3_x64_128}
202
+ * from from Austin Applyby's original MurmurHash3 {@code c++} code in SMHasher.</p>
203
+ *
204
+ * @param data The input byte array
205
+ * @param offset The first element of array
206
+ * @param length The length of array
207
+ * @param seed The initial seed value
208
+ * @return The 128-bit hash (2 longs)
209
+ */
210
+ public static long[] hash128x64(final byte[] data, final int offset, final int length, final long seed) {
211
+ long h1 = seed;
212
+ long h2 = seed;
213
+ final int nblocks = length >> 4;
214
+
215
+ // body
216
+ for (int i = 0; i < nblocks; i++) {
217
+ final int index = offset + (i << 4);
218
+ long k1 = getLittleEndianLong(data, index);
219
+ long k2 = getLittleEndianLong(data, index + 8);
220
+
221
+ // mix functions for k1
222
+ k1 *= C1;
223
+ k1 = Long.rotateLeft(k1, R1);
224
+ k1 *= C2;
225
+ h1 ^= k1;
226
+ h1 = Long.rotateLeft(h1, R2);
227
+ h1 += h2;
228
+ h1 = h1 * M + N1;
229
+
230
+ // mix functions for k2
231
+ k2 *= C2;
232
+ k2 = Long.rotateLeft(k2, R3);
233
+ k2 *= C1;
234
+ h2 ^= k2;
235
+ h2 = Long.rotateLeft(h2, R1);
236
+ h2 += h1;
237
+ h2 = h2 * M + N2;
238
+ }
239
+
240
+ // tail
241
+ long k1 = 0;
242
+ long k2 = 0;
243
+ final int index = offset + (nblocks << 4);
244
+ switch (offset + length - index) {
245
+ case 15:
246
+ k2 ^= ((long) data[index + 14] & 0xff) << 48;
247
+ case 14:
248
+ k2 ^= ((long) data[index + 13] & 0xff) << 40;
249
+ case 13:
250
+ k2 ^= ((long) data[index + 12] & 0xff) << 32;
251
+ case 12:
252
+ k2 ^= ((long) data[index + 11] & 0xff) << 24;
253
+ case 11:
254
+ k2 ^= ((long) data[index + 10] & 0xff) << 16;
255
+ case 10:
256
+ k2 ^= ((long) data[index + 9] & 0xff) << 8;
257
+ case 9:
258
+ k2 ^= data[index + 8] & 0xff;
259
+ k2 *= C2;
260
+ k2 = Long.rotateLeft(k2, R3);
261
+ k2 *= C1;
262
+ h2 ^= k2;
263
+
264
+ case 8:
265
+ k1 ^= ((long) data[index + 7] & 0xff) << 56;
266
+ case 7:
267
+ k1 ^= ((long) data[index + 6] & 0xff) << 48;
268
+ case 6:
269
+ k1 ^= ((long) data[index + 5] & 0xff) << 40;
270
+ case 5:
271
+ k1 ^= ((long) data[index + 4] & 0xff) << 32;
272
+ case 4:
273
+ k1 ^= ((long) data[index + 3] & 0xff) << 24;
274
+ case 3:
275
+ k1 ^= ((long) data[index + 2] & 0xff) << 16;
276
+ case 2:
277
+ k1 ^= ((long) data[index + 1] & 0xff) << 8;
278
+ case 1:
279
+ k1 ^= data[index] & 0xff;
280
+ k1 *= C1;
281
+ k1 = Long.rotateLeft(k1, R1);
282
+ k1 *= C2;
283
+ h1 ^= k1;
284
+ }
285
+
286
+ // finalization
287
+ h1 ^= length;
288
+ h2 ^= length;
289
+
290
+ h1 += h2;
291
+ h2 += h1;
292
+
293
+ h1 = fmix64(h1);
294
+ h2 = fmix64(h2);
295
+
296
+ h1 += h2;
297
+ h2 += h1;
298
+
299
+ return new long[] { h1, h2 };
300
+ }
162
301
  }
@@ -12,6 +12,8 @@ require 'splitclient-rb/cache/adapters/memory_adapter'
12
12
  require 'splitclient-rb/cache/adapters/redis_adapter'
13
13
  require 'splitclient-rb/cache/fetchers/segment_fetcher'
14
14
  require 'splitclient-rb/cache/fetchers/split_fetcher'
15
+ require 'splitclient-rb/cache/hashers/impression_hasher'
16
+ require 'splitclient-rb/cache/observers/impression_observer'
15
17
  require 'splitclient-rb/cache/repositories/repository'
16
18
  require 'splitclient-rb/cache/repositories/segments_repository'
17
19
  require 'splitclient-rb/cache/repositories/splits_repository'
@@ -28,6 +30,7 @@ require 'splitclient-rb/cache/senders/impressions_formatter'
28
30
  require 'splitclient-rb/cache/senders/impressions_sender'
29
31
  require 'splitclient-rb/cache/senders/metrics_sender'
30
32
  require 'splitclient-rb/cache/senders/events_sender'
33
+ require 'splitclient-rb/cache/senders/impressions_count_sender'
31
34
  require 'splitclient-rb/cache/senders/localhost_repo_cleaner'
32
35
  require 'splitclient-rb/cache/stores/store_utils'
33
36
  require 'splitclient-rb/cache/stores/localhost_split_builder'
@@ -52,6 +55,8 @@ require 'splitclient-rb/engine/api/metrics'
52
55
  require 'splitclient-rb/engine/api/segments'
53
56
  require 'splitclient-rb/engine/api/splits'
54
57
  require 'splitclient-rb/engine/api/events'
58
+ require 'splitclient-rb/engine/common/impressions_counter'
59
+ require 'splitclient-rb/engine/common/impressions_manager'
55
60
  require 'splitclient-rb/engine/parser/condition'
56
61
  require 'splitclient-rb/engine/parser/partition'
57
62
  require 'splitclient-rb/engine/parser/evaluator'
@@ -105,7 +110,7 @@ require 'splitclient-rb/sse/notification_processor'
105
110
  require 'splitclient-rb/sse/sse_handler'
106
111
 
107
112
  # C extension
108
- #require 'murmurhash/murmurhash_mri'
113
+ require 'murmurhash/murmurhash_mri'
109
114
 
110
115
  module SplitIoClient
111
116
  def self.root
@@ -0,0 +1,34 @@
1
+ module SplitIoClient
2
+ module Hashers
3
+ class ImpressionHasher
4
+ def initialize
5
+ @murmur_hash_128_64 = case RUBY_PLATFORM
6
+ when 'java'
7
+ Proc.new { |key, seed| Java::MurmurHash3.hash128x64(key, seed) }
8
+ else
9
+ Proc.new { |key, seed| Digest::MurmurHashMRI3_x64_128.rawdigest(key, [seed].pack('L')) }
10
+ end
11
+ end
12
+
13
+ def process(impression)
14
+ impression_data = "#{unknown_if_null(impression[:k])}"
15
+ impression_data << ":#{unknown_if_null(impression[:f])}"
16
+ impression_data << ":#{unknown_if_null(impression[:t])}"
17
+ impression_data << ":#{unknown_if_null(impression[:r])}"
18
+ impression_data << ":#{zero_if_null(impression[:c])}"
19
+
20
+ @murmur_hash_128_64.call(impression_data, 0)[0];
21
+ end
22
+
23
+ private
24
+
25
+ def unknown_if_null(value)
26
+ value == nil ? "UNKNOWN" : value
27
+ end
28
+
29
+ def zero_if_null(value)
30
+ value == nil ? 0 : value
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ module SplitIoClient
2
+ module Observers
3
+ class ImpressionObserver
4
+ LAST_SEEN_CACHE_SIZE = 500000
5
+
6
+ def initialize
7
+ @cache = LruRedux::TTL::ThreadSafeCache.new(LAST_SEEN_CACHE_SIZE)
8
+ @impression_hasher = Hashers::ImpressionHasher.new
9
+ end
10
+
11
+ def test_and_set(impression)
12
+ return if impression.nil?
13
+
14
+ hash = @impression_hasher.process(impression)
15
+ previous = @cache[hash]
16
+ @cache[hash] = impression[:m]
17
+
18
+ previous.nil? ? nil : [previous, impression[:m]].min
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,18 +10,10 @@ module SplitIoClient
10
10
  @adapter = @config.impressions_adapter
11
11
  end
12
12
 
13
- # Store impression data in the selected adapter
14
- def add(matching_key, bucketing_key, split_name, treatment, time)
15
- @adapter.add_to_queue(
16
- m: metadata,
17
- i: impression_data(
18
- matching_key,
19
- bucketing_key,
20
- split_name,
21
- treatment,
22
- time
23
- )
24
- )
13
+ def add_bulk(impressions)
14
+ impressions.each do |impression|
15
+ @adapter.add_to_queue(impression)
16
+ end
25
17
  rescue ThreadError # queue is full
26
18
  if random_sampler.rand(1..1000) <= 2 # log only 0.2 % of the time
27
19
  @config.logger.warn("Dropping impressions. Current size is \
@@ -30,12 +22,6 @@ module SplitIoClient
30
22
  end
31
23
  end
32
24
 
33
- def add_bulk(key, bucketing_key, treatments, time)
34
- treatments.each do |split_name, treatment|
35
- add(key, bucketing_key, split_name, treatment, time)
36
- end
37
- end
38
-
39
25
  def batch
40
26
  return [] if @config.impressions_bulk_size.zero?
41
27
 
@@ -12,28 +12,17 @@ module SplitIoClient
12
12
  @adapter = @config.impressions_adapter
13
13
  end
14
14
 
15
- def add(matching_key, bucketing_key, split_name, treatment, time)
16
- add_bulk(matching_key, bucketing_key, { split_name => treatment }, time)
17
- end
18
-
19
- def add_bulk(matching_key, bucketing_key, treatments, time)
20
- impressions = treatments.map do |split_name, treatment|
21
- {
22
- m: metadata,
23
- i: impression_data(
24
- matching_key,
25
- bucketing_key,
26
- split_name,
27
- treatment,
28
- time
29
- )
30
- }.to_json
15
+ def add_bulk(impressions)
16
+ impressions_json = impressions.map do |impression|
17
+ impression.to_json
31
18
  end
32
19
 
33
- impressions_list_size = @adapter.add_to_queue(key, impressions)
20
+ impressions_list_size = @adapter.add_to_queue(key, impressions_json)
34
21
 
35
22
  # Synchronizer might not be running
36
- @adapter.expire(key, EXPIRE_SECONDS) if impressions.size == impressions_list_size
23
+ @adapter.expire(key, EXPIRE_SECONDS) if impressions_json.size == impressions_list_size
24
+ rescue StandardError => e
25
+ @config.logger.error("Exception while add_bulk_v2: #{e}")
37
26
  end
38
27
 
39
28
  def get_impressions(number_of_impressions = 0)
@@ -6,7 +6,7 @@ module SplitIoClient
6
6
  # Repository which forwards impressions interface to the selected adapter
7
7
  class ImpressionsRepository < Repository
8
8
  extend Forwardable
9
- def_delegators :@repository, :add, :add_bulk, :batch, :clear, :empty?
9
+ def_delegators :@repository, :add_bulk, :batch, :clear, :empty?
10
10
 
11
11
  def initialize(config)
12
12
  super(config)
@@ -17,32 +17,6 @@ module SplitIoClient
17
17
  Repositories::Impressions::RedisRepository.new(@config)
18
18
  end
19
19
  end
20
-
21
- protected
22
-
23
- def impression_data(matching_key, bucketing_key, split_name, treatment, timestamp)
24
- {
25
- k: matching_key,
26
- b: bucketing_key,
27
- f: split_name,
28
- t: treatment[:treatment],
29
- r: applied_rule(treatment[:label]),
30
- c: treatment[:change_number],
31
- m: timestamp
32
- }
33
- end
34
-
35
- def metadata
36
- {
37
- s: "#{@config.language}-#{@config.version}",
38
- i: @config.machine_ip,
39
- n: @config.machine_name
40
- }
41
- end
42
-
43
- def applied_rule(label)
44
- @config.labels_enabled ? label : nil
45
- end
46
20
  end
47
21
  end
48
22
  end
@@ -18,24 +18,22 @@ module SplitIoClient
18
18
  end
19
19
  end
20
20
 
21
- def add(impression)
22
- enqueue(impression)
23
- end
24
-
25
21
  def add_bulk(impressions)
26
- impressions[:split_names].each do |split_name|
22
+ return unless @listener
23
+ impressions.each do |impression|
27
24
  enqueue(
28
- split_name: split_name.to_s,
29
- matching_key: impressions[:matching_key],
30
- bucketing_key: impressions[:bucketing_key],
31
- time: impressions[:time],
25
+ split_name: impression[:i][:f],
26
+ matching_key: impression[:i][:k],
27
+ bucketing_key: impression[:i][:b],
28
+ time: impression[:i][:m],
32
29
  treatment: {
33
- label: impressions[:treatments_labels_change_numbers][split_name.to_sym][:label],
34
- treatment: impressions[:treatments_labels_change_numbers][split_name.to_sym][:treatment],
35
- change_number: impressions[:treatments_labels_change_numbers][split_name.to_sym][:change_number]
30
+ label: impression[:i][:r],
31
+ treatment: impression[:i][:t],
32
+ change_number: impression[:i][:c]
36
33
  },
37
- attributes: impressions[:attributes]
38
- ) unless impressions[:treatments_labels_change_numbers][split_name.to_sym].nil?
34
+ previous_time: impression[:i][:pt],
35
+ attributes: impression[:attributes]
36
+ ) unless impression.nil?
39
37
  end
40
38
  end
41
39
 
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Cache
5
+ module Senders
6
+ class ImpressionsCountSender
7
+ COUNTER_REFRESH_RATE_SECONDS = 1800
8
+
9
+ def initialize(config, impression_counter, impressions_api)
10
+ @config = config
11
+ @impression_counter = impression_counter
12
+ @impressions_api = impressions_api
13
+ end
14
+
15
+ def call
16
+ impressions_count_thread
17
+
18
+ if defined?(PhusionPassenger)
19
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
20
+ impressions_count_thread if forked
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def impressions_count_thread
28
+ @config.threads[:impressions_count_sender] = Thread.new do
29
+ begin
30
+ @config.logger.info('Starting impressions count service')
31
+
32
+ loop do
33
+ post_impressions_count
34
+
35
+ sleep(COUNTER_REFRESH_RATE_SECONDS)
36
+ end
37
+ rescue SplitIoClient::SDKShutdownException
38
+ post_impressions_count
39
+
40
+ @config.logger.info('Posting impressions count due to shutdown')
41
+ end
42
+ end
43
+
44
+ def post_impressions_count
45
+ @impressions_api.post_count(formatter(@impression_counter.pop_all))
46
+ rescue StandardError => error
47
+ @config.log_found_exception(__method__.to_s, error)
48
+ end
49
+
50
+ def formatter(counts)
51
+ return if counts.empty?
52
+
53
+ formated_counts = {pf: []}
54
+
55
+ counts.each do |key, value|
56
+ key_splited = key.split('::')
57
+
58
+ formated_counts[:pf] << {
59
+ f: key_splited[0].to_s, # feature name
60
+ m: key_splited[1].to_i, # time frame
61
+ rc: value # count
62
+ }
63
+ end
64
+
65
+ formated_counts
66
+ rescue StandardError => error
67
+ @config.log_found_exception(__method__.to_s, error)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -17,12 +17,10 @@ module SplitIoClient
17
17
 
18
18
  formatted_impressions = unique_features(filtered_impressions).each_with_object([]) do |feature, memo|
19
19
  feature_impressions = feature_impressions(filtered_impressions, feature)
20
- ip = feature_impressions.first[:m][:i]
21
20
  current_impressions = current_impressions(feature_impressions)
22
21
  memo << {
23
- testName: feature.to_sym,
24
- keyImpressions: current_impressions,
25
- ip: ip
22
+ f: feature.to_sym,
23
+ i: current_impressions
26
24
  }
27
25
  end
28
26
 
@@ -40,12 +38,13 @@ module SplitIoClient
40
38
  def current_impressions(feature_impressions)
41
39
  feature_impressions.map do |impression|
42
40
  {
43
- keyName: impression[:i][:k],
44
- treatment: impression[:i][:t],
45
- time: impression[:i][:m],
46
- bucketingKey: impression[:i][:b],
47
- label: impression[:i][:r],
48
- changeNumber: impression[:i][:c]
41
+ k: impression[:i][:k],
42
+ t: impression[:i][:t],
43
+ m: impression[:i][:m],
44
+ b: impression[:i][:b],
45
+ r: impression[:i][:r],
46
+ c: impression[:i][:c],
47
+ pt: impression[:i][:pt]
49
48
  }
50
49
  end
51
50
  end
@@ -73,7 +72,8 @@ module SplitIoClient
73
72
  "#{impression[:i][:k]}:" \
74
73
  "#{impression[:i][:b]}:" \
75
74
  "#{impression[:i][:c]}:" \
76
- "#{impression[:i][:t]}"
75
+ "#{impression[:i][:t]}:" \
76
+ "#{impression[:i][:pt]}"
77
77
  end
78
78
  end
79
79
  end
@@ -4,10 +4,10 @@ module SplitIoClient
4
4
  module Cache
5
5
  module Senders
6
6
  class ImpressionsSender
7
- def initialize(impressions_repository, api_key, config)
7
+ def initialize(impressions_repository, config, impressions_api)
8
8
  @impressions_repository = impressions_repository
9
- @api_key = api_key
10
9
  @config = config
10
+ @impressions_api = impressions_api
11
11
  end
12
12
 
13
13
  def call
@@ -50,7 +50,7 @@ module SplitIoClient
50
50
  end
51
51
 
52
52
  def impressions_api
53
- @impressions_api ||= SplitIoClient::Api::Impressions.new(@api_key, @config)
53
+ @impressions_api
54
54
  end
55
55
  end
56
56
  end
@@ -9,7 +9,7 @@ module SplitIoClient
9
9
  # @param api_key [String] the API key for your split account
10
10
  #
11
11
  # @return [SplitIoClient] split.io client instance
12
- def initialize(api_key, metrics, splits_repository, segments_repository, impressions_repository, metrics_repository, events_repository, sdk_blocker, config)
12
+ def initialize(api_key, metrics, splits_repository, segments_repository, impressions_repository, metrics_repository, events_repository, sdk_blocker, config, impressions_manager)
13
13
  @api_key = api_key
14
14
  @metrics = metrics
15
15
  @splits_repository = splits_repository
@@ -20,17 +20,21 @@ module SplitIoClient
20
20
  @sdk_blocker = sdk_blocker
21
21
  @destroyed = false
22
22
  @config = config
23
+ @impressions_manager = impressions_manager
23
24
  end
24
25
 
25
26
  def get_treatment(
26
27
  key, split_name, attributes = {}, split_data = nil, store_impressions = true,
27
28
  multiple = false, evaluator = nil
28
29
  )
29
- treatment = treatment(key, split_name, attributes, split_data, store_impressions, multiple, evaluator)
30
+ impressions = []
31
+ result = treatment(key, split_name, attributes, split_data, store_impressions, multiple, evaluator, 'get_treatment', impressions)
32
+ @impressions_manager.track(impressions)
33
+
30
34
  if multiple
31
- treatment.tap { |t| t.delete(:config) }
35
+ result.tap { |t| t.delete(:config) }
32
36
  else
33
- treatment[:treatment]
37
+ result[:treatment]
34
38
  end
35
39
  end
36
40
 
@@ -38,7 +42,11 @@ module SplitIoClient
38
42
  key, split_name, attributes = {}, split_data = nil, store_impressions = true,
39
43
  multiple = false, evaluator = nil
40
44
  )
41
- treatment(key, split_name, attributes, split_data, store_impressions, multiple, evaluator, 'get_treatment_with_config')
45
+ impressions = []
46
+ result = treatment(key, split_name, attributes, split_data, store_impressions, multiple, evaluator, 'get_treatment_with_config', impressions)
47
+ @impressions_manager.track(impressions)
48
+
49
+ result
42
50
  end
43
51
 
44
52
  def get_treatments(key, split_names, attributes = {})
@@ -74,53 +82,6 @@ module SplitIoClient
74
82
  @destroyed = true
75
83
  end
76
84
 
77
- def store_impression(split_name, matching_key, bucketing_key, treatment, attributes)
78
- time = (Time.now.to_f * 1000.0).to_i
79
-
80
- @impressions_repository.add(
81
- matching_key,
82
- bucketing_key,
83
- split_name,
84
- treatment,
85
- time
86
- )
87
-
88
- route_impression(split_name, matching_key, bucketing_key, time, treatment, attributes)
89
-
90
- rescue StandardError => error
91
- @config.log_found_exception(__method__.to_s, error)
92
- end
93
-
94
- def route_impression(split_name, matching_key, bucketing_key, time, treatment, attributes)
95
- impression_router.add(
96
- split_name: split_name,
97
- matching_key: matching_key,
98
- bucketing_key: bucketing_key,
99
- time: time,
100
- treatment: {
101
- label: treatment[:label],
102
- treatment: treatment[:treatment],
103
- change_number: treatment[:change_number]
104
- },
105
- attributes: attributes
106
- )
107
- end
108
-
109
- def route_impressions(split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
110
- impression_router.add_bulk(
111
- split_names: split_names,
112
- matching_key: matching_key,
113
- bucketing_key: bucketing_key,
114
- time: time,
115
- treatments_labels_change_numbers: treatments_labels_change_numbers,
116
- attributes: attributes
117
- )
118
- end
119
-
120
- def impression_router
121
- @impression_router ||= SplitIoClient::ImpressionRouter.new(@config)
122
- end
123
-
124
85
  def track(key, traffic_type_name, event_type, value = nil, properties = nil)
125
86
  return false unless valid_client && @config.split_validator.valid_track_parameters(key, traffic_type_name, event_type, value, properties)
126
87
 
@@ -239,26 +200,20 @@ module SplitIoClient
239
200
 
240
201
  bucketing_key, matching_key = keys_from_key(key)
241
202
  bucketing_key = bucketing_key ? bucketing_key.to_s : nil
242
- matching_key = matching_key ? matching_key.to_s : nil
203
+ matching_key = matching_key ? matching_key.to_s : nil
243
204
 
244
205
  evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, @config, true)
245
206
  start = Time.now
207
+ impressions = []
246
208
  treatments_labels_change_numbers =
247
209
  @splits_repository.get_splits(sanitized_split_names).each_with_object({}) do |(name, data), memo|
248
- memo.merge!(name => treatment(key, name, attributes, data, false, true, evaluator))
210
+ memo.merge!(name => treatment(key, name, attributes, data, false, true, evaluator, calling_method, impressions))
249
211
  end
250
212
  latency = (Time.now - start) * 1000.0
251
213
  # Measure
252
214
  @metrics.time('sdk.' + calling_method, latency)
253
215
 
254
- treatments_for_impressions = get_treatment_for_impressions(treatments_labels_change_numbers)
255
-
256
- time = (Time.now.to_f * 1000.0).to_i
257
- @impressions_repository.add_bulk(
258
- matching_key, bucketing_key, treatments_for_impressions, time
259
- ) unless treatments_for_impressions == {}
260
-
261
- route_impressions(sanitized_split_names, matching_key, bucketing_key, time, treatments_for_impressions, attributes)
216
+ @impressions_manager.track(impressions)
262
217
 
263
218
  split_names_keys = treatments_labels_change_numbers.keys
264
219
  treatments = treatments_labels_change_numbers.values.map do |v|
@@ -284,7 +239,7 @@ module SplitIoClient
284
239
  # @return [String/Hash] Treatment as String or Hash of treatments in case of array of features
285
240
  def treatment(
286
241
  key, split_name, attributes = {}, split_data = nil, store_impressions = true,
287
- multiple = false, evaluator = nil, calling_method = 'get_treatment'
242
+ multiple = false, evaluator = nil, calling_method = 'get_treatment', impressions = []
288
243
  )
289
244
  control_treatment = { treatment: Engine::Models::Treatment::CONTROL }
290
245
 
@@ -329,15 +284,18 @@ module SplitIoClient
329
284
  end
330
285
 
331
286
  latency = (Time.now - start) * 1000.0
332
-
333
- store_impression(split_name, matching_key, bucketing_key, treatment_data, attributes) if store_impressions
287
+
288
+ impression = @impressions_manager.build_impression(matching_key, bucketing_key, split_name, treatment_data, { attributes: attributes, time: nil })
289
+ impressions << impression unless impression.nil?
334
290
 
335
291
  # Measure
336
292
  @metrics.time('sdk.' + calling_method, latency) unless multiple
337
293
  rescue StandardError => error
294
+ p error
338
295
  @config.log_found_exception(__method__.to_s, error)
339
296
 
340
- store_impression(split_name, matching_key, bucketing_key, control_treatment, attributes) if store_impressions
297
+ impression = @impressions_manager.build_impression(matching_key, bucketing_key, split_name, control_treatment, { attributes: attributes, time: nil })
298
+ impressions << impression unless impression.nil?
341
299
 
342
300
  return parsed_treatment(multiple, control_treatment.merge({ label: Engine::Models::Label::EXCEPTION }))
343
301
  end
@@ -357,12 +315,5 @@ module SplitIoClient
357
315
  def parsed_attributes(attributes)
358
316
  return attributes || attributes.to_h
359
317
  end
360
-
361
- def get_treatment_for_impressions(treatments_labels_change_numbers)
362
- return treatments_labels_change_numbers.select{|imp|
363
- treatments_labels_change_numbers[imp][:label] != Engine::Models::Label::NOT_FOUND &&
364
- !treatments_labels_change_numbers[imp][:label].nil?
365
- }
366
- end
367
318
  end
368
319
  end
@@ -14,16 +14,31 @@ module SplitIoClient
14
14
  return
15
15
  end
16
16
 
17
- impressions_by_ip(impressions).each do |ip, impressions_ip|
18
- response = post_api("#{@config.events_uri}/testImpressions/bulk", @api_key, impressions_ip)
19
-
20
- if response.success?
21
- @config.split_logger.log_if_debug("Impressions reported: #{total_impressions(impressions)}")
22
- else
23
- @config.logger.error("Unexpected status code while posting impressions: #{response.status}." \
24
- ' - Check your API key and base URI')
25
- raise 'Split SDK failed to connect to backend to post impressions'
26
- end
17
+ response = post_api("#{@config.events_uri}/testImpressions/bulk", @api_key, impressions, impressions_headers)
18
+
19
+ if response.success?
20
+ @config.split_logger.log_if_debug("Impressions reported: #{total_impressions(impressions)}")
21
+ else
22
+ @config.logger.error("Unexpected status code while posting impressions: #{response.status}." \
23
+ ' - Check your API key and base URI')
24
+ raise 'Split SDK failed to connect to backend to post impressions'
25
+ end
26
+ end
27
+
28
+ def post_count(impressions_count)
29
+ if impressions_count.nil? || impressions_count[:pf].empty?
30
+ @config.split_logger.log_if_debug('No impressions count to send')
31
+ return
32
+ end
33
+
34
+ response = post_api("#{@config.events_uri}/testImpressions/count", @api_key, impressions_count)
35
+
36
+ if response.success?
37
+ @config.split_logger.log_if_debug("Impressions count sent: #{impressions_count[:pf].length}")
38
+ else
39
+ @config.logger.error("Unexpected status code while posting impressions count: #{response.status}." \
40
+ ' - Check your API key and base URI')
41
+ raise 'Split SDK failed to connect to backend to post impressions'
27
42
  end
28
43
  end
29
44
 
@@ -31,14 +46,16 @@ module SplitIoClient
31
46
  return 0 if impressions.nil?
32
47
 
33
48
  impressions.reduce(0) do |impressions_count, impression|
34
- impressions_count += impression[:keyImpressions].length
49
+ impressions_count += impression[:i].length
35
50
  end
36
51
  end
37
52
 
38
53
  private
39
54
 
40
- def impressions_by_ip(impressions)
41
- impressions.group_by { |impression| impression[:ip] }
55
+ def impressions_headers
56
+ {
57
+ 'SplitSDKImpressionsMode' => @config.impressions_mode.to_s
58
+ }
42
59
  end
43
60
  end
44
61
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module SplitIoClient
6
+ module Engine
7
+ module Common
8
+ TIME_INTERVAL_MS = 3600 * 1000
9
+
10
+ class ImpressionCounter
11
+ DEFAULT_AMOUNT = 1
12
+
13
+ def initialize
14
+ @cache = Concurrent::Hash.new
15
+ end
16
+
17
+ def inc(split_name, time_frame)
18
+ key = make_key(split_name, time_frame)
19
+
20
+ current_amount = @cache[key]
21
+ @cache[key] = current_amount.nil? ? DEFAULT_AMOUNT : (current_amount + DEFAULT_AMOUNT)
22
+ end
23
+
24
+ def pop_all
25
+ to_return = Concurrent::Hash.new
26
+
27
+ @cache.each do |key, value|
28
+ to_return[key] = value
29
+ end
30
+ @cache.clear
31
+
32
+ to_return
33
+ end
34
+
35
+ def truncate_time_frame(timestamp_ms)
36
+ timestamp_ms - (timestamp_ms % TIME_INTERVAL_MS)
37
+ end
38
+
39
+ def make_key(split_name, time_frame)
40
+ "#{split_name}::#{truncate_time_frame(time_frame)}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Common
6
+ class ImpressionManager
7
+ def initialize(config, impressions_repository, impression_counter)
8
+ @config = config
9
+ @impressions_repository = impressions_repository
10
+ @impression_counter = impression_counter
11
+ @impression_router = SplitIoClient::ImpressionRouter.new(@config)
12
+ @impression_observer = SplitIoClient::Observers::ImpressionObserver.new
13
+ end
14
+
15
+ # added param time for test
16
+ def build_impression(matching_key, bucketing_key, split_name, treatment, params = {})
17
+ impression_data = impression_data(matching_key, bucketing_key, split_name, treatment, params[:time])
18
+
19
+ impression_data[:pt] = @impression_observer.test_and_set(impression_data) unless redis?
20
+
21
+ return impression_optimized(split_name, impression_data, params[:attributes]) if optimized? && !redis?
22
+
23
+ impression(impression_data, params[:attributes])
24
+ rescue StandardError => error
25
+ @config.log_found_exception(__method__.to_s, error)
26
+ end
27
+
28
+ def track(impressions)
29
+ @impressions_repository.add_bulk(impressions)
30
+ @impression_router.add_bulk(impressions)
31
+ rescue StandardError => error
32
+ @config.log_found_exception(__method__.to_s, error)
33
+ end
34
+
35
+ private
36
+
37
+ # added param time for test
38
+ def impression_data(matching_key, bucketing_key, split_name, treatment, time = nil)
39
+ {
40
+ k: matching_key,
41
+ b: bucketing_key,
42
+ f: split_name,
43
+ t: treatment[:treatment],
44
+ r: applied_rule(treatment[:label]),
45
+ c: treatment[:change_number],
46
+ m: time || (Time.now.to_f * 1000.0).to_i,
47
+ pt: nil
48
+ }
49
+ end
50
+
51
+ def metadata
52
+ {
53
+ s: "#{@config.language}-#{@config.version}",
54
+ i: @config.machine_ip,
55
+ n: @config.machine_name
56
+ }
57
+ end
58
+
59
+ def applied_rule(label)
60
+ @config.labels_enabled ? label : nil
61
+ end
62
+
63
+ def optimized?
64
+ @config.impressions_mode == :optimized
65
+ end
66
+
67
+ def impression_optimized(split_name, impression_data, attributes)
68
+ @impression_counter.inc(split_name, impression_data[:m])
69
+
70
+ impression(impression_data, attributes) if should_queue_impression?(impression_data)
71
+ end
72
+
73
+ def should_queue_impression?(impression)
74
+ impression[:pt].nil? || (impression[:pt] < ((Time.now.to_f * 1000.0).to_i - Common::TIME_INTERVAL_MS))
75
+ end
76
+
77
+ def impression(impression_data, attributes)
78
+ { m: metadata, i: impression_data, attributes: attributes }
79
+ end
80
+
81
+ def redis?
82
+ @config.impressions_adapter.class.to_s == 'SplitIoClient::Cache::Adapters::RedisAdapter'
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,7 +1,3 @@
1
- unless defined?(JRUBY_VERSION)
2
- require 'digest/murmurhash'
3
- end
4
-
5
1
  module SplitIoClient
6
2
  # Misc class in charge of providing hash functions and
7
3
  # determination of treatment based on concept of buckets
@@ -13,7 +9,7 @@ module SplitIoClient
13
9
  when 'java'
14
10
  Proc.new { |key, seed| Java::MurmurHash3.murmurhash3_x86_32(key, seed) }
15
11
  else
16
- Proc.new { |key, seed| Digest::MurmurHash3_x86_32.rawdigest(key, [seed].pack('L')) }
12
+ Proc.new { |key, seed| Digest::MurmurHashMRI3_x86_32.rawdigest(key, [seed].pack('L')) }
17
13
  end
18
14
  end
19
15
 
@@ -1,7 +1,3 @@
1
- unless defined?(JRUBY_VERSION)
2
- require 'digest/murmurhash'
3
- end
4
-
5
1
  module SplitIoClient
6
2
  module Engine
7
3
  module Parser
@@ -9,14 +9,13 @@ module SplitIoClient
9
9
  repositories,
10
10
  api_key,
11
11
  config,
12
- sdk_blocker,
13
- metrics
12
+ params
14
13
  )
15
- split_fetcher = SplitFetcher.new(repositories[:splits], api_key, metrics, config, sdk_blocker)
16
- segment_fetcher = SegmentFetcher.new(repositories[:segments], api_key, metrics, config, sdk_blocker)
17
- sync_params = { split_fetcher: split_fetcher, segment_fetcher: segment_fetcher }
14
+ split_fetcher = SplitFetcher.new(repositories[:splits], api_key, params[:metrics], config, params[:sdk_blocker])
15
+ segment_fetcher = SegmentFetcher.new(repositories[:segments], api_key, params[:metrics], config, params[:sdk_blocker])
16
+ sync_params = { split_fetcher: split_fetcher, segment_fetcher: segment_fetcher, imp_counter: params[:impression_counter] }
18
17
 
19
- @synchronizer = Synchronizer.new(repositories, api_key, config, sdk_blocker, sync_params)
18
+ @synchronizer = Synchronizer.new(repositories, api_key, config, params[:sdk_blocker], sync_params)
20
19
  notification_manager_keeper = SplitIoClient::SSE::NotificationManagerKeeper.new(config) do |manager|
21
20
  manager.on_occupancy { |publisher_available| process_occupancy(publisher_available) }
22
21
  manager.on_push_shutdown { process_push_shutdown }
@@ -23,6 +23,8 @@ module SplitIoClient
23
23
  @sdk_blocker = sdk_blocker
24
24
  @split_fetcher = params[:split_fetcher]
25
25
  @segment_fetcher = params[:segment_fetcher]
26
+ @impressions_api = SplitIoClient::Api::Impressions.new(@api_key, @config)
27
+ @impression_counter = params[:imp_counter]
26
28
  end
27
29
 
28
30
  def sync_all
@@ -35,6 +37,7 @@ module SplitIoClient
35
37
  impressions_sender
36
38
  metrics_sender
37
39
  events_sender
40
+ impressions_count_sender
38
41
  end
39
42
 
40
43
  def start_periodic_fetch
@@ -73,7 +76,7 @@ module SplitIoClient
73
76
 
74
77
  # Starts thread which loops constantly and sends impressions to the Split API
75
78
  def impressions_sender
76
- ImpressionsSender.new(@impressions_repository, @api_key, @config).call
79
+ ImpressionsSender.new(@impressions_repository, @config, @impressions_api).call
77
80
  end
78
81
 
79
82
  # Starts thread which loops constantly and sends metrics to the Split API
@@ -85,6 +88,11 @@ module SplitIoClient
85
88
  def events_sender
86
89
  EventsSender.new(@events_repository, @config).call
87
90
  end
91
+
92
+ # Starts thread which loops constantly and sends impressions count to the Split API
93
+ def impressions_count_sender
94
+ ImpressionsCountSender.new(@config, @impression_counter, @impressions_api).call
95
+ end
88
96
  end
89
97
  end
90
98
  end
@@ -53,7 +53,9 @@ module SplitIoClient
53
53
  @segments_refresh_rate = opts[:segments_refresh_rate] || SplitConfig.default_segments_refresh_rate
54
54
  @metrics_refresh_rate = opts[:metrics_refresh_rate] || SplitConfig.default_metrics_refresh_rate
55
55
 
56
- @impressions_refresh_rate = opts[:impressions_refresh_rate] || SplitConfig.default_impressions_refresh_rate
56
+ @impressions_mode = init_impressions_mode(opts[:impressions_mode])
57
+
58
+ @impressions_refresh_rate = SplitConfig.init_impressions_refresh_rate(@impressions_mode, opts[:impressions_refresh_rate], SplitConfig.default_impressions_refresh_rate)
57
59
  @impressions_queue_size = opts[:impressions_queue_size] || SplitConfig.default_impressions_queue_size
58
60
  @impressions_adapter = SplitConfig.init_cache_adapter(
59
61
  opts[:cache_adapter] || SplitConfig.default_cache_adapter, :queue_adapter, @impressions_queue_size, @redis_url
@@ -270,6 +272,30 @@ module SplitIoClient
270
272
 
271
273
  attr_accessor :streaming_enabled
272
274
 
275
+ attr_accessor :impressions_mode
276
+
277
+ def self.default_impressions_mode
278
+ :optimized
279
+ end
280
+
281
+ def init_impressions_mode(impressions_mode)
282
+ impressions_mode ||= SplitConfig.default_impressions_mode
283
+
284
+ case impressions_mode
285
+ when :debug
286
+ return :debug
287
+ else
288
+ @logger.error('You passed an invalid impressions_mode, impressions_mode should be one of the following values: :debug or :optimized. Defaulting to :optimized mode') unless impressions_mode == :optimized
289
+ return :optimized
290
+ end
291
+ end
292
+
293
+ def self.init_impressions_refresh_rate(impressions_mode, refresh_rate, default_rate)
294
+ return (refresh_rate.nil? || refresh_rate <= 0 ? default_rate : refresh_rate) if impressions_mode == :debug
295
+
296
+ return refresh_rate.nil? || refresh_rate <= 0 ? SplitConfig.default_impressions_refresh_rate_optimized : [default_rate, refresh_rate].max
297
+ end
298
+
273
299
  def self.default_streaming_enabled
274
300
  true
275
301
  end
@@ -387,6 +413,10 @@ module SplitIoClient
387
413
  60
388
414
  end
389
415
 
416
+ def self.default_impressions_refresh_rate_optimized
417
+ 300
418
+ end
419
+
390
420
  def self.default_impression_listener_refresh_rate
391
421
  0
392
422
  end
@@ -34,10 +34,12 @@ module SplitIoClient
34
34
  @metrics_repository = MetricsRepository.new(@config)
35
35
  @sdk_blocker = SDKBlocker.new(@splits_repository, @segments_repository, @config)
36
36
  @metrics = Metrics.new(100, @metrics_repository)
37
+ @impression_counter = SplitIoClient::Engine::Common::ImpressionCounter.new
38
+ @impressions_manager = SplitIoClient::Engine::Common::ImpressionManager.new(@config, @impressions_repository, @impression_counter)
37
39
 
38
40
  start!
39
41
 
40
- @client = SplitClient.new(@api_key, @metrics, @splits_repository, @segments_repository, @impressions_repository, @metrics_repository, @events_repository, @sdk_blocker, @config)
42
+ @client = SplitClient.new(@api_key, @metrics, @splits_repository, @segments_repository, @impressions_repository, @metrics_repository, @events_repository, @sdk_blocker, @config, @impressions_manager)
41
43
  @manager = SplitManager.new(@splits_repository, @sdk_blocker, @config)
42
44
 
43
45
  validate_api_key
@@ -51,7 +53,8 @@ module SplitIoClient
51
53
  if @config.localhost_mode
52
54
  start_localhost_components
53
55
  else
54
- SplitIoClient::Engine::SyncManager.new(repositories, @api_key, @config, @sdk_blocker, @metrics).start
56
+ params = { sdk_blocker: @sdk_blocker, metrics: @metrics, impression_counter: @impression_counter }
57
+ SplitIoClient::Engine::SyncManager.new(repositories, @api_key, @config, params).start
55
58
  end
56
59
  end
57
60
 
@@ -1,3 +1,3 @@
1
1
  module SplitIoClient
2
- VERSION = '7.1.4.pre.rc3'
2
+ VERSION = '7.1.4.pre.rc8'
3
3
  end
@@ -26,7 +26,14 @@ Gem::Specification.new do |spec|
26
26
  lib/murmurhash/murmurhash.jar]
27
27
  )
28
28
  else
29
- spec.add_runtime_dependency 'digest-murmurhash', '>= 1.1.1'
29
+ spec.files.concat(
30
+ %w[ext/murmurhash/3_x86_32.c
31
+ ext/murmurhash/3_x64_128.c
32
+ ext/murmurhash/extconf.rb
33
+ ext/murmurhash/murmurhash.c
34
+ ext/murmurhash/murmurhash.h]
35
+ )
36
+ spec.extensions = ['ext/murmurhash/extconf.rb']
30
37
  end
31
38
 
32
39
  spec.add_development_dependency 'allocation_stats'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: splitclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.4.pre.rc3
4
+ version: 7.1.4.pre.rc8
5
5
  platform: java
6
6
  authors:
7
7
  - Split Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-26 00:00:00.000000000 Z
11
+ date: 2020-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -337,6 +337,8 @@ files:
337
337
  - lib/splitclient-rb/cache/adapters/redis_adapter.rb
338
338
  - lib/splitclient-rb/cache/fetchers/segment_fetcher.rb
339
339
  - lib/splitclient-rb/cache/fetchers/split_fetcher.rb
340
+ - lib/splitclient-rb/cache/hashers/impression_hasher.rb
341
+ - lib/splitclient-rb/cache/observers/impression_observer.rb
340
342
  - lib/splitclient-rb/cache/repositories/events/memory_repository.rb
341
343
  - lib/splitclient-rb/cache/repositories/events/redis_repository.rb
342
344
  - lib/splitclient-rb/cache/repositories/events_repository.rb
@@ -351,6 +353,7 @@ files:
351
353
  - lib/splitclient-rb/cache/repositories/splits_repository.rb
352
354
  - lib/splitclient-rb/cache/routers/impression_router.rb
353
355
  - lib/splitclient-rb/cache/senders/events_sender.rb
356
+ - lib/splitclient-rb/cache/senders/impressions_count_sender.rb
354
357
  - lib/splitclient-rb/cache/senders/impressions_formatter.rb
355
358
  - lib/splitclient-rb/cache/senders/impressions_sender.rb
356
359
  - lib/splitclient-rb/cache/senders/localhost_repo_cleaner.rb
@@ -370,6 +373,8 @@ files:
370
373
  - lib/splitclient-rb/engine/api/segments.rb
371
374
  - lib/splitclient-rb/engine/api/splits.rb
372
375
  - lib/splitclient-rb/engine/auth_api_client.rb
376
+ - lib/splitclient-rb/engine/common/impressions_counter.rb
377
+ - lib/splitclient-rb/engine/common/impressions_manager.rb
373
378
  - lib/splitclient-rb/engine/evaluator/splitter.rb
374
379
  - lib/splitclient-rb/engine/matchers/all_keys_matcher.rb
375
380
  - lib/splitclient-rb/engine/matchers/between_matcher.rb