@@ -369,6 +369,212 @@ describe('EppoClient E2E test', () => {
369
369
expect ( assignment ) . toEqual ( 'control' ) ;
370
370
} ) ;
371
371
372
+ describe ( 'assignment logging deduplication' , ( ) => {
373
+ let client : EppoClient ;
374
+ let mockLogger : IAssignmentLogger ;
375
+
376
+ beforeEach ( ( ) => {
377
+ mockLogger = td . object < IAssignmentLogger > ( ) ;
378
+
379
+ storage . setEntries ( { [ flagKey ] : mockExperimentConfig } ) ;
380
+ client = new EppoClient ( storage ) ;
381
+ client . setLogger ( mockLogger ) ;
382
+ } ) ;
383
+
384
+ it ( 'logs duplicate assignments without an assignment cache' , ( ) => {
385
+ client . disableAssignmentCache ( ) ;
386
+
387
+ client . getAssignment ( 'subject-10' , flagKey ) ;
388
+ client . getAssignment ( 'subject-10' , flagKey ) ;
389
+
390
+ // call count should be 2 because there is no cache.
391
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 2 ) ;
392
+ } ) ;
393
+
394
+ it ( 'does not log duplicate assignments' , ( ) => {
395
+ client . useNonExpiringAssignmentCache ( ) ;
396
+
397
+ client . getAssignment ( 'subject-10' , flagKey ) ;
398
+ client . getAssignment ( 'subject-10' , flagKey ) ;
399
+
400
+ // call count should be 1 because the second call is a cache hit and not logged.
401
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 1 ) ;
402
+ } ) ;
403
+
404
+ it ( 'logs assignment again after the lru cache is full' , ( ) => {
405
+ client . useLRUAssignmentCache ( 2 ) ;
406
+
407
+ client . getAssignment ( 'subject-10' , flagKey ) ; // logged
408
+ client . getAssignment ( 'subject-10' , flagKey ) ; // cached
409
+
410
+ client . getAssignment ( 'subject-11' , flagKey ) ; // logged
411
+ client . getAssignment ( 'subject-11' , flagKey ) ; // cached
412
+
413
+ client . getAssignment ( 'subject-12' , flagKey ) ; // cache evicted subject-10, logged
414
+ client . getAssignment ( 'subject-10' , flagKey ) ; // previously evicted, logged
415
+ client . getAssignment ( 'subject-12' , flagKey ) ; // cached
416
+
417
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 4 ) ;
418
+ } ) ;
419
+
420
+ it ( 'does not cache assignments if the logger had an exception' , ( ) => {
421
+ td . when ( mockLogger . logAssignment ( td . matchers . anything ( ) ) ) . thenThrow (
422
+ new Error ( 'logging error' ) ,
423
+ ) ;
424
+
425
+ const client = new EppoClient ( storage ) ;
426
+ client . setLogger ( mockLogger ) ;
427
+
428
+ client . getAssignment ( 'subject-10' , flagKey ) ;
429
+ client . getAssignment ( 'subject-10' , flagKey ) ;
430
+
431
+ // call count should be 2 because the first call had an exception
432
+ // therefore we are not sure the logger was successful and try again.
433
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 2 ) ;
434
+ } ) ;
435
+
436
+ it ( 'logs for each unique flag' , ( ) => {
437
+ storage . setEntries ( {
438
+ [ flagKey ] : mockExperimentConfig ,
439
+ 'flag-2' : {
440
+ ...mockExperimentConfig ,
441
+ name : 'flag-2' ,
442
+ } ,
443
+ 'flag-3' : {
444
+ ...mockExperimentConfig ,
445
+ name : 'flag-3' ,
446
+ } ,
447
+ } ) ;
448
+
449
+ client . useNonExpiringAssignmentCache ( ) ;
450
+
451
+ client . getAssignment ( 'subject-10' , flagKey ) ;
452
+ client . getAssignment ( 'subject-10' , flagKey ) ;
453
+ client . getAssignment ( 'subject-10' , 'flag-2' ) ;
454
+ client . getAssignment ( 'subject-10' , 'flag-2' ) ;
455
+ client . getAssignment ( 'subject-10' , 'flag-3' ) ;
456
+ client . getAssignment ( 'subject-10' , 'flag-3' ) ;
457
+ client . getAssignment ( 'subject-10' , flagKey ) ;
458
+ client . getAssignment ( 'subject-10' , 'flag-2' ) ;
459
+ client . getAssignment ( 'subject-10' , 'flag-3' ) ;
460
+
461
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 3 ) ;
462
+ } ) ;
463
+
464
+ it ( 'logs twice for the same flag when rollout increases/flag changes' , ( ) => {
465
+ client . useNonExpiringAssignmentCache ( ) ;
466
+
467
+ storage . setEntries ( {
468
+ [ flagKey ] : {
469
+ ...mockExperimentConfig ,
470
+ allocations : {
471
+ allocation1 : {
472
+ percentExposure : 1 ,
473
+ variations : [
474
+ {
475
+ name : 'control' ,
476
+ value : 'control' ,
477
+ typedValue : 'control' ,
478
+ shardRange : {
479
+ start : 0 ,
480
+ end : 100 ,
481
+ } ,
482
+ } ,
483
+ {
484
+ name : 'treatment' ,
485
+ value : 'treatment' ,
486
+ typedValue : 'treatment' ,
487
+ shardRange : {
488
+ start : 0 ,
489
+ end : 0 ,
490
+ } ,
491
+ } ,
492
+ ] ,
493
+ } ,
494
+ } ,
495
+ } ,
496
+ } ) ;
497
+ client . getAssignment ( 'subject-10' , flagKey ) ;
498
+
499
+ storage . setEntries ( {
500
+ [ flagKey ] : {
501
+ ...mockExperimentConfig ,
502
+ allocations : {
503
+ allocation1 : {
504
+ percentExposure : 1 ,
505
+ variations : [
506
+ {
507
+ name : 'control' ,
508
+ value : 'control' ,
509
+ typedValue : 'control' ,
510
+ shardRange : {
511
+ start : 0 ,
512
+ end : 0 ,
513
+ } ,
514
+ } ,
515
+ {
516
+ name : 'treatment' ,
517
+ value : 'treatment' ,
518
+ typedValue : 'treatment' ,
519
+ shardRange : {
520
+ start : 0 ,
521
+ end : 100 ,
522
+ } ,
523
+ } ,
524
+ ] ,
525
+ } ,
526
+ } ,
527
+ } ,
528
+ } ) ;
529
+ client . getAssignment ( 'subject-10' , flagKey ) ;
530
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 2 ) ;
531
+ } ) ;
532
+
533
+ it ( 'logs the same subject/flag/variation after two changes' , ( ) => {
534
+ client . useNonExpiringAssignmentCache ( ) ;
535
+
536
+ // original configuration version
537
+ storage . setEntries ( { [ flagKey ] : mockExperimentConfig } ) ;
538
+
539
+ client . getAssignment ( 'subject-10' , flagKey ) ; // log this assignment
540
+ client . getAssignment ( 'subject-10' , flagKey ) ; // cache hit, don't log
541
+
542
+ // change the flag
543
+ storage . setEntries ( {
544
+ [ flagKey ] : {
545
+ ...mockExperimentConfig ,
546
+ allocations : {
547
+ allocation1 : {
548
+ percentExposure : 1 ,
549
+ variations : [
550
+ {
551
+ name : 'some-new-treatment' ,
552
+ value : 'some-new-treatment' ,
553
+ typedValue : 'some-new-treatment' ,
554
+ shardRange : {
555
+ start : 0 ,
556
+ end : 100 ,
557
+ } ,
558
+ } ,
559
+ ] ,
560
+ } ,
561
+ } ,
562
+ } ,
563
+ } ) ;
564
+
565
+ client . getAssignment ( 'subject-10' , flagKey ) ; // log this assignment
566
+ client . getAssignment ( 'subject-10' , flagKey ) ; // cache hit, don't log
567
+
568
+ // change the flag again, back to the original
569
+ storage . setEntries ( { [ flagKey ] : mockExperimentConfig } ) ;
570
+
571
+ client . getAssignment ( 'subject-10' , flagKey ) ; // important: log this assignment
572
+ client . getAssignment ( 'subject-10' , flagKey ) ; // cache hit, don't log
573
+
574
+ expect ( td . explain ( mockLogger . logAssignment ) . callCount ) . toEqual ( 3 ) ;
575
+ } ) ;
576
+ } ) ;
577
+
372
578
it ( 'only returns variation if subject matches rules' , ( ) => {
373
579
const entry = {
374
580
...mockExperimentConfig ,
0 commit comments