airship-ruby 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airship-ruby.rb +863 -7
  3. metadata +44 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab833704c33a585663f50c324eb3043069f89c7e
4
- data.tar.gz: 7b93a9fb4efba8fec265007cb0d6481cd731fa39
3
+ metadata.gz: 1ba365d15a83bb21a63473b1041622a071bd82af
4
+ data.tar.gz: 4d603e416b360b645f3cb62879aca664de6bf2bc
5
5
  SHA512:
6
- metadata.gz: 0a0699a91237de60295b82c29f08962c95716c0d0a56e2e3a8a975e4475d266cde7d32fd44a9341d040ca814076ef3f8c59dc589006b18f63148c67807737817
7
- data.tar.gz: c7729d15a5074f3e62910f6b5363f6810e66bc8847bf49d2b53d83f07302eec273e6dece6d981778b41686c733351cffb1f90ff0a27475782722966ff5c1eeb6
6
+ metadata.gz: 126756844ae45837c019c66ed7c5a229c339cf09fdfddeb9f26141a5fbc7628f16bf80c3e091fb463a719201d7da9c4348f69a64dd8ea1f83b5994e8f45e1072
7
+ data.tar.gz: f620cbba80339307b196c3d3565db427c85130156f9c6ccfddd4577002b1fe75fc3efae1ca2c7ac6fd9d9363e951af06b106231bf5e715d9b5f48f7157916e84
@@ -1,17 +1,873 @@
1
1
  require 'faraday'
2
2
  require 'json'
3
+ require 'concurrent'
4
+ require 'digest'
5
+ require 'rubygems'
6
+ require 'json-schema'
7
+ require 'time'
8
+ require 'date'
9
+
3
10
 
4
11
  class Airship
12
+ SCHEMA = {
13
+ "type" => "object",
14
+ "properties" => {
15
+ "type" => {
16
+ "type" => "string",
17
+ "pattern" => "^([A-Z][a-zA-Z]*)+$",
18
+ "maxLength" => 50,
19
+ },
20
+ "is_group" => {
21
+ "type" => "boolean",
22
+ },
23
+ "id" => {
24
+ "type" => "string",
25
+ "maxLength" => 250,
26
+ "minLength" => 1,
27
+ },
28
+ "display_name" => {
29
+ "type" => "string",
30
+ "maxLength" => 250,
31
+ "minLength" => 1,
32
+ },
33
+ "attributes" => {
34
+ "type" => "object",
35
+ "patternProperties" => {
36
+ "^[a-zA-Z][a-zA-Z_]{0,48}[a-zA-Z]$" => {
37
+ "oneOf" => [
38
+ {
39
+ "type" => "string",
40
+ "maxLength" => 3000,
41
+ },
42
+ {
43
+ "type" => "boolean"
44
+ },
45
+ {
46
+ "type" => "number"
47
+ },
48
+ ],
49
+ },
50
+ },
51
+ "maxProperties" => 100,
52
+ "additionalProperties" => false,
53
+ },
54
+ "group" => {
55
+ "type" => ["object", "null"],
56
+ "properties" => {
57
+ "type" => {
58
+ "type" => "string",
59
+ "pattern" => "^([A-Z][a-zA-Z]*)+$",
60
+ "maxLength" => 50,
61
+ },
62
+ "is_group" => {
63
+ "type" => "boolean",
64
+ "enum" => [true],
65
+ },
66
+ "id" => {
67
+ "type" => "string",
68
+ "maxLength" => 250,
69
+ "minLength" => 1,
70
+ },
71
+ "display_name" => {
72
+ "type" => "string",
73
+ "maxLength" => 250,
74
+ "minLength" => 1,
75
+ },
76
+ "attributes" => {
77
+ "type" => "object",
78
+ "patternProperties" => {
79
+ "^[a-zA-Z][a-zA-Z_]{0,48}[a-zA-Z]$" => {
80
+ "oneOf" => [
81
+ {
82
+ "type" => "string",
83
+ "maxLength" => 3000,
84
+ },
85
+ {
86
+ "type" => "boolean"
87
+ },
88
+ {
89
+ "type" => "number"
90
+ },
91
+ ],
92
+ },
93
+ },
94
+ "maxProperties" => 100,
95
+ "additionalProperties" => false,
96
+ },
97
+ },
98
+ "required" => ["id", "display_name"],
99
+ "additionalProperties" => false,
100
+ },
101
+ },
102
+ "required" => ["type", "id", "display_name"],
103
+ "additionalProperties" => false,
104
+ }
105
+
106
+ SERVER_URL = 'https://api.airshiphq.com'
107
+ IDENTIFY_ENDPOINT = "#{SERVER_URL}/v1/identify"
108
+ GATING_INFO_ENDPOINT = "#{SERVER_URL}/v1/gating-info"
109
+ PLATFORM = 'ruby'
110
+ VERSION = Gem::Specification::load(
111
+ File.join(
112
+ File.dirname(
113
+ File.dirname(
114
+ File.expand_path(__FILE__)
115
+ )
116
+ ),
117
+ 'airship-ruby.gemspec'
118
+ )
119
+ ).version.to_s
120
+
121
+ SDK_VERSION = "#{PLATFORM}:#{VERSION}"
122
+
123
+ CONTROL_TYPE_BOOLEAN = 'boolean'
124
+ CONTROL_TYPE_MULTIVARIATE = 'multivariate'
125
+
126
+ DISTRIBUTION_TYPE_RULE_BASED = 'R'
127
+ DISTRIBUTION_TYPE_PERCENTAGE_BASED = 'P'
128
+
129
+ OBJECT_ATTRIBUTE_TYPE_STRING = 'STRING'
130
+ OBJECT_ATTRIBUTE_TYPE_INT = 'INT'
131
+ OBJECT_ATTRIBUTE_TYPE_FLOAT = 'FLOAT'
132
+ OBJECT_ATTRIBUTE_TYPE_BOOLEAN = 'BOOLEAN'
133
+ OBJECT_ATTRIBUTE_TYPE_DATE = 'DATE'
134
+ OBJECT_ATTRIBUTE_TYPE_DATETIME = 'DATETIME'
135
+
136
+ RULE_OPERATOR_TYPE_IS = 'IS'
137
+ RULE_OPERATOR_TYPE_IS_NOT = 'IS_NOT'
138
+ RULE_OPERATOR_TYPE_IN = 'IN'
139
+ RULE_OPERATOR_TYPE_NOT_IN = 'NOT_IN'
140
+ RULE_OPERATOR_TYPE_LT = 'LT'
141
+ RULE_OPERATOR_TYPE_LTE = 'LTE'
142
+ RULE_OPERATOR_TYPE_GT = 'GT'
143
+ RULE_OPERATOR_TYPE_GTE = 'GTE'
144
+ RULE_OPERATOR_TYPE_FROM = 'FROM'
145
+ RULE_OPERATOR_TYPE_UNTIL = 'UNTIL'
146
+ RULE_OPERATOR_TYPE_AFTER = 'AFTER'
147
+ RULE_OPERATOR_TYPE_BEFORE = 'BEFORE'
148
+
149
+ @@sdk_id = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten.sample(6).join('')
150
+
151
+ class << self
152
+ def get_hashed_value(s)
153
+ Digest::MD5.hexdigest(s).to_i(base=16).fdiv(340282366920938463463374607431768211455)
154
+ end
155
+ end
156
+
5
157
  def initialize(options)
6
- @gatingInfo = nil
7
- @gatingInfoThread = nil
8
158
 
9
- @gatingInfoMap = nil
159
+ @api_key = options[:api_key]
160
+ @env_key = options[:env_key]
161
+
162
+ if @api_key.nil?
163
+ raise Exception.new('Missing api_key')
164
+ end
165
+
166
+ if @env_key.nil?
167
+ raise Exception.new('Missing env_key')
168
+ end
169
+
170
+ @gating_info = nil
171
+ @gating_info_downloader_task = nil
172
+
173
+ @gating_info_map = nil
174
+
175
+ @max_gate_stats_batch_size = 500
176
+ @gate_stats_upload_batch_interval = 60
177
+
178
+ @gate_stats_watcher = nil
179
+ @gate_stats_last_task_scheduled_timestamp = Concurrent::AtomicFixnum.new(0)
180
+
181
+ @gate_stats_uploader_tasks = []
182
+
183
+ @gate_stats_batch = []
184
+
185
+ @gate_stats_batch_lock = Concurrent::Semaphore.new(1)
186
+ end
187
+
188
+ def init
189
+ # Not thread safe yet
190
+ if @gating_info_downloader_task.nil?
191
+ @gating_info_downloader_task = self._create_poller
192
+ @gating_info_downloader_task.execute
193
+ end
194
+
195
+ if @gate_stats_watcher.nil?
196
+ @gate_stats_watcher = self._create_watcher
197
+ @gate_stats_watcher.execute
198
+ end
199
+ # Thread safe after this point
200
+ end
201
+
202
+ def _get_gating_info_map(gating_info)
203
+ map = {}
204
+
205
+ controls = gating_info['controls']
206
+
207
+ controls.each do |control|
208
+ control_info = {}
209
+
210
+ control_info['id'] = control['id']
211
+ control_info['is_on'] = control['is_on']
212
+ control_info['rule_based_distribution_default_variation'] = control['rule_based_distribution_default_variation']
213
+ control_info['rule_sets'] = control['rule_sets']
214
+ control_info['distributions'] = control['distributions']
215
+ control_info['type'] = control['type']
216
+ control_info['default_variation'] = control['default_variation']
217
+
218
+ enablements = control['enablements']
219
+ enablements_info = {}
220
+
221
+ enablements.each do |enablement|
222
+ client_identities_map = enablements_info[enablement['client_object_type_name']]
223
+
224
+ if client_identities_map.nil?
225
+ enablements_info[enablement['client_object_type_name']] = {}
226
+ end
227
+
228
+ enablements_info[enablement['client_object_type_name']][enablement['client_object_identity']] = [enablement['is_enabled'], enablement['variation']]
229
+ end
230
+
231
+ control_info['enablements_info'] = enablements_info
232
+
233
+ map[control['short_name']] = control_info
234
+ end
235
+
236
+ map
237
+ end
238
+
239
+ def _create_poller
240
+ Concurrent::TimerTask.new(execution_interval: 60, timeout_interval: 10, run_now: true) do |task|
241
+ conn = Faraday.new(url: "#{GATING_INFO_ENDPOINT}/#{@env_key}")
242
+ response = conn.get do |req|
243
+ req.options.timeout = 10
244
+ req.headers['api-key'] = @api_key
245
+ end
246
+ if response.status == 200
247
+ gating_info = JSON.parse(response.body)
248
+ gating_info_map = self._get_gating_info_map(gating_info)
249
+ @gating_info = gating_info
250
+ @gating_info_map = gating_info_map
251
+ end
252
+ end
253
+ end
254
+
255
+ def _create_watcher
256
+ Concurrent::TimerTask.new(execution_interval: @gate_stats_upload_batch_interval, timeout_interval: 10, run_now: true) do |task|
257
+ now = Time.now.utc.to_i
258
+ if now - @gate_stats_last_task_scheduled_timestamp.value >= @gate_stats_upload_batch_interval
259
+ processed = self._process_batch(0)
260
+ if processed
261
+ @gate_stats_last_task_scheduled_timestamp.value = now
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ def _create_processor(batch)
268
+ return Concurrent::ScheduledTask.execute(0) do |task|
269
+ conn = Faraday.new(url: IDENTIFY_ENDPOINT)
270
+ response = conn.post do |req|
271
+ req.options.timeout = 10
272
+ req.headers['Content-Type'] = 'application/json'
273
+ req.headers['api-key'] = @api_key
274
+ req.body = JSON.generate({
275
+ 'env_key' => @env_key,
276
+ 'objects' => batch,
277
+ })
278
+ end
279
+ end
280
+ end
281
+
282
+ def _process_batch(limit, gate_stats=nil)
283
+ # This is sort of a weird function.
284
+ # We process the batch if the batch size
285
+ # is more than limit. The second param
286
+ # allows for an additional gate_states to
287
+ # be inserted before the processing check
288
+ # is performed.
289
+
290
+ processed = false
291
+ @gate_stats_batch_lock.acquire
292
+ if !gate_stats.nil?
293
+ @gate_stats_batch.push(gate_stats)
294
+ end
295
+ if @gate_stats_batch.size > limit
296
+ new_gate_stats_uploader_tasks = []
297
+ @gate_stats_uploader_tasks.each do |task|
298
+ if !task.fulfilled?
299
+ new_gate_stats_uploader_tasks.push(task)
300
+ end
301
+ end
302
+ old_batch = @gate_stats_batch
303
+ @gate_stats_batch = []
304
+ new_gate_stats_uploader_tasks.push(self._create_processor(old_batch))
305
+ @gate_stats_uploader_tasks = new_gate_stats_uploader_tasks
306
+ processed = true
307
+ end
308
+ @gate_stats_batch_lock.release
309
+ processed
310
+ end
311
+
312
+ def _upload_stats_async(gate_stats)
313
+ processed = self._process_batch(@max_gate_stats_batch_size - 1, gate_stats)
314
+ if processed
315
+ now = Time.now.utc.to_i
316
+ @gate_stats_last_task_scheduled_timestamp.value = now
317
+ end
318
+ end
319
+
320
+ def _satisfies_rule(rule, object)
321
+ attribute_type = rule['attribute_type']
322
+ operator = rule['operator']
323
+ attribute_name = rule['attribute_name']
324
+ value = rule['value']
325
+ value_list = rule['value_list']
326
+
327
+ if object['attributes'].nil? || object['attributes'][attribute_name].nil?
328
+ return false
329
+ end
330
+
331
+ attribute_val = object['attributes'][attribute_name]
332
+
333
+ if attribute_type == OBJECT_ATTRIBUTE_TYPE_STRING
334
+ if operator == RULE_OPERATOR_TYPE_IS
335
+ return attribute_val == value
336
+ elsif operator == RULE_OPERATOR_TYPE_IS_NOT
337
+ return attribute_val != value
338
+ elsif operator == RULE_OPERATOR_TYPE_IN
339
+ return !value_list.index(attribute_val).nil?
340
+ elsif operator == RULE_OPERATOR_TYPE_NOT_IN
341
+ return value_list.index(attribute_val).nil?
342
+ else
343
+ return false
344
+ end
345
+ elsif attribute_type == OBJECT_ATTRIBUTE_TYPE_INT
346
+ value = value && value.to_i
347
+ value_list = value_list && value_list.map { |v| v.to_i }
348
+
349
+ if operator == RULE_OPERATOR_TYPE_IS
350
+ return attribute_val == value
351
+ elsif operator == RULE_OPERATOR_TYPE_IS_NOT
352
+ return attribute_val != value
353
+ elsif operator == RULE_OPERATOR_TYPE_IN
354
+ return !value_list.index(attribute_val).nil?
355
+ elsif operator == RULE_OPERATOR_TYPE_NOT_IN
356
+ return value_list.index(attribute_val).nil?
357
+ elsif operator == RULE_OPERATOR_TYPE_LT
358
+ return attribute_val < value
359
+ elsif operator == RULE_OPERATOR_TYPE_LTE
360
+ return attribute_val <= value
361
+ elsif operator == RULE_OPERATOR_TYPE_GT
362
+ return attribute_val > value
363
+ elsif operator == RULE_OPERATOR_TYPE_GTE
364
+ return attribute_val >= value
365
+ else
366
+ return false
367
+ end
368
+ elsif attribute_type == OBJECT_ATTRIBUTE_TYPE_FLOAT
369
+ value = value && value.to_f
370
+ value_list = value_list && value_list.map { |v| v.to_f }
371
+
372
+ if operator == RULE_OPERATOR_TYPE_IS
373
+ return attribute_val == value
374
+ elsif operator == RULE_OPERATOR_TYPE_IS_NOT
375
+ return attribute_val != value
376
+ elsif operator == RULE_OPERATOR_TYPE_IN
377
+ return !value_list.index(attribute_val).nil?
378
+ elsif operator == RULE_OPERATOR_TYPE_NOT_IN
379
+ return value_list.index(attribute_val).nil?
380
+ elsif operator == RULE_OPERATOR_TYPE_LT
381
+ return attribute_val < value
382
+ elsif operator == RULE_OPERATOR_TYPE_LTE
383
+ return attribute_val <= value
384
+ elsif operator == RULE_OPERATOR_TYPE_GT
385
+ return attribute_val > value
386
+ elsif operator == RULE_OPERATOR_TYPE_GTE
387
+ return attribute_val >= value
388
+ else
389
+ return false
390
+ end
391
+ elsif attribute_type == OBJECT_ATTRIBUTE_TYPE_BOOLEAN
392
+ value = (value == 'true') ? true : false
393
+ if operator == RULE_OPERATOR_TYPE_IS
394
+ return attribute_val == value
395
+ elsif operator == RULE_OPERATOR_TYPE_IS_NOT
396
+ return attribute_val != value
397
+ else
398
+ return false
399
+ end
400
+ elsif attribute_type == OBJECT_ATTRIBUTE_TYPE_DATE
401
+ unix_timestamp = nil
402
+ begin
403
+ unix_timestamp = DateTime.parse(attribute_val).to_time.to_i
404
+ rescue Exception => e
405
+ return false
406
+ end
407
+
408
+ iso_format = DateTime.parse(attribute_val).iso8601
409
+
410
+ if !iso_format.end_with?('T00:00:00+00:00')
411
+ return false
412
+ end
413
+
414
+ value = value && DateTime.parse(value).to_time.to_i
415
+ value_list = value_list && value_list.map { |v| DateTime.parse(v).to_time.to_i }
416
+
417
+ attribute_val = unix_timestamp
418
+
419
+ if operator == RULE_OPERATOR_TYPE_IS
420
+ return attribute_val == value
421
+ elsif operator == RULE_OPERATOR_TYPE_IS_NOT
422
+ return attribute_val != value
423
+ elsif operator == RULE_OPERATOR_TYPE_IN
424
+ return !value_list.index(attribute_val).nil?
425
+ elsif operator == RULE_OPERATOR_TYPE_NOT_IN
426
+ return value_list.index(attribute_val).nil?
427
+ elsif operator == RULE_OPERATOR_TYPE_FROM
428
+ return attribute_val >= value
429
+ elsif operator == RULE_OPERATOR_TYPE_UNTIL
430
+ return attribute_val <= value
431
+ elsif operator == RULE_OPERATOR_TYPE_AFTER
432
+ return attribute_val > value
433
+ elsif operator == RULE_OPERATOR_TYPE_BEFORE
434
+ return attribute_val < value
435
+ else
436
+ return false
437
+ end
438
+ elsif attribute_type == OBJECT_ATTRIBUTE_TYPE_DATETIME
439
+ unix_timestamp = nil
440
+ begin
441
+ unix_timestamp = DateTime.parse(attribute_val).to_time.to_i
442
+ rescue Exception => e
443
+ return false
444
+ end
445
+
446
+ value = value && DateTime.parse(value).to_time.to_i
447
+ value_list = value_list && value_list.map { |v| DateTime.parse(v).to_time.to_i }
448
+
449
+ attribute_val = unix_timestamp
450
+
451
+ if operator == RULE_OPERATOR_TYPE_IS
452
+ return attribute_val == value
453
+ elsif operator == RULE_OPERATOR_TYPE_IS_NOT
454
+ return attribute_val != value
455
+ elsif operator == RULE_OPERATOR_TYPE_IN
456
+ return !value_list.index(attribute_val).nil?
457
+ elsif operator == RULE_OPERATOR_TYPE_NOT_IN
458
+ return value_list.index(attribute_val).nil?
459
+ elsif operator == RULE_OPERATOR_TYPE_FROM
460
+ return attribute_val >= value
461
+ elsif operator == RULE_OPERATOR_TYPE_UNTIL
462
+ return attribute_val <= value
463
+ elsif operator == RULE_OPERATOR_TYPE_AFTER
464
+ return attribute_val > value
465
+ elsif operator == RULE_OPERATOR_TYPE_BEFORE
466
+ return attribute_val < value
467
+ else
468
+ return false
469
+ end
470
+ else
471
+ return false
472
+ end
473
+ end
474
+
475
+ def _get_gate_values_for_object(control_info, object)
476
+ if !control_info['enablements_info'][object['type']].nil?
477
+ if !control_info['enablements_info'][object['type']][object['id']].nil?
478
+ is_enabled, variation = control_info['enablements_info'][object['type']][object['id']]
479
+ return {
480
+ 'is_enabled' => is_enabled,
481
+ 'variation' => variation,
482
+ 'is_eligible' => is_enabled,
483
+ '_from_enablement' => true,
484
+ }
485
+ end
486
+ end
487
+
488
+ sampled_inside_base_population = false
489
+ control_info['rule_sets'].each do |rule_set|
490
+ if sampled_inside_base_population
491
+ break
492
+ end
493
+
494
+ rules = rule_set['rules']
495
+
496
+ if rule_set['client_object_type_name'] != object['type']
497
+ next
498
+ end
499
+
500
+ satisfies_all_rules = true
501
+ rules.each do |rule|
502
+ satisfies_all_rules = satisfies_all_rules && self._satisfies_rule(rule, object)
503
+ end
504
+
505
+ if satisfies_all_rules
506
+ hash_key = "SAMPLING:control_#{control_info['id']}:env_#{@gating_info['env']['id']}:rule_set_#{rule_set['id']}:client_object_#{object['type']}_#{object['id']}"
507
+ if Airship.get_hashed_value(hash_key) <= rule_set['sampling_percentage']
508
+ sampled_inside_base_population = true
509
+ end
510
+ end
511
+ end
512
+
513
+ if !sampled_inside_base_population
514
+ return {
515
+ 'is_enabled' => false,
516
+ 'variation' => nil,
517
+ 'is_eligible' => false,
518
+ }
519
+ end
520
+
521
+ if control_info['type'] == CONTROL_TYPE_BOOLEAN
522
+ return {
523
+ 'is_enabled' => true,
524
+ 'variation' => nil,
525
+ 'is_eligible' => true,
526
+ }
527
+ elsif control_info['type'] == CONTROL_TYPE_MULTIVARIATE
528
+ if control_info['distributions'].size == 0
529
+ return {
530
+ 'is_enabled' => true,
531
+ 'variation' => control_info['default_variation'],
532
+ 'is_eligible' => true,
533
+ }
534
+ end
535
+
536
+ percentage_based_distributions = control_info['distributions'].select { |d| d['type'] == DISTRIBUTION_TYPE_PERCENTAGE_BASED }
537
+ rule_based_distributions = control_info['distributions'].select { |d| d['type'] == DISTRIBUTION_TYPE_RULE_BASED }
538
+
539
+ if percentage_based_distributions.size != 0 && rule_based_distributions.size != 0
540
+ puts 'Rule integrity error: please contact support@airshiphq.com'
541
+ return {
542
+ 'is_enabled' => false,
543
+ 'variation' => nil,
544
+ 'is_eligible' => false,
545
+ }
546
+ end
547
+
548
+ if percentage_based_distributions.size != 0
549
+ delta = 0.0001
550
+ sum_percentages = 0.0
551
+ running_percentages = []
552
+ percentage_based_distributions.each do |distribution|
553
+ sum_percentages += distribution['percentage']
554
+ if running_percentages.size == 0
555
+ running_percentages.push(distribution['percentage'])
556
+ else
557
+ running_percentages.push(running_percentages[running_percentages.size - 1] + distribution['percentage'])
558
+ end
559
+ end
560
+
561
+ if (1.0 - sum_percentages).abs > delta
562
+ puts 'Rule integrity error: please contact support@airshiphq.com'
563
+ return {
564
+ 'is_enabled' => false,
565
+ 'variation' => nil,
566
+ 'is_eligible' => false,
567
+ }
568
+ end
569
+
570
+ hash_key = "DISTRIBUTION:control_#{control_info['id']}:env_#{@gating_info['env']['id']}:client_object_#{object['type']}_#{object['id']}"
571
+ hashed_percentage = Airship.get_hashed_value(hash_key)
572
+
573
+ running_percentages.each_with_index do |percentage, i|
574
+ if hashed_percentage <= percentage
575
+ return {
576
+ 'is_enabled' => true,
577
+ 'variation' => percentage_based_distributions[i]['variation'],
578
+ 'is_eligible' => true,
579
+ }
580
+ end
581
+ end
582
+
583
+ return {
584
+ 'is_enabled' => true,
585
+ 'variation' => percentage_based_distributions[percentage_based_distributions.size - 1]['variation'],
586
+ 'is_eligible' => true,
587
+ }
588
+ else
589
+ rule_based_distributions.each do |distribution|
590
+
591
+ rule_set = distribution['rule_set']
592
+ rules = rule_set['rules']
593
+
594
+ if rule_set['client_object_type_name'] != object['type']
595
+ next
596
+ end
597
+
598
+ satisfies_all_rules = true
599
+ rules.each do |rule|
600
+ satisfies_all_rules = satisfies_all_rules && self._satisfies_rule(rule, object)
601
+ end
602
+
603
+ if satisfies_all_rules
604
+ return {
605
+ 'is_enabled' => true,
606
+ 'variation' => distribution['variation'],
607
+ 'is_eligible' => true,
608
+ }
609
+ end
610
+ end
611
+
612
+ return {
613
+ 'is_enabled' => true,
614
+ 'variation' => control_info['rule_based_distribution_default_variation'] || control_info['default_variation'],
615
+ 'is_eligible' => true,
616
+ '_rule_based_default_variation' => true,
617
+ }
618
+ end
619
+ else
620
+ return {
621
+ 'is_enabled' => false,
622
+ 'variation' => nil,
623
+ 'is_eligible' => false,
624
+ }
625
+ end
626
+ end
627
+
628
+ def _get_gate_values(control_short_name, object)
629
+ if @gating_info_map[control_short_name].nil?
630
+ return {
631
+ 'is_enabled' => false,
632
+ 'variation' => nil,
633
+ 'is_eligible' => false,
634
+ '_should_send_stats' => false,
635
+ }
636
+ end
637
+
638
+ control_info = @gating_info_map[control_short_name]
639
+
640
+ if !control_info['is_on']
641
+ return {
642
+ 'is_enabled' => false,
643
+ 'variation' => nil,
644
+ 'is_eligible' => false,
645
+ '_should_send_stats' => true,
646
+ }
647
+ end
648
+
649
+ group = nil
650
+ if !object['group'].nil?
651
+ group = object['group']
652
+ end
653
+
654
+ result = self._get_gate_values_for_object(control_info, object)
655
+
656
+ if !group.nil?
657
+ if group['type'].nil?
658
+ group['type'] = "#{object['type']}Group"
659
+ group['is_group'] = true
660
+ end
661
+ group_result = self._get_gate_values_for_object(control_info, group)
662
+
663
+ if result['_from_enablement'] == true && !result['is_enabled']
664
+ # Do nothing
665
+ elsif result['_from_enablement'] != true && group_result['_from_enablement'] == true && !group_result['is_enabled']
666
+ result['is_enabled'] = group_result['is_enabled']
667
+ result['variation'] = group_result['variation']
668
+ result['is_eligible'] = group_result['is_eligible']
669
+ elsif result['is_enabled']
670
+ if result['_rule_based_default_variation'] == true
671
+ if group_result['is_enabled']
672
+ result['is_enabled'] = group_result['is_enabled']
673
+ result['variation'] = group_result['variation']
674
+ result['is_eligible'] = group_result['is_eligible']
675
+ else
676
+ # Do nothing
677
+ end
678
+ else
679
+ # Do nothing
680
+ end
681
+ elsif group_result['is_enabled']
682
+ result['is_enabled'] = group_result['is_enabled']
683
+ result['variation'] = group_result['variation']
684
+ result['is_eligible'] = group_result['is_eligible']
685
+ else
686
+ # Do nothing
687
+ end
688
+ end
689
+
690
+ result['_should_send_stats'] = true
691
+ result
692
+ end
693
+
694
+ def _clone_object(object)
695
+ copy = object.clone
696
+
697
+ if !object['attributes'].nil?
698
+ copy['attributes'] = object['attributes'].clone
699
+ end
700
+
701
+ if !object['group'].nil?
702
+ copy['group'] = object['group'].clone
703
+
704
+ if !object['group']['attributes'].nil?
705
+ copy['group']['attributes'] = object['group']['attributes'].clone
706
+ end
707
+ end
708
+
709
+ copy
710
+ end
711
+
712
+ def _validate_nesting(object)
713
+ if object['is_group'] == true && !object['group'].nil?
714
+ return 'A group cannot be nested inside another group'
715
+ end
716
+ end
717
+
718
+ def enabled?(control_short_name, object)
719
+ if @gating_info_map.nil?
720
+ return false
721
+ end
722
+
723
+ validation_errors = JSON::Validator.fully_validate(SCHEMA, object)
724
+ if validation_errors.size > 0
725
+ puts validation_errors[0]
726
+ return false
727
+ end
728
+
729
+ object = self._clone_object(object)
730
+
731
+ error = self._validate_nesting(object)
732
+
733
+ if !error.nil?
734
+ puts error
735
+ return false
736
+ end
737
+
738
+ gate_timestamp = Time.now.iso8601
739
+
740
+ start = Time.now
741
+
742
+ gate_values = self._get_gate_values(control_short_name, object)
743
+ is_enabled = gate_values['is_enabled']
744
+ variation = gate_values['variation']
745
+ is_eligible = gate_values['is_eligible']
746
+ _should_send_stats = gate_values['_should_send_stats']
747
+
748
+ finish = Time.now
749
+
750
+ if _should_send_stats
751
+ sdk_gate_timestamp = gate_timestamp
752
+ sdk_gate_latency = "#{(finish - start) * 1000 * 1000}us"
753
+ sdk_version = SDK_VERSION
754
+
755
+ stats = {}
756
+ stats['sdk_gate_control_short_name'] = control_short_name
757
+ stats['sdk_gate_timestamp'] = sdk_gate_timestamp
758
+ stats['sdk_gate_latency'] = sdk_gate_latency
759
+ stats['sdk_version'] = sdk_version
760
+ stats['sdk_id'] = @@sdk_id
761
+
762
+ object['stats'] = stats
763
+
764
+ self._upload_stats_async(object)
765
+ end
766
+
767
+ return is_enabled
768
+ end
769
+
770
+ def variation(control_short_name, object)
771
+ if @gating_info_map.nil?
772
+ return nil
773
+ end
774
+
775
+ validation_errors = JSON::Validator.fully_validate(SCHEMA, object)
776
+ if validation_errors.size > 0
777
+ puts validation_errors[0]
778
+ return nil
779
+ end
780
+
781
+ object = self._clone_object(object)
782
+
783
+ error = self._validate_nesting(object)
784
+
785
+ if !error.nil?
786
+ puts error
787
+ return nil
788
+ end
789
+
790
+ gate_timestamp = Time.now.iso8601
791
+
792
+ start = Time.now
793
+
794
+ gate_values = self._get_gate_values(control_short_name, object)
795
+ is_enabled = gate_values['is_enabled']
796
+ variation = gate_values['variation']
797
+ is_eligible = gate_values['is_eligible']
798
+ _should_send_stats = gate_values['_should_send_stats']
799
+
800
+ finish = Time.now
801
+
802
+ if _should_send_stats
803
+ sdk_gate_timestamp = gate_timestamp
804
+ sdk_gate_latency = "#{(finish - start) * 1000 * 1000}us"
805
+ sdk_version = SDK_VERSION
806
+
807
+ stats = {}
808
+ stats['sdk_gate_control_short_name'] = control_short_name
809
+ stats['sdk_gate_timestamp'] = sdk_gate_timestamp
810
+ stats['sdk_gate_latency'] = sdk_gate_latency
811
+ stats['sdk_version'] = sdk_version
812
+ stats['sdk_id'] = @@sdk_id
813
+
814
+ object['stats'] = stats
815
+
816
+ self._upload_stats_async(object)
817
+ end
818
+
819
+ return variation
820
+ end
821
+
822
+ def eligible?(control_short_name, object)
823
+ if @gating_info_map.nil?
824
+ return false
825
+ end
826
+
827
+ validation_errors = JSON::Validator.fully_validate(SCHEMA, object)
828
+ if validation_errors.size > 0
829
+ puts validation_errors[0]
830
+ return false
831
+ end
832
+
833
+ object = self._clone_object(object)
834
+
835
+ error = self._validate_nesting(object)
836
+
837
+ if !error.nil?
838
+ puts error
839
+ return false
840
+ end
841
+
842
+ gate_timestamp = Time.now.iso8601
843
+
844
+ start = Time.now
845
+
846
+ gate_values = self._get_gate_values(control_short_name, object)
847
+ is_enabled = gate_values['is_enabled']
848
+ variation = gate_values['variation']
849
+ is_eligible = gate_values['is_eligible']
850
+ _should_send_stats = gate_values['_should_send_stats']
851
+
852
+ finish = Time.now
853
+
854
+ if _should_send_stats
855
+ sdk_gate_timestamp = gate_timestamp
856
+ sdk_gate_latency = "#{(finish - start) * 1000 * 1000}us"
857
+ sdk_version = SDK_VERSION
858
+
859
+ stats = {}
860
+ stats['sdk_gate_control_short_name'] = control_short_name
861
+ stats['sdk_gate_timestamp'] = sdk_gate_timestamp
862
+ stats['sdk_gate_latency'] = sdk_gate_latency
863
+ stats['sdk_version'] = sdk_version
864
+ stats['sdk_id'] = @@sdk_id
865
+
866
+ object['stats'] = stats
10
867
 
11
- @maxGateStatsBatchSize = 500
12
- @gateStatsUploadBatchInterval = 60
868
+ self._upload_stats_async(object)
869
+ end
13
870
 
14
- @gateStatsUploadThread = nil
15
- @gateStatsBatch = []
871
+ return is_eligible
16
872
  end
17
873
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airship-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Airship Dev Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-15 00:00:00.000000000 Z
11
+ date: 2018-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -38,6 +38,48 @@ dependencies:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.7.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.5
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.5
55
+ - !ruby/object:Gem::Dependency
56
+ name: public_suffix
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json-schema
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '2.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '2.8'
41
83
  description: Ruby SDK
42
84
  email: support@airshiphq.com
43
85
  executables: []