airship-ruby 0.1.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []