optimizely-sdk 3.9.0 → 4.0.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.
- checksums.yaml +4 -4
- data/LICENSE +202 -202
- data/lib/optimizely/audience.rb +127 -97
- data/lib/optimizely/bucketer.rb +156 -156
- data/lib/optimizely/condition_tree_evaluator.rb +123 -123
- data/lib/optimizely/config/datafile_project_config.rb +539 -508
- data/lib/optimizely/config/proxy_config.rb +34 -34
- data/lib/optimizely/config_manager/async_scheduler.rb +95 -95
- data/lib/optimizely/config_manager/http_project_config_manager.rb +330 -321
- data/lib/optimizely/config_manager/project_config_manager.rb +24 -24
- data/lib/optimizely/config_manager/static_project_config_manager.rb +53 -47
- data/lib/optimizely/decide/optimizely_decide_option.rb +28 -28
- data/lib/optimizely/decide/optimizely_decision.rb +60 -60
- data/lib/optimizely/decide/optimizely_decision_message.rb +26 -26
- data/lib/optimizely/decision_service.rb +563 -500
- data/lib/optimizely/error_handler.rb +39 -39
- data/lib/optimizely/event/batch_event_processor.rb +235 -234
- data/lib/optimizely/event/entity/conversion_event.rb +44 -43
- data/lib/optimizely/event/entity/decision.rb +38 -38
- data/lib/optimizely/event/entity/event_batch.rb +86 -86
- data/lib/optimizely/event/entity/event_context.rb +50 -50
- data/lib/optimizely/event/entity/impression_event.rb +48 -47
- data/lib/optimizely/event/entity/snapshot.rb +33 -33
- data/lib/optimizely/event/entity/snapshot_event.rb +48 -48
- data/lib/optimizely/event/entity/user_event.rb +22 -22
- data/lib/optimizely/event/entity/visitor.rb +36 -35
- data/lib/optimizely/event/entity/visitor_attribute.rb +38 -37
- data/lib/optimizely/event/event_factory.rb +156 -155
- data/lib/optimizely/event/event_processor.rb +25 -25
- data/lib/optimizely/event/forwarding_event_processor.rb +44 -43
- data/lib/optimizely/event/user_event_factory.rb +88 -88
- data/lib/optimizely/event_builder.rb +221 -228
- data/lib/optimizely/event_dispatcher.rb +71 -71
- data/lib/optimizely/exceptions.rb +135 -139
- data/lib/optimizely/helpers/constants.rb +415 -397
- data/lib/optimizely/helpers/date_time_utils.rb +30 -30
- data/lib/optimizely/helpers/event_tag_utils.rb +132 -132
- data/lib/optimizely/helpers/group.rb +31 -31
- data/lib/optimizely/helpers/http_utils.rb +65 -64
- data/lib/optimizely/helpers/validator.rb +183 -183
- data/lib/optimizely/helpers/variable_type.rb +67 -67
- data/lib/optimizely/logger.rb +46 -45
- data/lib/optimizely/notification_center.rb +174 -176
- data/lib/optimizely/optimizely_config.rb +271 -272
- data/lib/optimizely/optimizely_factory.rb +181 -181
- data/lib/optimizely/optimizely_user_context.rb +204 -107
- data/lib/optimizely/params.rb +31 -31
- data/lib/optimizely/project_config.rb +99 -91
- data/lib/optimizely/semantic_version.rb +166 -166
- data/lib/optimizely/{custom_attribute_condition_evaluator.rb → user_condition_evaluator.rb} +391 -369
- data/lib/optimizely/user_profile_service.rb +35 -35
- data/lib/optimizely/version.rb +21 -21
- data/lib/optimizely.rb +1130 -1117
- metadata +13 -13
data/lib/optimizely.rb
CHANGED
@@ -1,1117 +1,1130 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
#
|
4
|
-
# Copyright 2016-
|
5
|
-
#
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
-
# you may not use this file except in compliance with the License.
|
8
|
-
# You may obtain a copy of the License at
|
9
|
-
#
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
-
#
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
-
# See the License for the specific language governing permissions and
|
16
|
-
# limitations under the License.
|
17
|
-
#
|
18
|
-
require_relative 'optimizely/audience'
|
19
|
-
require_relative 'optimizely/config/datafile_project_config'
|
20
|
-
require_relative 'optimizely/config_manager/http_project_config_manager'
|
21
|
-
require_relative 'optimizely/config_manager/static_project_config_manager'
|
22
|
-
require_relative 'optimizely/decide/optimizely_decide_option'
|
23
|
-
require_relative 'optimizely/decide/optimizely_decision'
|
24
|
-
require_relative 'optimizely/decide/optimizely_decision_message'
|
25
|
-
require_relative 'optimizely/decision_service'
|
26
|
-
require_relative 'optimizely/error_handler'
|
27
|
-
require_relative 'optimizely/event_builder'
|
28
|
-
require_relative 'optimizely/event/forwarding_event_processor'
|
29
|
-
require_relative 'optimizely/event/event_factory'
|
30
|
-
require_relative 'optimizely/event/user_event_factory'
|
31
|
-
require_relative 'optimizely/event_dispatcher'
|
32
|
-
require_relative 'optimizely/exceptions'
|
33
|
-
require_relative 'optimizely/helpers/constants'
|
34
|
-
require_relative 'optimizely/helpers/group'
|
35
|
-
require_relative 'optimizely/helpers/validator'
|
36
|
-
require_relative 'optimizely/helpers/variable_type'
|
37
|
-
require_relative 'optimizely/logger'
|
38
|
-
require_relative 'optimizely/notification_center'
|
39
|
-
require_relative 'optimizely/optimizely_config'
|
40
|
-
require_relative 'optimizely/optimizely_user_context'
|
41
|
-
|
42
|
-
module Optimizely
|
43
|
-
class Project
|
44
|
-
include Optimizely::Decide
|
45
|
-
|
46
|
-
attr_reader :notification_center
|
47
|
-
# @api no-doc
|
48
|
-
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
|
49
|
-
:event_processor, :logger, :stopped
|
50
|
-
|
51
|
-
# Constructor for Projects.
|
52
|
-
#
|
53
|
-
# @param datafile - JSON string representing the project.
|
54
|
-
# @param event_dispatcher - Provides a dispatch_event method which if given a URL and params sends a request to it.
|
55
|
-
# @param logger - Optional component which provides a log method to log messages. By default nothing would be logged.
|
56
|
-
# @param error_handler - Optional component which provides a handle_error method to handle exceptions.
|
57
|
-
# By default all exceptions will be suppressed.
|
58
|
-
# @param user_profile_service - Optional component which provides methods to store and retreive user profiles.
|
59
|
-
# @param skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
|
60
|
-
# @params sdk_key - Optional string uniquely identifying the datafile corresponding to project and environment combination.
|
61
|
-
# Must provide at least one of datafile or sdk_key.
|
62
|
-
# @param config_manager - Optional Responds to 'config' method.
|
63
|
-
# @param notification_center - Optional Instance of NotificationCenter.
|
64
|
-
# @param event_processor - Optional Responds to process.
|
65
|
-
|
66
|
-
def initialize(
|
67
|
-
datafile = nil,
|
68
|
-
event_dispatcher = nil,
|
69
|
-
logger = nil,
|
70
|
-
error_handler = nil,
|
71
|
-
skip_json_validation = false,
|
72
|
-
user_profile_service = nil,
|
73
|
-
sdk_key = nil,
|
74
|
-
config_manager = nil,
|
75
|
-
notification_center = nil,
|
76
|
-
event_processor = nil,
|
77
|
-
default_decide_options = []
|
78
|
-
)
|
79
|
-
@logger = logger || NoOpLogger.new
|
80
|
-
@error_handler = error_handler || NoOpErrorHandler.new
|
81
|
-
@event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
|
82
|
-
@user_profile_service = user_profile_service
|
83
|
-
@default_decide_options = []
|
84
|
-
|
85
|
-
if default_decide_options.is_a? Array
|
86
|
-
@default_decide_options = default_decide_options.clone
|
87
|
-
else
|
88
|
-
@logger.log(Logger::DEBUG, 'Provided default decide options is not an array.')
|
89
|
-
@default_decide_options = []
|
90
|
-
end
|
91
|
-
|
92
|
-
begin
|
93
|
-
validate_instantiation_options
|
94
|
-
rescue InvalidInputError => e
|
95
|
-
@logger = SimpleLogger.new
|
96
|
-
@logger.log(Logger::ERROR, e.message)
|
97
|
-
end
|
98
|
-
|
99
|
-
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
|
100
|
-
|
101
|
-
@config_manager = if config_manager.respond_to?(:config)
|
102
|
-
config_manager
|
103
|
-
elsif sdk_key
|
104
|
-
HTTPProjectConfigManager.new(
|
105
|
-
sdk_key: sdk_key,
|
106
|
-
datafile: datafile,
|
107
|
-
logger: @logger,
|
108
|
-
error_handler: @error_handler,
|
109
|
-
skip_json_validation: skip_json_validation,
|
110
|
-
notification_center: @notification_center
|
111
|
-
)
|
112
|
-
else
|
113
|
-
StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
|
114
|
-
end
|
115
|
-
|
116
|
-
@decision_service = DecisionService.new(@logger, @user_profile_service)
|
117
|
-
|
118
|
-
@event_processor = if event_processor.respond_to?(:process)
|
119
|
-
event_processor
|
120
|
-
else
|
121
|
-
ForwardingEventProcessor.new(@event_dispatcher, @logger, @notification_center)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
# Create a context of the user for which decision APIs will be called.
|
126
|
-
#
|
127
|
-
# A user context will be created successfully even when the SDK is not fully configured yet.
|
128
|
-
#
|
129
|
-
# @param user_id - The user ID to be used for bucketing.
|
130
|
-
# @param attributes - A Hash representing user attribute names and values.
|
131
|
-
#
|
132
|
-
# @return [OptimizelyUserContext] An OptimizelyUserContext associated with this OptimizelyClient.
|
133
|
-
# @return [nil] If user attributes are not in valid format.
|
134
|
-
|
135
|
-
def create_user_context(user_id, attributes = nil)
|
136
|
-
# We do not check for is_valid here as a user context can be created successfully
|
137
|
-
# even when the SDK is not fully configured.
|
138
|
-
|
139
|
-
# validate user_id
|
140
|
-
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
141
|
-
{
|
142
|
-
user_id: user_id
|
143
|
-
}, @logger, Logger::ERROR
|
144
|
-
)
|
145
|
-
|
146
|
-
# validate attributes
|
147
|
-
return nil unless user_inputs_valid?(attributes)
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
feature_flag
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
#
|
299
|
-
#
|
300
|
-
# @
|
301
|
-
# @
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
unless
|
441
|
-
@logger.log(Logger::
|
442
|
-
return nil
|
443
|
-
end
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
unless
|
489
|
-
@logger.log(Logger::ERROR,
|
490
|
-
return false
|
491
|
-
end
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
return
|
555
|
-
end
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
end
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
end
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
#
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
return nil unless
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
end
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
#
|
823
|
-
#
|
824
|
-
#
|
825
|
-
#
|
826
|
-
#
|
827
|
-
#
|
828
|
-
#
|
829
|
-
#
|
830
|
-
#
|
831
|
-
#
|
832
|
-
#
|
833
|
-
#
|
834
|
-
#
|
835
|
-
#
|
836
|
-
#
|
837
|
-
#
|
838
|
-
#
|
839
|
-
#
|
840
|
-
#
|
841
|
-
#
|
842
|
-
#
|
843
|
-
#
|
844
|
-
#
|
845
|
-
#
|
846
|
-
#
|
847
|
-
#
|
848
|
-
#
|
849
|
-
#
|
850
|
-
#
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
#
|
857
|
-
#
|
858
|
-
|
859
|
-
@
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
#
|
876
|
-
#
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
@
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
#
|
912
|
-
#
|
913
|
-
#
|
914
|
-
#
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
#
|
992
|
-
#
|
993
|
-
#
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
true
|
1043
|
-
end
|
1044
|
-
|
1045
|
-
def
|
1046
|
-
unless Helpers::Validator.
|
1047
|
-
@logger.log(Logger::ERROR, 'Provided
|
1048
|
-
@error_handler.handle_error(
|
1049
|
-
return false
|
1050
|
-
end
|
1051
|
-
true
|
1052
|
-
end
|
1053
|
-
|
1054
|
-
def
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
)
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
@
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright 2016-2022, Optimizely and contributors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require_relative 'optimizely/audience'
|
19
|
+
require_relative 'optimizely/config/datafile_project_config'
|
20
|
+
require_relative 'optimizely/config_manager/http_project_config_manager'
|
21
|
+
require_relative 'optimizely/config_manager/static_project_config_manager'
|
22
|
+
require_relative 'optimizely/decide/optimizely_decide_option'
|
23
|
+
require_relative 'optimizely/decide/optimizely_decision'
|
24
|
+
require_relative 'optimizely/decide/optimizely_decision_message'
|
25
|
+
require_relative 'optimizely/decision_service'
|
26
|
+
require_relative 'optimizely/error_handler'
|
27
|
+
require_relative 'optimizely/event_builder'
|
28
|
+
require_relative 'optimizely/event/forwarding_event_processor'
|
29
|
+
require_relative 'optimizely/event/event_factory'
|
30
|
+
require_relative 'optimizely/event/user_event_factory'
|
31
|
+
require_relative 'optimizely/event_dispatcher'
|
32
|
+
require_relative 'optimizely/exceptions'
|
33
|
+
require_relative 'optimizely/helpers/constants'
|
34
|
+
require_relative 'optimizely/helpers/group'
|
35
|
+
require_relative 'optimizely/helpers/validator'
|
36
|
+
require_relative 'optimizely/helpers/variable_type'
|
37
|
+
require_relative 'optimizely/logger'
|
38
|
+
require_relative 'optimizely/notification_center'
|
39
|
+
require_relative 'optimizely/optimizely_config'
|
40
|
+
require_relative 'optimizely/optimizely_user_context'
|
41
|
+
|
42
|
+
module Optimizely
|
43
|
+
class Project
|
44
|
+
include Optimizely::Decide
|
45
|
+
|
46
|
+
attr_reader :notification_center
|
47
|
+
# @api no-doc
|
48
|
+
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
|
49
|
+
:event_processor, :logger, :stopped
|
50
|
+
|
51
|
+
# Constructor for Projects.
|
52
|
+
#
|
53
|
+
# @param datafile - JSON string representing the project.
|
54
|
+
# @param event_dispatcher - Provides a dispatch_event method which if given a URL and params sends a request to it.
|
55
|
+
# @param logger - Optional component which provides a log method to log messages. By default nothing would be logged.
|
56
|
+
# @param error_handler - Optional component which provides a handle_error method to handle exceptions.
|
57
|
+
# By default all exceptions will be suppressed.
|
58
|
+
# @param user_profile_service - Optional component which provides methods to store and retreive user profiles.
|
59
|
+
# @param skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
|
60
|
+
# @params sdk_key - Optional string uniquely identifying the datafile corresponding to project and environment combination.
|
61
|
+
# Must provide at least one of datafile or sdk_key.
|
62
|
+
# @param config_manager - Optional Responds to 'config' method.
|
63
|
+
# @param notification_center - Optional Instance of NotificationCenter.
|
64
|
+
# @param event_processor - Optional Responds to process.
|
65
|
+
|
66
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
67
|
+
datafile = nil,
|
68
|
+
event_dispatcher = nil,
|
69
|
+
logger = nil,
|
70
|
+
error_handler = nil,
|
71
|
+
skip_json_validation = false, # rubocop:disable Style/OptionalBooleanParameter
|
72
|
+
user_profile_service = nil,
|
73
|
+
sdk_key = nil,
|
74
|
+
config_manager = nil,
|
75
|
+
notification_center = nil,
|
76
|
+
event_processor = nil,
|
77
|
+
default_decide_options = []
|
78
|
+
)
|
79
|
+
@logger = logger || NoOpLogger.new
|
80
|
+
@error_handler = error_handler || NoOpErrorHandler.new
|
81
|
+
@event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler)
|
82
|
+
@user_profile_service = user_profile_service
|
83
|
+
@default_decide_options = []
|
84
|
+
|
85
|
+
if default_decide_options.is_a? Array
|
86
|
+
@default_decide_options = default_decide_options.clone
|
87
|
+
else
|
88
|
+
@logger.log(Logger::DEBUG, 'Provided default decide options is not an array.')
|
89
|
+
@default_decide_options = []
|
90
|
+
end
|
91
|
+
|
92
|
+
begin
|
93
|
+
validate_instantiation_options
|
94
|
+
rescue InvalidInputError => e
|
95
|
+
@logger = SimpleLogger.new
|
96
|
+
@logger.log(Logger::ERROR, e.message)
|
97
|
+
end
|
98
|
+
|
99
|
+
@notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(@logger, @error_handler)
|
100
|
+
|
101
|
+
@config_manager = if config_manager.respond_to?(:config)
|
102
|
+
config_manager
|
103
|
+
elsif sdk_key
|
104
|
+
HTTPProjectConfigManager.new(
|
105
|
+
sdk_key: sdk_key,
|
106
|
+
datafile: datafile,
|
107
|
+
logger: @logger,
|
108
|
+
error_handler: @error_handler,
|
109
|
+
skip_json_validation: skip_json_validation,
|
110
|
+
notification_center: @notification_center
|
111
|
+
)
|
112
|
+
else
|
113
|
+
StaticProjectConfigManager.new(datafile, @logger, @error_handler, skip_json_validation)
|
114
|
+
end
|
115
|
+
|
116
|
+
@decision_service = DecisionService.new(@logger, @user_profile_service)
|
117
|
+
|
118
|
+
@event_processor = if event_processor.respond_to?(:process)
|
119
|
+
event_processor
|
120
|
+
else
|
121
|
+
ForwardingEventProcessor.new(@event_dispatcher, @logger, @notification_center)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Create a context of the user for which decision APIs will be called.
|
126
|
+
#
|
127
|
+
# A user context will be created successfully even when the SDK is not fully configured yet.
|
128
|
+
#
|
129
|
+
# @param user_id - The user ID to be used for bucketing.
|
130
|
+
# @param attributes - A Hash representing user attribute names and values.
|
131
|
+
#
|
132
|
+
# @return [OptimizelyUserContext] An OptimizelyUserContext associated with this OptimizelyClient.
|
133
|
+
# @return [nil] If user attributes are not in valid format.
|
134
|
+
|
135
|
+
def create_user_context(user_id, attributes = nil)
|
136
|
+
# We do not check for is_valid here as a user context can be created successfully
|
137
|
+
# even when the SDK is not fully configured.
|
138
|
+
|
139
|
+
# validate user_id
|
140
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
141
|
+
{
|
142
|
+
user_id: user_id
|
143
|
+
}, @logger, Logger::ERROR
|
144
|
+
)
|
145
|
+
|
146
|
+
# validate attributes
|
147
|
+
return nil unless user_inputs_valid?(attributes)
|
148
|
+
|
149
|
+
OptimizelyUserContext.new(self, user_id, attributes)
|
150
|
+
end
|
151
|
+
|
152
|
+
def decide(user_context, key, decide_options = [])
|
153
|
+
# raising on user context as it is internal and not provided directly by the user.
|
154
|
+
raise if user_context.class != OptimizelyUserContext
|
155
|
+
|
156
|
+
reasons = []
|
157
|
+
|
158
|
+
# check if SDK is ready
|
159
|
+
unless is_valid
|
160
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
|
161
|
+
reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
|
162
|
+
return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
|
163
|
+
end
|
164
|
+
|
165
|
+
# validate that key is a string
|
166
|
+
unless key.is_a?(String)
|
167
|
+
@logger.log(Logger::ERROR, 'Provided key is invalid')
|
168
|
+
reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
|
169
|
+
return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
|
170
|
+
end
|
171
|
+
|
172
|
+
# validate that key maps to a feature flag
|
173
|
+
config = project_config
|
174
|
+
feature_flag = config.get_feature_flag_from_key(key)
|
175
|
+
unless feature_flag
|
176
|
+
@logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
|
177
|
+
reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
|
178
|
+
return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
|
179
|
+
end
|
180
|
+
|
181
|
+
# merge decide_options and default_decide_options
|
182
|
+
if decide_options.is_a? Array
|
183
|
+
decide_options += @default_decide_options
|
184
|
+
else
|
185
|
+
@logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
|
186
|
+
decide_options = @default_decide_options
|
187
|
+
end
|
188
|
+
|
189
|
+
# Create Optimizely Decision Result.
|
190
|
+
user_id = user_context.user_id
|
191
|
+
attributes = user_context.user_attributes
|
192
|
+
variation_key = nil
|
193
|
+
feature_enabled = false
|
194
|
+
rule_key = nil
|
195
|
+
flag_key = key
|
196
|
+
all_variables = {}
|
197
|
+
decision_event_dispatched = false
|
198
|
+
experiment = nil
|
199
|
+
decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
|
200
|
+
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
|
201
|
+
variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
|
202
|
+
reasons.push(*reasons_received)
|
203
|
+
|
204
|
+
if variation
|
205
|
+
decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
|
206
|
+
else
|
207
|
+
decision, reasons_received = @decision_service.get_variation_for_feature(config, feature_flag, user_context, decide_options)
|
208
|
+
reasons.push(*reasons_received)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
|
212
|
+
if decision.is_a?(Optimizely::DecisionService::Decision)
|
213
|
+
experiment = decision.experiment
|
214
|
+
rule_key = experiment ? experiment['key'] : nil
|
215
|
+
variation = decision['variation']
|
216
|
+
variation_key = variation ? variation['key'] : nil
|
217
|
+
feature_enabled = variation ? variation['featureEnabled'] : false
|
218
|
+
decision_source = decision.source
|
219
|
+
end
|
220
|
+
|
221
|
+
if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions)
|
222
|
+
send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes)
|
223
|
+
decision_event_dispatched = true
|
224
|
+
end
|
225
|
+
|
226
|
+
# Generate all variables map if decide options doesn't include excludeVariables
|
227
|
+
unless decide_options.include? OptimizelyDecideOption::EXCLUDE_VARIABLES
|
228
|
+
feature_flag['variables'].each do |variable|
|
229
|
+
variable_value = get_feature_variable_for_variation(key, feature_enabled, variation, variable, user_id)
|
230
|
+
all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
should_include_reasons = decide_options.include? OptimizelyDecideOption::INCLUDE_REASONS
|
235
|
+
|
236
|
+
# Send notification
|
237
|
+
@notification_center.send_notifications(
|
238
|
+
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
|
239
|
+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'],
|
240
|
+
user_id, (attributes || {}),
|
241
|
+
flag_key: flag_key,
|
242
|
+
enabled: feature_enabled,
|
243
|
+
variables: all_variables,
|
244
|
+
variation_key: variation_key,
|
245
|
+
rule_key: rule_key,
|
246
|
+
reasons: should_include_reasons ? reasons : [],
|
247
|
+
decision_event_dispatched: decision_event_dispatched
|
248
|
+
)
|
249
|
+
|
250
|
+
OptimizelyDecision.new(
|
251
|
+
variation_key: variation_key,
|
252
|
+
enabled: feature_enabled,
|
253
|
+
variables: all_variables,
|
254
|
+
rule_key: rule_key,
|
255
|
+
flag_key: flag_key,
|
256
|
+
user_context: user_context,
|
257
|
+
reasons: should_include_reasons ? reasons : []
|
258
|
+
)
|
259
|
+
end
|
260
|
+
|
261
|
+
def decide_all(user_context, decide_options = [])
|
262
|
+
# raising on user context as it is internal and not provided directly by the user.
|
263
|
+
raise if user_context.class != OptimizelyUserContext
|
264
|
+
|
265
|
+
# check if SDK is ready
|
266
|
+
unless is_valid
|
267
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_all').message)
|
268
|
+
return {}
|
269
|
+
end
|
270
|
+
|
271
|
+
keys = []
|
272
|
+
project_config.feature_flags.each do |feature_flag|
|
273
|
+
keys.push(feature_flag['key'])
|
274
|
+
end
|
275
|
+
decide_for_keys(user_context, keys, decide_options)
|
276
|
+
end
|
277
|
+
|
278
|
+
def decide_for_keys(user_context, keys, decide_options = [])
|
279
|
+
# raising on user context as it is internal and not provided directly by the user.
|
280
|
+
raise if user_context.class != OptimizelyUserContext
|
281
|
+
|
282
|
+
# check if SDK is ready
|
283
|
+
unless is_valid
|
284
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_for_keys').message)
|
285
|
+
return {}
|
286
|
+
end
|
287
|
+
|
288
|
+
enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
|
289
|
+
|
290
|
+
decisions = {}
|
291
|
+
keys.each do |key|
|
292
|
+
decision = decide(user_context, key, decide_options)
|
293
|
+
decisions[key] = decision unless enabled_flags_only && !decision.enabled
|
294
|
+
end
|
295
|
+
decisions
|
296
|
+
end
|
297
|
+
|
298
|
+
# Gets variation using variation key or id and flag key.
|
299
|
+
#
|
300
|
+
# @param flag_key - flag key from which the variation is required.
|
301
|
+
# @param target_value - variation value either id or key that will be matched.
|
302
|
+
# @param attribute - string representing variation attribute.
|
303
|
+
#
|
304
|
+
# @return [variation]
|
305
|
+
# @return [nil] if no variation found in flag_variation_map.
|
306
|
+
|
307
|
+
def get_flag_variation(flag_key, target_value, attribute)
|
308
|
+
project_config.get_variation_from_flag(flag_key, target_value, attribute)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Buckets visitor and sends impression event to Optimizely.
|
312
|
+
#
|
313
|
+
# @param experiment_key - Experiment which needs to be activated.
|
314
|
+
# @param user_id - String ID for user.
|
315
|
+
# @param attributes - Hash representing user attributes and values to be recorded.
|
316
|
+
#
|
317
|
+
# @return [Variation Key] representing the variation the user will be bucketed in.
|
318
|
+
# @return [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
|
319
|
+
|
320
|
+
def activate(experiment_key, user_id, attributes = nil)
|
321
|
+
unless is_valid
|
322
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('activate').message)
|
323
|
+
return nil
|
324
|
+
end
|
325
|
+
|
326
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
327
|
+
{
|
328
|
+
experiment_key: experiment_key,
|
329
|
+
user_id: user_id
|
330
|
+
}, @logger, Logger::ERROR
|
331
|
+
)
|
332
|
+
|
333
|
+
config = project_config
|
334
|
+
|
335
|
+
variation_key = get_variation_with_config(experiment_key, user_id, attributes, config)
|
336
|
+
|
337
|
+
if variation_key.nil?
|
338
|
+
@logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
|
339
|
+
return nil
|
340
|
+
end
|
341
|
+
|
342
|
+
# Create and dispatch impression event
|
343
|
+
experiment = config.get_experiment_from_key(experiment_key)
|
344
|
+
send_impression(
|
345
|
+
config, experiment, variation_key, '', experiment_key, true,
|
346
|
+
Optimizely::DecisionService::DECISION_SOURCES['EXPERIMENT'], user_id, attributes
|
347
|
+
)
|
348
|
+
|
349
|
+
variation_key
|
350
|
+
end
|
351
|
+
|
352
|
+
# Gets variation where visitor will be bucketed.
|
353
|
+
#
|
354
|
+
# @param experiment_key - Experiment for which visitor variation needs to be determined.
|
355
|
+
# @param user_id - String ID for user.
|
356
|
+
# @param attributes - Hash representing user attributes.
|
357
|
+
#
|
358
|
+
# @return [variation key] where visitor will be bucketed.
|
359
|
+
# @return [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
|
360
|
+
|
361
|
+
def get_variation(experiment_key, user_id, attributes = nil)
|
362
|
+
unless is_valid
|
363
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_variation').message)
|
364
|
+
return nil
|
365
|
+
end
|
366
|
+
|
367
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
368
|
+
{
|
369
|
+
experiment_key: experiment_key,
|
370
|
+
user_id: user_id
|
371
|
+
}, @logger, Logger::ERROR
|
372
|
+
)
|
373
|
+
|
374
|
+
config = project_config
|
375
|
+
|
376
|
+
get_variation_with_config(experiment_key, user_id, attributes, config)
|
377
|
+
end
|
378
|
+
|
379
|
+
# Force a user into a variation for a given experiment.
|
380
|
+
#
|
381
|
+
# @param experiment_key - String - key identifying the experiment.
|
382
|
+
# @param user_id - String - The user ID to be used for bucketing.
|
383
|
+
# @param variation_key - The variation key specifies the variation which the user will
|
384
|
+
# be forced into. If nil, then clear the existing experiment-to-variation mapping.
|
385
|
+
#
|
386
|
+
# @return [Boolean] indicates if the set completed successfully.
|
387
|
+
|
388
|
+
def set_forced_variation(experiment_key, user_id, variation_key)
|
389
|
+
unless is_valid
|
390
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('set_forced_variation').message)
|
391
|
+
return nil
|
392
|
+
end
|
393
|
+
|
394
|
+
input_values = {experiment_key: experiment_key, user_id: user_id}
|
395
|
+
input_values[:variation_key] = variation_key unless variation_key.nil?
|
396
|
+
return false unless Optimizely::Helpers::Validator.inputs_valid?(input_values, @logger, Logger::ERROR)
|
397
|
+
|
398
|
+
config = project_config
|
399
|
+
|
400
|
+
@decision_service.set_forced_variation(config, experiment_key, user_id, variation_key)
|
401
|
+
end
|
402
|
+
|
403
|
+
# Gets the forced variation for a given user and experiment.
|
404
|
+
#
|
405
|
+
# @param experiment_key - String - Key identifying the experiment.
|
406
|
+
# @param user_id - String - The user ID to be used for bucketing.
|
407
|
+
#
|
408
|
+
# @return [String] The forced variation key.
|
409
|
+
|
410
|
+
def get_forced_variation(experiment_key, user_id)
|
411
|
+
unless is_valid
|
412
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_forced_variation').message)
|
413
|
+
return nil
|
414
|
+
end
|
415
|
+
|
416
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
417
|
+
{
|
418
|
+
experiment_key: experiment_key,
|
419
|
+
user_id: user_id
|
420
|
+
}, @logger, Logger::ERROR
|
421
|
+
)
|
422
|
+
|
423
|
+
config = project_config
|
424
|
+
|
425
|
+
forced_variation_key = nil
|
426
|
+
forced_variation, = @decision_service.get_forced_variation(config, experiment_key, user_id)
|
427
|
+
forced_variation_key = forced_variation['key'] if forced_variation
|
428
|
+
|
429
|
+
forced_variation_key
|
430
|
+
end
|
431
|
+
|
432
|
+
# Send conversion event to Optimizely.
|
433
|
+
#
|
434
|
+
# @param event_key - Event key representing the event which needs to be recorded.
|
435
|
+
# @param user_id - String ID for user.
|
436
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
437
|
+
# @param event_tags - Hash representing metadata associated with the event.
|
438
|
+
|
439
|
+
def track(event_key, user_id, attributes = nil, event_tags = nil)
|
440
|
+
unless is_valid
|
441
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('track').message)
|
442
|
+
return nil
|
443
|
+
end
|
444
|
+
|
445
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
446
|
+
{
|
447
|
+
event_key: event_key,
|
448
|
+
user_id: user_id
|
449
|
+
}, @logger, Logger::ERROR
|
450
|
+
)
|
451
|
+
|
452
|
+
return nil unless user_inputs_valid?(attributes, event_tags)
|
453
|
+
|
454
|
+
config = project_config
|
455
|
+
|
456
|
+
event = config.get_event_from_key(event_key)
|
457
|
+
unless event
|
458
|
+
@logger.log(Logger::INFO, "Not tracking user '#{user_id}' for event '#{event_key}'.")
|
459
|
+
return nil
|
460
|
+
end
|
461
|
+
|
462
|
+
user_event = UserEventFactory.create_conversion_event(config, event, user_id, attributes, event_tags)
|
463
|
+
@event_processor.process(user_event)
|
464
|
+
@logger.log(Logger::INFO, "Tracking event '#{event_key}' for user '#{user_id}'.")
|
465
|
+
|
466
|
+
if @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:TRACK]).positive?
|
467
|
+
log_event = EventFactory.create_log_event(user_event, @logger)
|
468
|
+
@notification_center.send_notifications(
|
469
|
+
NotificationCenter::NOTIFICATION_TYPES[:TRACK],
|
470
|
+
event_key, user_id, attributes, event_tags, log_event
|
471
|
+
)
|
472
|
+
end
|
473
|
+
nil
|
474
|
+
end
|
475
|
+
|
476
|
+
# Determine whether a feature is enabled.
|
477
|
+
# Sends an impression event if the user is bucketed into an experiment using the feature.
|
478
|
+
#
|
479
|
+
# @param feature_flag_key - String unique key of the feature.
|
480
|
+
# @param user_id - String ID of the user.
|
481
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
482
|
+
#
|
483
|
+
# @return [True] if the feature is enabled.
|
484
|
+
# @return [False] if the feature is disabled.
|
485
|
+
# @return [False] if the feature is not found.
|
486
|
+
|
487
|
+
def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
|
488
|
+
unless is_valid
|
489
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('is_feature_enabled').message)
|
490
|
+
return false
|
491
|
+
end
|
492
|
+
|
493
|
+
return false unless Optimizely::Helpers::Validator.inputs_valid?(
|
494
|
+
{
|
495
|
+
feature_flag_key: feature_flag_key,
|
496
|
+
user_id: user_id
|
497
|
+
}, @logger, Logger::ERROR
|
498
|
+
)
|
499
|
+
|
500
|
+
return false unless user_inputs_valid?(attributes)
|
501
|
+
|
502
|
+
config = project_config
|
503
|
+
|
504
|
+
feature_flag = config.get_feature_flag_from_key(feature_flag_key)
|
505
|
+
unless feature_flag
|
506
|
+
@logger.log(Logger::ERROR, "No feature flag was found for key '#{feature_flag_key}'.")
|
507
|
+
return false
|
508
|
+
end
|
509
|
+
|
510
|
+
user_context = create_user_context(user_id, attributes)
|
511
|
+
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
|
512
|
+
|
513
|
+
feature_enabled = false
|
514
|
+
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
|
515
|
+
if decision.is_a?(Optimizely::DecisionService::Decision)
|
516
|
+
variation = decision['variation']
|
517
|
+
feature_enabled = variation['featureEnabled']
|
518
|
+
if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
|
519
|
+
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
|
520
|
+
source_info = {
|
521
|
+
experiment_key: decision.experiment['key'],
|
522
|
+
variation_key: variation['key']
|
523
|
+
}
|
524
|
+
# Send event if Decision came from a feature test.
|
525
|
+
send_impression(
|
526
|
+
config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
|
527
|
+
)
|
528
|
+
elsif decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions
|
529
|
+
send_impression(
|
530
|
+
config, decision.experiment, variation['key'], feature_flag_key, decision.experiment['key'], feature_enabled, source_string, user_id, attributes
|
531
|
+
)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
if decision.nil? && config.send_flag_decisions
|
536
|
+
send_impression(
|
537
|
+
config, nil, '', feature_flag_key, '', feature_enabled, source_string, user_id, attributes
|
538
|
+
)
|
539
|
+
end
|
540
|
+
|
541
|
+
@notification_center.send_notifications(
|
542
|
+
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
|
543
|
+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
|
544
|
+
user_id, (attributes || {}),
|
545
|
+
feature_key: feature_flag_key,
|
546
|
+
feature_enabled: feature_enabled,
|
547
|
+
source: source_string,
|
548
|
+
source_info: source_info || {}
|
549
|
+
)
|
550
|
+
|
551
|
+
if feature_enabled == true
|
552
|
+
@logger.log(Logger::INFO,
|
553
|
+
"Feature '#{feature_flag_key}' is enabled for user '#{user_id}'.")
|
554
|
+
return true
|
555
|
+
end
|
556
|
+
|
557
|
+
@logger.log(Logger::INFO,
|
558
|
+
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
|
559
|
+
false
|
560
|
+
end
|
561
|
+
|
562
|
+
# Gets keys of all feature flags which are enabled for the user.
|
563
|
+
#
|
564
|
+
# @param user_id - ID for user.
|
565
|
+
# @param attributes - Dict representing user attributes.
|
566
|
+
# @return [feature flag keys] A List of feature flag keys that are enabled for the user.
|
567
|
+
|
568
|
+
def get_enabled_features(user_id, attributes = nil)
|
569
|
+
enabled_features = []
|
570
|
+
unless is_valid
|
571
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_enabled_features').message)
|
572
|
+
return enabled_features
|
573
|
+
end
|
574
|
+
|
575
|
+
return enabled_features unless Optimizely::Helpers::Validator.inputs_valid?(
|
576
|
+
{
|
577
|
+
user_id: user_id
|
578
|
+
}, @logger, Logger::ERROR
|
579
|
+
)
|
580
|
+
|
581
|
+
return enabled_features unless user_inputs_valid?(attributes)
|
582
|
+
|
583
|
+
config = project_config
|
584
|
+
|
585
|
+
config.feature_flags.each do |feature|
|
586
|
+
enabled_features.push(feature['key']) if is_feature_enabled(
|
587
|
+
feature['key'],
|
588
|
+
user_id,
|
589
|
+
attributes
|
590
|
+
) == true
|
591
|
+
end
|
592
|
+
enabled_features
|
593
|
+
end
|
594
|
+
|
595
|
+
# Get the value of the specified variable in the feature flag.
|
596
|
+
#
|
597
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
598
|
+
# @param variable_key - String key of variable for which we are getting the value
|
599
|
+
# @param user_id - String user ID
|
600
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
601
|
+
#
|
602
|
+
# @return [*] the type-casted variable value.
|
603
|
+
# @return [nil] if the feature flag or variable are not found.
|
604
|
+
|
605
|
+
def get_feature_variable(feature_flag_key, variable_key, user_id, attributes = nil)
|
606
|
+
unless is_valid
|
607
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable').message)
|
608
|
+
return nil
|
609
|
+
end
|
610
|
+
get_feature_variable_for_type(
|
611
|
+
feature_flag_key,
|
612
|
+
variable_key,
|
613
|
+
nil,
|
614
|
+
user_id,
|
615
|
+
attributes
|
616
|
+
)
|
617
|
+
end
|
618
|
+
|
619
|
+
# Get the String value of the specified variable in the feature flag.
|
620
|
+
#
|
621
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
622
|
+
# @param variable_key - String key of variable for which we are getting the string value
|
623
|
+
# @param user_id - String user ID
|
624
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
625
|
+
#
|
626
|
+
# @return [String] the string variable value.
|
627
|
+
# @return [nil] if the feature flag or variable are not found.
|
628
|
+
|
629
|
+
def get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes = nil)
|
630
|
+
unless is_valid
|
631
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_string').message)
|
632
|
+
return nil
|
633
|
+
end
|
634
|
+
get_feature_variable_for_type(
|
635
|
+
feature_flag_key,
|
636
|
+
variable_key,
|
637
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['STRING'],
|
638
|
+
user_id,
|
639
|
+
attributes
|
640
|
+
)
|
641
|
+
end
|
642
|
+
|
643
|
+
# Get the Json value of the specified variable in the feature flag in a Dict.
|
644
|
+
#
|
645
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
646
|
+
# @param variable_key - String key of variable for which we are getting the string value
|
647
|
+
# @param user_id - String user ID
|
648
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
649
|
+
#
|
650
|
+
# @return [Dict] the Dict containing variable value.
|
651
|
+
# @return [nil] if the feature flag or variable are not found.
|
652
|
+
|
653
|
+
def get_feature_variable_json(feature_flag_key, variable_key, user_id, attributes = nil)
|
654
|
+
unless is_valid
|
655
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_json').message)
|
656
|
+
return nil
|
657
|
+
end
|
658
|
+
get_feature_variable_for_type(
|
659
|
+
feature_flag_key,
|
660
|
+
variable_key,
|
661
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['JSON'],
|
662
|
+
user_id,
|
663
|
+
attributes
|
664
|
+
)
|
665
|
+
end
|
666
|
+
|
667
|
+
# Get the Boolean value of the specified variable in the feature flag.
|
668
|
+
#
|
669
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
670
|
+
# @param variable_key - String key of variable for which we are getting the string value
|
671
|
+
# @param user_id - String user ID
|
672
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
673
|
+
#
|
674
|
+
# @return [Boolean] the boolean variable value.
|
675
|
+
# @return [nil] if the feature flag or variable are not found.
|
676
|
+
|
677
|
+
def get_feature_variable_boolean(feature_flag_key, variable_key, user_id, attributes = nil)
|
678
|
+
unless is_valid
|
679
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_boolean').message)
|
680
|
+
return nil
|
681
|
+
end
|
682
|
+
|
683
|
+
get_feature_variable_for_type(
|
684
|
+
feature_flag_key,
|
685
|
+
variable_key,
|
686
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['BOOLEAN'],
|
687
|
+
user_id,
|
688
|
+
attributes
|
689
|
+
)
|
690
|
+
end
|
691
|
+
|
692
|
+
# Get the Double value of the specified variable in the feature flag.
|
693
|
+
#
|
694
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
695
|
+
# @param variable_key - String key of variable for which we are getting the string value
|
696
|
+
# @param user_id - String user ID
|
697
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
698
|
+
#
|
699
|
+
# @return [Boolean] the double variable value.
|
700
|
+
# @return [nil] if the feature flag or variable are not found.
|
701
|
+
|
702
|
+
def get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes = nil)
|
703
|
+
unless is_valid
|
704
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_double').message)
|
705
|
+
return nil
|
706
|
+
end
|
707
|
+
|
708
|
+
get_feature_variable_for_type(
|
709
|
+
feature_flag_key,
|
710
|
+
variable_key,
|
711
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['DOUBLE'],
|
712
|
+
user_id,
|
713
|
+
attributes
|
714
|
+
)
|
715
|
+
end
|
716
|
+
|
717
|
+
# Get values of all the variables in the feature flag and returns them in a Dict
|
718
|
+
#
|
719
|
+
# @param feature_flag_key - String key of feature flag
|
720
|
+
# @param user_id - String user ID
|
721
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
722
|
+
#
|
723
|
+
# @return [Dict] the Dict containing all the varible values
|
724
|
+
# @return [nil] if the feature flag is not found.
|
725
|
+
|
726
|
+
def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
|
727
|
+
unless is_valid
|
728
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_all_feature_variables').message)
|
729
|
+
return nil
|
730
|
+
end
|
731
|
+
|
732
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
733
|
+
{
|
734
|
+
feature_flag_key: feature_flag_key,
|
735
|
+
user_id: user_id
|
736
|
+
},
|
737
|
+
@logger, Logger::ERROR
|
738
|
+
)
|
739
|
+
|
740
|
+
return nil unless user_inputs_valid?(attributes)
|
741
|
+
|
742
|
+
config = project_config
|
743
|
+
|
744
|
+
feature_flag = config.get_feature_flag_from_key(feature_flag_key)
|
745
|
+
unless feature_flag
|
746
|
+
@logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
|
747
|
+
return nil
|
748
|
+
end
|
749
|
+
|
750
|
+
user_context = create_user_context(user_id, attributes)
|
751
|
+
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
|
752
|
+
variation = decision ? decision['variation'] : nil
|
753
|
+
feature_enabled = variation ? variation['featureEnabled'] : false
|
754
|
+
all_variables = {}
|
755
|
+
|
756
|
+
feature_flag['variables'].each do |variable|
|
757
|
+
variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
|
758
|
+
all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
|
759
|
+
end
|
760
|
+
|
761
|
+
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
|
762
|
+
if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
|
763
|
+
source_info = {
|
764
|
+
experiment_key: decision.experiment['key'],
|
765
|
+
variation_key: variation['key']
|
766
|
+
}
|
767
|
+
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
|
768
|
+
end
|
769
|
+
|
770
|
+
@notification_center.send_notifications(
|
771
|
+
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
|
772
|
+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
|
773
|
+
feature_key: feature_flag_key,
|
774
|
+
feature_enabled: feature_enabled,
|
775
|
+
source: source_string,
|
776
|
+
variable_values: all_variables,
|
777
|
+
source_info: source_info || {}
|
778
|
+
)
|
779
|
+
|
780
|
+
all_variables
|
781
|
+
end
|
782
|
+
|
783
|
+
# Get the Integer value of the specified variable in the feature flag.
|
784
|
+
#
|
785
|
+
# @param feature_flag_key - String key of feature flag the variable belongs to
|
786
|
+
# @param variable_key - String key of variable for which we are getting the string value
|
787
|
+
# @param user_id - String user ID
|
788
|
+
# @param attributes - Hash representing visitor attributes and values which need to be recorded.
|
789
|
+
#
|
790
|
+
# @return [Integer] variable value.
|
791
|
+
# @return [nil] if the feature flag or variable are not found.
|
792
|
+
|
793
|
+
def get_feature_variable_integer(feature_flag_key, variable_key, user_id, attributes = nil)
|
794
|
+
unless is_valid
|
795
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_feature_variable_integer').message)
|
796
|
+
return nil
|
797
|
+
end
|
798
|
+
|
799
|
+
get_feature_variable_for_type(
|
800
|
+
feature_flag_key,
|
801
|
+
variable_key,
|
802
|
+
Optimizely::Helpers::Constants::VARIABLE_TYPES['INTEGER'],
|
803
|
+
user_id,
|
804
|
+
attributes
|
805
|
+
)
|
806
|
+
end
|
807
|
+
|
808
|
+
def is_valid
|
809
|
+
config = project_config
|
810
|
+
config.is_a?(Optimizely::ProjectConfig)
|
811
|
+
end
|
812
|
+
|
813
|
+
def close
|
814
|
+
return if @stopped
|
815
|
+
|
816
|
+
@stopped = true
|
817
|
+
@config_manager.stop! if @config_manager.respond_to?(:stop!)
|
818
|
+
@event_processor.stop! if @event_processor.respond_to?(:stop!)
|
819
|
+
end
|
820
|
+
|
821
|
+
def get_optimizely_config
|
822
|
+
# Get OptimizelyConfig object containing experiments and features data
|
823
|
+
# Returns Object
|
824
|
+
#
|
825
|
+
# OptimizelyConfig Object Schema
|
826
|
+
# {
|
827
|
+
# 'experimentsMap' => {
|
828
|
+
# 'my-fist-experiment' => {
|
829
|
+
# 'id' => '111111',
|
830
|
+
# 'key' => 'my-fist-experiment'
|
831
|
+
# 'variationsMap' => {
|
832
|
+
# 'variation_1' => {
|
833
|
+
# 'id' => '121212',
|
834
|
+
# 'key' => 'variation_1',
|
835
|
+
# 'variablesMap' => {
|
836
|
+
# 'age' => {
|
837
|
+
# 'id' => '222222',
|
838
|
+
# 'key' => 'age',
|
839
|
+
# 'type' => 'integer',
|
840
|
+
# 'value' => '0',
|
841
|
+
# }
|
842
|
+
# }
|
843
|
+
# }
|
844
|
+
# }
|
845
|
+
# }
|
846
|
+
# },
|
847
|
+
# 'featuresMap' => {
|
848
|
+
# 'awesome-feature' => {
|
849
|
+
# 'id' => '333333',
|
850
|
+
# 'key' => 'awesome-feature',
|
851
|
+
# 'experimentsMap' => Object,
|
852
|
+
# 'variablesMap' => Object,
|
853
|
+
# }
|
854
|
+
# },
|
855
|
+
# 'revision' => '13',
|
856
|
+
# }
|
857
|
+
#
|
858
|
+
unless is_valid
|
859
|
+
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('get_optimizely_config').message)
|
860
|
+
return nil
|
861
|
+
end
|
862
|
+
|
863
|
+
# config_manager might not contain optimizely_config if its supplied by the consumer
|
864
|
+
# Generating a new OptimizelyConfig object in this case as a fallback
|
865
|
+
if @config_manager.respond_to?(:optimizely_config)
|
866
|
+
@config_manager.optimizely_config
|
867
|
+
else
|
868
|
+
OptimizelyConfig.new(project_config).config
|
869
|
+
end
|
870
|
+
end
|
871
|
+
|
872
|
+
private
|
873
|
+
|
874
|
+
def get_variation_with_config(experiment_key, user_id, attributes, config)
|
875
|
+
# Gets variation where visitor will be bucketed.
|
876
|
+
#
|
877
|
+
# experiment_key - Experiment for which visitor variation needs to be determined.
|
878
|
+
# user_id - String ID for user.
|
879
|
+
# attributes - Hash representing user attributes.
|
880
|
+
# config - Instance of DatfileProjectConfig
|
881
|
+
#
|
882
|
+
# Returns [variation key] where visitor will be bucketed.
|
883
|
+
# Returns [nil] if experiment is not Running, if user is not in experiment, or if datafile is invalid.
|
884
|
+
experiment = config.get_experiment_from_key(experiment_key)
|
885
|
+
return nil if experiment.nil?
|
886
|
+
|
887
|
+
experiment_id = experiment['id']
|
888
|
+
|
889
|
+
return nil unless user_inputs_valid?(attributes)
|
890
|
+
|
891
|
+
user_context = create_user_context(user_id, attributes)
|
892
|
+
variation_id, = @decision_service.get_variation(config, experiment_id, user_context)
|
893
|
+
variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
|
894
|
+
variation_key = variation['key'] if variation
|
895
|
+
decision_notification_type = if config.feature_experiment?(experiment_id)
|
896
|
+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_TEST']
|
897
|
+
else
|
898
|
+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['AB_TEST']
|
899
|
+
end
|
900
|
+
@notification_center.send_notifications(
|
901
|
+
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
|
902
|
+
decision_notification_type, user_id, (attributes || {}),
|
903
|
+
experiment_key: experiment_key,
|
904
|
+
variation_key: variation_key
|
905
|
+
)
|
906
|
+
|
907
|
+
variation_key
|
908
|
+
end
|
909
|
+
|
910
|
+
def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type, user_id, attributes = nil)
|
911
|
+
# Get the variable value for the given feature variable and cast it to the specified type
|
912
|
+
# The default value is returned if the feature flag is not enabled for the user.
|
913
|
+
#
|
914
|
+
# feature_flag_key - String key of feature flag the variable belongs to
|
915
|
+
# variable_key - String key of variable for which we are getting the string value
|
916
|
+
# variable_type - String requested type for feature variable
|
917
|
+
# user_id - String user ID
|
918
|
+
# attributes - Hash representing visitor attributes and values which need to be recorded.
|
919
|
+
#
|
920
|
+
# Returns the type-casted variable value.
|
921
|
+
# Returns nil if the feature flag or variable or user ID is empty
|
922
|
+
# in case of variable type mismatch
|
923
|
+
|
924
|
+
return nil unless Optimizely::Helpers::Validator.inputs_valid?(
|
925
|
+
{
|
926
|
+
feature_flag_key: feature_flag_key,
|
927
|
+
variable_key: variable_key,
|
928
|
+
user_id: user_id,
|
929
|
+
variable_type: variable_type
|
930
|
+
},
|
931
|
+
@logger, Logger::ERROR
|
932
|
+
)
|
933
|
+
|
934
|
+
return nil unless user_inputs_valid?(attributes)
|
935
|
+
|
936
|
+
config = project_config
|
937
|
+
|
938
|
+
feature_flag = config.get_feature_flag_from_key(feature_flag_key)
|
939
|
+
unless feature_flag
|
940
|
+
@logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")
|
941
|
+
return nil
|
942
|
+
end
|
943
|
+
|
944
|
+
variable = config.get_feature_variable(feature_flag, variable_key)
|
945
|
+
|
946
|
+
# Error message logged in DatafileProjectConfig- get_feature_flag_from_key
|
947
|
+
return nil if variable.nil?
|
948
|
+
|
949
|
+
# If variable_type is nil, set it equal to variable['type']
|
950
|
+
variable_type ||= variable['type']
|
951
|
+
# Returns nil if type differs
|
952
|
+
if variable['type'] != variable_type
|
953
|
+
@logger.log(Logger::WARN,
|
954
|
+
"Requested variable as type '#{variable_type}' but variable '#{variable_key}' is of type '#{variable['type']}'.")
|
955
|
+
return nil
|
956
|
+
end
|
957
|
+
|
958
|
+
user_context = create_user_context(user_id, attributes)
|
959
|
+
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
|
960
|
+
variation = decision ? decision['variation'] : nil
|
961
|
+
feature_enabled = variation ? variation['featureEnabled'] : false
|
962
|
+
|
963
|
+
variable_value = get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
|
964
|
+
variable_value = Helpers::VariableType.cast_value_to_type(variable_value, variable_type, @logger)
|
965
|
+
|
966
|
+
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
|
967
|
+
if decision && decision['source'] == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
|
968
|
+
source_info = {
|
969
|
+
experiment_key: decision.experiment['key'],
|
970
|
+
variation_key: variation['key']
|
971
|
+
}
|
972
|
+
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
|
973
|
+
end
|
974
|
+
|
975
|
+
@notification_center.send_notifications(
|
976
|
+
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
|
977
|
+
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
|
978
|
+
feature_key: feature_flag_key,
|
979
|
+
feature_enabled: feature_enabled,
|
980
|
+
source: source_string,
|
981
|
+
variable_key: variable_key,
|
982
|
+
variable_type: variable_type,
|
983
|
+
variable_value: variable_value,
|
984
|
+
source_info: source_info || {}
|
985
|
+
)
|
986
|
+
|
987
|
+
variable_value
|
988
|
+
end
|
989
|
+
|
990
|
+
def get_feature_variable_for_variation(feature_flag_key, feature_enabled, variation, variable, user_id)
|
991
|
+
# Helper method to get the non type-casted value for a variable attached to a
|
992
|
+
# feature flag. Returns appropriate variable value depending on whether there
|
993
|
+
# was a matching variation, feature was enabled or not or varible was part of the
|
994
|
+
# available variation or not. Also logs the appropriate message explaining how it
|
995
|
+
# evaluated the value of the variable.
|
996
|
+
#
|
997
|
+
# feature_flag_key - String key of feature flag the variable belongs to
|
998
|
+
# feature_enabled - Boolean indicating if feature is enabled or not
|
999
|
+
# variation - varition returned by decision service
|
1000
|
+
# user_id - String user ID
|
1001
|
+
#
|
1002
|
+
# Returns string value of the variable.
|
1003
|
+
|
1004
|
+
config = project_config
|
1005
|
+
variable_value = variable['defaultValue']
|
1006
|
+
if variation
|
1007
|
+
if feature_enabled == true
|
1008
|
+
variation_variable_usages = config.variation_id_to_variable_usage_map[variation['id']]
|
1009
|
+
variable_id = variable['id']
|
1010
|
+
if variation_variable_usages&.key?(variable_id)
|
1011
|
+
variable_value = variation_variable_usages[variable_id]['value']
|
1012
|
+
@logger.log(Logger::INFO,
|
1013
|
+
"Got variable value '#{variable_value}' for variable '#{variable['key']}' of feature flag '#{feature_flag_key}'.")
|
1014
|
+
else
|
1015
|
+
@logger.log(Logger::DEBUG,
|
1016
|
+
"Variable value is not defined. Returning the default variable value '#{variable_value}' for variable '#{variable['key']}'.")
|
1017
|
+
|
1018
|
+
end
|
1019
|
+
else
|
1020
|
+
@logger.log(Logger::DEBUG,
|
1021
|
+
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'. Returning the default variable value '#{variable_value}'.")
|
1022
|
+
end
|
1023
|
+
else
|
1024
|
+
@logger.log(Logger::INFO,
|
1025
|
+
"User '#{user_id}' was not bucketed into experiment or rollout for feature flag '#{feature_flag_key}'. Returning the default variable value '#{variable_value}'.")
|
1026
|
+
end
|
1027
|
+
variable_value
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
def user_inputs_valid?(attributes = nil, event_tags = nil)
|
1031
|
+
# Helper method to validate user inputs.
|
1032
|
+
#
|
1033
|
+
# attributes - Dict representing user attributes.
|
1034
|
+
# event_tags - Dict representing metadata associated with an event.
|
1035
|
+
#
|
1036
|
+
# Returns boolean True if inputs are valid. False otherwise.
|
1037
|
+
|
1038
|
+
return false if !attributes.nil? && !attributes_valid?(attributes)
|
1039
|
+
|
1040
|
+
return false if !event_tags.nil? && !event_tags_valid?(event_tags)
|
1041
|
+
|
1042
|
+
true
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
def attributes_valid?(attributes)
|
1046
|
+
unless Helpers::Validator.attributes_valid?(attributes)
|
1047
|
+
@logger.log(Logger::ERROR, 'Provided attributes are in an invalid format.')
|
1048
|
+
@error_handler.handle_error(InvalidAttributeFormatError)
|
1049
|
+
return false
|
1050
|
+
end
|
1051
|
+
true
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
def event_tags_valid?(event_tags)
|
1055
|
+
unless Helpers::Validator.event_tags_valid?(event_tags)
|
1056
|
+
@logger.log(Logger::ERROR, 'Provided event tags are in an invalid format.')
|
1057
|
+
@error_handler.handle_error(InvalidEventTagFormatError)
|
1058
|
+
return false
|
1059
|
+
end
|
1060
|
+
true
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
def validate_instantiation_options
|
1064
|
+
raise InvalidInputError, 'logger' unless Helpers::Validator.logger_valid?(@logger)
|
1065
|
+
|
1066
|
+
unless Helpers::Validator.error_handler_valid?(@error_handler)
|
1067
|
+
@error_handler = NoOpErrorHandler.new
|
1068
|
+
raise InvalidInputError, 'error_handler'
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
return if Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
|
1072
|
+
|
1073
|
+
@event_dispatcher = EventDispatcher.new(logger: @logger, error_handler: @error_handler)
|
1074
|
+
raise InvalidInputError, 'event_dispatcher'
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def send_impression(config, experiment, variation_key, flag_key, rule_key, enabled, rule_type, user_id, attributes = nil)
|
1078
|
+
if experiment.nil?
|
1079
|
+
experiment = {
|
1080
|
+
'id' => '',
|
1081
|
+
'key' => '',
|
1082
|
+
'layerId' => '',
|
1083
|
+
'status' => '',
|
1084
|
+
'variations' => [],
|
1085
|
+
'trafficAllocation' => [],
|
1086
|
+
'audienceIds' => [],
|
1087
|
+
'audienceConditions' => [],
|
1088
|
+
'forcedVariations' => {}
|
1089
|
+
}
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
experiment_id = experiment['id']
|
1093
|
+
experiment_key = experiment['key']
|
1094
|
+
|
1095
|
+
variation_id = config.get_variation_id_from_key_by_experiment_id(experiment_id, variation_key) unless experiment_id.empty?
|
1096
|
+
|
1097
|
+
unless variation_id
|
1098
|
+
variation = !flag_key.empty? ? get_flag_variation(flag_key, variation_key, 'key') : nil
|
1099
|
+
variation_id = variation ? variation['id'] : ''
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
metadata = {
|
1103
|
+
flag_key: flag_key,
|
1104
|
+
rule_key: rule_key,
|
1105
|
+
rule_type: rule_type,
|
1106
|
+
variation_key: variation_key,
|
1107
|
+
enabled: enabled
|
1108
|
+
}
|
1109
|
+
|
1110
|
+
user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes)
|
1111
|
+
@event_processor.process(user_event)
|
1112
|
+
return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive?
|
1113
|
+
|
1114
|
+
@logger.log(Logger::INFO, "Activating user '#{user_id}' in experiment '#{experiment_key}'.")
|
1115
|
+
|
1116
|
+
experiment = nil if experiment_id == ''
|
1117
|
+
variation = nil
|
1118
|
+
variation = config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) unless experiment.nil?
|
1119
|
+
log_event = EventFactory.create_log_event(user_event, @logger)
|
1120
|
+
@notification_center.send_notifications(
|
1121
|
+
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
|
1122
|
+
experiment, user_id, attributes, variation, log_event
|
1123
|
+
)
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
def project_config
|
1127
|
+
@config_manager.config
|
1128
|
+
end
|
1129
|
+
end
|
1130
|
+
end
|