diff --git a/.github/workflows/instrumented.yml b/.github/workflows/instrumented.yml index c4f08418d..69ac5077c 100644 --- a/.github/workflows/instrumented.yml +++ b/.github/workflows/instrumented.yml @@ -11,12 +11,12 @@ concurrency: jobs: test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - api-level: [ 29 ] + api-level: [ 28 ] shard: [ 0, 1, 2, 3 ] steps: @@ -53,7 +53,6 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - avd-name: macOS-avd-arm64-v8a-29 force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true diff --git a/build.gradle b/build.gradle index 2f6e4d3fb..f60bc39ea 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android' apply from: 'spec.gradle' ext { - splitVersion = '5.2.0' + splitVersion = '5.3.0-rc1' } android { diff --git a/spec.gradle b/spec.gradle index b2e667495..e8bcdf624 100644 --- a/spec.gradle +++ b/spec.gradle @@ -1,5 +1,5 @@ ext { // flags spec contains a String with the feature flags specification - flagsSpec = '1.2' + flagsSpec = '1.3' } diff --git a/src/androidTest/assets/attributes_test_split_change.json b/src/androidTest/assets/attributes_test_split_change.json index 980b0a0fb..69a7aa177 100644 --- a/src/androidTest/assets/attributes_test_split_change.json +++ b/src/androidTest/assets/attributes_test_split_change.json @@ -1,182 +1,187 @@ { - "splits":[ + "ff": { + "splits": [ { - "trafficTypeName":"client", - "name":"workm", - "trafficAllocation":100, - "trafficAllocationSeed":147392224, - "seed":524417105, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"on", - "changeNumber":1602796638344, - "algo":2, - "configurations":{ - - }, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"client", - "attribute":"num_value" - }, - "matcherType":"EQUAL_TO", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":{ - "dataType":"NUMBER", - "value":10 - }, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on_num_10", - "size":100 + "trafficTypeName": "client", + "name": "workm", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": { + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": "num_value" }, - { - "treatment":"off", - "size":0 - } - ], - "label":"rule 1" - } - ] - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"client", - "attribute":"str_value" + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 }, - "matcherType":"MATCHES_STRING", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":"yes" - } - ] - }, - "partitions":[ - { - "treatment":"on_str_yes", - "size":100 + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"rule 2" + "partitions": [ + { + "treatment": "on_num_10", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "rule 1" + } + ] }, { - "trafficTypeName":"client", - "name":"workm1", - "trafficAllocation":100, - "trafficAllocationSeed":147392224, - "seed":524417105, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"on", - "changeNumber":1602796638344, - "algo":2, - "configurations":{ - - }, - "conditions":[ + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"client", - "attribute":"num_value_a" - }, - "matcherType":"EQUAL_TO", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":{ - "dataType":"NUMBER", - "value":20 - }, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on_num_20", - "size":100 - }, - { - "treatment":"off", - "size":0 - } - ], - "label":"rule 3" + "keySelector": { + "trafficType": "client", + "attribute": "str_value" + }, + "matcherType": "MATCHES_STRING", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": "yes" } - ] + ] + }, + "partitions": [ + { + "treatment": "on_str_yes", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "rule 2" }, { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"client", - "attribute":"str_value_a" + "trafficTypeName": "client", + "name": "workm1", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": { + }, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": "num_value_a" }, - "matcherType":"MATCHES_STRING", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":"no" - } - ] - }, - "partitions":[ - { - "treatment":"on_str_no", - "size":100 + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 20 + }, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, + "partitions": [ + { + "treatment": "on_num_20", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "rule 3" + } + ] + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ { - "treatment":"off", - "size":0 + "keySelector": { + "trafficType": "client", + "attribute": "str_value_a" + }, + "matcherType": "MATCHES_STRING", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": "no" } - ], - "label":"rule 3" + ] + }, + "partitions": [ + { + "treatment": "on_str_no", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "rule 3" } - ], - "since":1602796638344, - "till":1602796638344 + ], + "since": 1602796638344, + "till": 1602796638344 + }, + "rbs": { + "d": [], + "s": 1602796638341, + "t": 1602796638341 + } } \ No newline at end of file diff --git a/src/androidTest/assets/bucket_split_test.json b/src/androidTest/assets/bucket_split_test.json index 1a9d3e212..f5608b9b7 100644 --- a/src/androidTest/assets/bucket_split_test.json +++ b/src/androidTest/assets/bucket_split_test.json @@ -1,439 +1,446 @@ { - "splits": [ - { - "name": "bucket_test", - "changeNumber": 1506703262916, - "trafficAllocationSeed": -1277047201, - "seed": 1844331483, - "configurations": {}, - "conditions": [ - { - "conditionType": "rollout", - "label": "default rule", - "matcherGroup": { - "matchers": [ - { - "matcherType": "ALL_KEYS", - "keySelector": { - "trafficType": "account" - }, - "negate": false + "ff": { + "splits": [ + { + "name": "bucket_test", + "changeNumber": 1506703262916, + "trafficAllocationSeed": -1277047201, + "seed": 1844331483, + "configurations": {}, + "conditions": [ + { + "conditionType": "rollout", + "label": "default rule", + "matcherGroup": { + "matchers": [ + { + "matcherType": "ALL_KEYS", + "keySelector": { + "trafficType": "account" + }, + "negate": false + } + ], + "combiner": "AND" + }, + "partitions": [ + { + "size": 1, + "treatment": "V1" + }, + { + "size": 1, + "treatment": "V2" + }, + { + "size": 1, + "treatment": "V3" + }, + { + "size": 1, + "treatment": "V4" + }, + { + "size": 1, + "treatment": "V5" + }, + { + "size": 1, + "treatment": "V6" + }, + { + "size": 1, + "treatment": "V7" + }, + { + "size": 1, + "treatment": "V8" + }, + { + "size": 1, + "treatment": "V9" + }, + { + "size": 1, + "treatment": "V10" + }, + { + "size": 1, + "treatment": "V11" + }, + { + "size": 1, + "treatment": "V12" + }, + { + "size": 1, + "treatment": "V13" + }, + { + "size": 1, + "treatment": "V14" + }, + { + "size": 1, + "treatment": "V15" + }, + { + "size": 1, + "treatment": "V16" + }, + { + "size": 1, + "treatment": "V17" + }, + { + "size": 1, + "treatment": "V18" + }, + { + "size": 1, + "treatment": "V19" + }, + { + "size": 1, + "treatment": "V20" + }, + { + "size": 1, + "treatment": "V21" + }, + { + "size": 1, + "treatment": "V22" + }, + { + "size": 1, + "treatment": "V23" + }, + { + "size": 1, + "treatment": "V24" + }, + { + "size": 1, + "treatment": "V25" + }, + { + "size": 1, + "treatment": "V26" + }, + { + "size": 1, + "treatment": "V27" + }, + { + "size": 1, + "treatment": "V28" + }, + { + "size": 1, + "treatment": "V29" + }, + { + "size": 1, + "treatment": "V30" + }, + { + "size": 1, + "treatment": "V31" + }, + { + "size": 1, + "treatment": "V32" + }, + { + "size": 1, + "treatment": "V33" + }, + { + "size": 1, + "treatment": "V34" + }, + { + "size": 1, + "treatment": "V35" + }, + { + "size": 1, + "treatment": "V36" + }, + { + "size": 1, + "treatment": "V37" + }, + { + "size": 1, + "treatment": "V38" + }, + { + "size": 1, + "treatment": "V39" + }, + { + "size": 1, + "treatment": "V40" + }, + { + "size": 1, + "treatment": "V41" + }, + { + "size": 1, + "treatment": "V42" + }, + { + "size": 1, + "treatment": "V43" + }, + { + "size": 1, + "treatment": "V44" + }, + { + "size": 1, + "treatment": "V45" + }, + { + "size": 1, + "treatment": "V46" + }, + { + "size": 1, + "treatment": "V47" + }, + { + "size": 1, + "treatment": "V48" + }, + { + "size": 1, + "treatment": "V49" + }, + { + "size": 1, + "treatment": "V50" + }, + { + "size": 1, + "treatment": "V51" + }, + { + "size": 1, + "treatment": "V52" + }, + { + "size": 1, + "treatment": "V53" + }, + { + "size": 1, + "treatment": "V54" + }, + { + "size": 1, + "treatment": "V55" + }, + { + "size": 1, + "treatment": "V56" + }, + { + "size": 1, + "treatment": "V57" + }, + { + "size": 1, + "treatment": "V58" + }, + { + "size": 1, + "treatment": "V59" + }, + { + "size": 1, + "treatment": "V60" + }, + { + "size": 1, + "treatment": "V61" + }, + { + "size": 1, + "treatment": "V62" + }, + { + "size": 1, + "treatment": "V63" + }, + { + "size": 1, + "treatment": "V64" + }, + { + "size": 1, + "treatment": "V65" + }, + { + "size": 1, + "treatment": "V66" + }, + { + "size": 1, + "treatment": "V67" + }, + { + "size": 1, + "treatment": "V68" + }, + { + "size": 1, + "treatment": "V69" + }, + { + "size": 1, + "treatment": "V70" + }, + { + "size": 1, + "treatment": "V71" + }, + { + "size": 1, + "treatment": "V72" + }, + { + "size": 1, + "treatment": "V73" + }, + { + "size": 1, + "treatment": "V74" + }, + { + "size": 1, + "treatment": "V75" + }, + { + "size": 1, + "treatment": "V76" + }, + { + "size": 1, + "treatment": "V77" + }, + { + "size": 1, + "treatment": "V78" + }, + { + "size": 1, + "treatment": "V79" + }, + { + "size": 1, + "treatment": "V80" + }, + { + "size": 1, + "treatment": "V81" + }, + { + "size": 1, + "treatment": "V82" + }, + { + "size": 1, + "treatment": "V83" + }, + { + "size": 1, + "treatment": "V84" + }, + { + "size": 1, + "treatment": "V85" + }, + { + "size": 1, + "treatment": "V86" + }, + { + "size": 1, + "treatment": "V87" + }, + { + "size": 1, + "treatment": "V88" + }, + { + "size": 1, + "treatment": "V89" + }, + { + "size": 1, + "treatment": "V90" + }, + { + "size": 1, + "treatment": "V91" + }, + { + "size": 1, + "treatment": "V92" + }, + { + "size": 1, + "treatment": "V93" + }, + { + "size": 1, + "treatment": "V94" + }, + { + "size": 1, + "treatment": "V95" + }, + { + "size": 1, + "treatment": "V96" + }, + { + "size": 1, + "treatment": "V97" + }, + { + "size": 1, + "treatment": "V98" + }, + { + "size": 1, + "treatment": "V99" + }, + { + "size": 1, + "treatment": "V100" } - ], - "combiner": "AND" - }, - "partitions": [ - { - "size": 1, - "treatment": "V1" - }, - { - "size": 1, - "treatment": "V2" - }, - { - "size": 1, - "treatment": "V3" - }, - { - "size": 1, - "treatment": "V4" - }, - { - "size": 1, - "treatment": "V5" - }, - { - "size": 1, - "treatment": "V6" - }, - { - "size": 1, - "treatment": "V7" - }, - { - "size": 1, - "treatment": "V8" - }, - { - "size": 1, - "treatment": "V9" - }, - { - "size": 1, - "treatment": "V10" - }, - { - "size": 1, - "treatment": "V11" - }, - { - "size": 1, - "treatment": "V12" - }, - { - "size": 1, - "treatment": "V13" - }, - { - "size": 1, - "treatment": "V14" - }, - { - "size": 1, - "treatment": "V15" - }, - { - "size": 1, - "treatment": "V16" - }, - { - "size": 1, - "treatment": "V17" - }, - { - "size": 1, - "treatment": "V18" - }, - { - "size": 1, - "treatment": "V19" - }, - { - "size": 1, - "treatment": "V20" - }, - { - "size": 1, - "treatment": "V21" - }, - { - "size": 1, - "treatment": "V22" - }, - { - "size": 1, - "treatment": "V23" - }, - { - "size": 1, - "treatment": "V24" - }, - { - "size": 1, - "treatment": "V25" - }, - { - "size": 1, - "treatment": "V26" - }, - { - "size": 1, - "treatment": "V27" - }, - { - "size": 1, - "treatment": "V28" - }, - { - "size": 1, - "treatment": "V29" - }, - { - "size": 1, - "treatment": "V30" - }, - { - "size": 1, - "treatment": "V31" - }, - { - "size": 1, - "treatment": "V32" - }, - { - "size": 1, - "treatment": "V33" - }, - { - "size": 1, - "treatment": "V34" - }, - { - "size": 1, - "treatment": "V35" - }, - { - "size": 1, - "treatment": "V36" - }, - { - "size": 1, - "treatment": "V37" - }, - { - "size": 1, - "treatment": "V38" - }, - { - "size": 1, - "treatment": "V39" - }, - { - "size": 1, - "treatment": "V40" - }, - { - "size": 1, - "treatment": "V41" - }, - { - "size": 1, - "treatment": "V42" - }, - { - "size": 1, - "treatment": "V43" - }, - { - "size": 1, - "treatment": "V44" - }, - { - "size": 1, - "treatment": "V45" - }, - { - "size": 1, - "treatment": "V46" - }, - { - "size": 1, - "treatment": "V47" - }, - { - "size": 1, - "treatment": "V48" - }, - { - "size": 1, - "treatment": "V49" - }, - { - "size": 1, - "treatment": "V50" - }, - { - "size": 1, - "treatment": "V51" - }, - { - "size": 1, - "treatment": "V52" - }, - { - "size": 1, - "treatment": "V53" - }, - { - "size": 1, - "treatment": "V54" - }, - { - "size": 1, - "treatment": "V55" - }, - { - "size": 1, - "treatment": "V56" - }, - { - "size": 1, - "treatment": "V57" - }, - { - "size": 1, - "treatment": "V58" - }, - { - "size": 1, - "treatment": "V59" - }, - { - "size": 1, - "treatment": "V60" - }, - { - "size": 1, - "treatment": "V61" - }, - { - "size": 1, - "treatment": "V62" - }, - { - "size": 1, - "treatment": "V63" - }, - { - "size": 1, - "treatment": "V64" - }, - { - "size": 1, - "treatment": "V65" - }, - { - "size": 1, - "treatment": "V66" - }, - { - "size": 1, - "treatment": "V67" - }, - { - "size": 1, - "treatment": "V68" - }, - { - "size": 1, - "treatment": "V69" - }, - { - "size": 1, - "treatment": "V70" - }, - { - "size": 1, - "treatment": "V71" - }, - { - "size": 1, - "treatment": "V72" - }, - { - "size": 1, - "treatment": "V73" - }, - { - "size": 1, - "treatment": "V74" - }, - { - "size": 1, - "treatment": "V75" - }, - { - "size": 1, - "treatment": "V76" - }, - { - "size": 1, - "treatment": "V77" - }, - { - "size": 1, - "treatment": "V78" - }, - { - "size": 1, - "treatment": "V79" - }, - { - "size": 1, - "treatment": "V80" - }, - { - "size": 1, - "treatment": "V81" - }, - { - "size": 1, - "treatment": "V82" - }, - { - "size": 1, - "treatment": "V83" - }, - { - "size": 1, - "treatment": "V84" - }, - { - "size": 1, - "treatment": "V85" - }, - { - "size": 1, - "treatment": "V86" - }, - { - "size": 1, - "treatment": "V87" - }, - { - "size": 1, - "treatment": "V88" - }, - { - "size": 1, - "treatment": "V89" - }, - { - "size": 1, - "treatment": "V90" - }, - { - "size": 1, - "treatment": "V91" - }, - { - "size": 1, - "treatment": "V92" - }, - { - "size": 1, - "treatment": "V93" - }, - { - "size": 1, - "treatment": "V94" - }, - { - "size": 1, - "treatment": "V95" - }, - { - "size": 1, - "treatment": "V96" - }, - { - "size": 1, - "treatment": "V97" - }, - { - "size": 1, - "treatment": "V98" - }, - { - "size": 1, - "treatment": "V99" - }, - { - "size": 1, - "treatment": "V100" - } - ] - } - ], - "defaultTreatment": "V1", - "trafficAllocation": 100, - "algo": 2, - "trafficTypeName": "account", - "status": "ACTIVE", - "killed": false - } - ], - "since":1506703262916, - "till":1506703262916 + ] + } + ], + "defaultTreatment": "V1", + "trafficAllocation": 100, + "algo": 2, + "trafficTypeName": "account", + "status": "ACTIVE", + "killed": false + } + ], + "since": 1506703262916, + "till": 1506703262916 + }, + "rbs": { + "d": [], + "s": 1506703262911, + "t": 1506703262911 + } } diff --git a/src/androidTest/assets/simple_split.json b/src/androidTest/assets/simple_split.json index c3e17e20b..009545ab8 100644 --- a/src/androidTest/assets/simple_split.json +++ b/src/androidTest/assets/simple_split.json @@ -1,107 +1,114 @@ { - "splits": [ - { - "trafficTypeName": "client", - "name": "workm", - "trafficAllocation": 100, - "trafficAllocationSeed": 147392224, - "seed": 524417105, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "on", - "changeNumber": 1602796638344, - "algo": 2, - "configurations": {}, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "splits": [ + { + "trafficTypeName": "client", + "name": "workm", + "trafficAllocation": 100, + "trafficAllocationSeed": 147392224, + "seed": 524417105, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "changeNumber": 1602796638344, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 100 + }, { - "keySelector": { - "trafficType": "client", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "new_segment" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "conta", + "size": 0 } - ] + ], + "label": "in segment new_segment" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - }, - { - "treatment": "free", - "size": 100 - }, - { - "treatment": "conta", - "size": 0 - } - ], - "label": "in segment new_segment" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "client", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 0 + }, + { + "treatment": "conta", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - }, - { - "treatment": "off", - "size": 0 - }, - { - "treatment": "free", - "size": 0 - }, - { - "treatment": "conta", - "size": 0 - } - ], - "label": "default rule" - } - ] - } - ], - "since": 1602796638344, - "till": 1602796638344 + ], + "label": "default rule" + } + ] + } + ], + "since": 1602796638344, + "till": 1602796638344 + }, + "rbs": { + "d": [], + "s": 1602796638341, + "t": 1602796638341 + } } diff --git a/src/androidTest/assets/split_changes_1.json b/src/androidTest/assets/split_changes_1.json index 7eee38b79..abdd14bdd 100644 --- a/src/androidTest/assets/split_changes_1.json +++ b/src/androidTest/assets/split_changes_1.json @@ -1,2533 +1,2540 @@ { - "splits":[ - { - "trafficTypeName":"account", - "name":"FACUNDO_TEST", - "trafficAllocation":59, - "trafficAllocationSeed":-2108186082, - "seed":-1947050785, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1506703262916, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "nico_test", - "othertest" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "bla" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "ff": { + "splits": [ + { + "trafficTypeName": "account", + "name": "FACUNDO_TEST", + "trafficAllocation": 59, + "trafficAllocationSeed": -2108186082, + "seed": -1947050785, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "bla" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - }, - { - "treatment":"visa", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testing", - "trafficAllocation":100, - "trafficAllocationSeed":527505678, - "seed":-1716462249, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1506440189077, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"test_definition_as_of", - "treatments":[ - "on" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":true, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"Identify_UI", - "treatments":[ - "on" - ] - }, - "stringMatcherData":null + "treatment": "off", + "size": 100 + }, + { + "treatment": "visa", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testing", + "trafficAllocation": 100, + "trafficAllocationSeed": 527505678, + "seed": -1716462249, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506440189077, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "test_definition_as_of", + "treatments": [ + "on" + ] + }, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "Identify_UI", + "treatments": [ + "on" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in split test_definition_as_of treatment [on] and not in split Identify_UI treatment [on]" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"test_definition_as_of", - "treatments":[ - "off" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] + ], + "label": "in split test_definition_as_of treatment [on] and not in split Identify_UI treatment [on]" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "test_definition_as_of", + "treatments": [ + "off" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in split test_definition_as_of treatment [off]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testing222", - "trafficAllocation":100, - "trafficAllocationSeed":-397360967, - "seed":1058132210, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1505162627437, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 - }, - { - "treatment":"off", - "size":100 + ], + "label": "in split test_definition_as_of treatment [off]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testing222", + "trafficAllocation": 100, + "trafficAllocationSeed": -397360967, + "seed": 1058132210, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1505162627437, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"test222", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"a_new_split_2", - "trafficAllocation":99, - "trafficAllocationSeed":-1349440646, - "seed":-1536389703, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1505161671620, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "adil", - "bb", - "bbb", - "dd3c0800-30f1-11e7-ba78-12395d4a9634", - "pato", - "tito" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment2" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"test_copy" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "test222", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":52 - }, - { - "treatment":"off", - "size":48 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "a_new_split_2", + "trafficAllocation": 99, + "trafficAllocationSeed": -1349440646, + "seed": -1536389703, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1505161671620, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "adil", + "bb", + "bbb", + "dd3c0800-30f1-11e7-ba78-12395d4a9634", + "pato", + "tito" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"in segment all" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"asda" - }, - "matcherType":"STARTS_WITH", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "ee", - "aa" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - }, + "partitions": [ { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":true, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment2" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 - }, - { - "treatment":"off", - "size":100 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"asda starts with [ee, aa] and not in segment segment2" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"pp" - }, - "matcherType":"PART_OF_SET", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "pato", - "adil" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":100 - }, - { - "treatment":"off", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "test_copy" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"pp part of [pato, adil]" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"eee" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "1", - "2", - "trevorrr" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":20 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":80 - }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"eee in list [1, 2, ...]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"test_string_without_attr", - "trafficAllocation":100, - "trafficAllocationSeed":-782597068, - "seed":-1682478887, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1504805281437, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"Segment3" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 52 }, { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "something" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 48 + }, + { + "treatment": "testo", + "size": 0 } - ] + ], + "label": "in segment all" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "asda" + }, + "matcherType": "STARTS_WITH", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "ee", + "aa" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": true, + "userDefinedSegmentMatcherData": { + "segmentName": "segment2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment Segment3 and in list [something]" - } - ] - }, - { - "trafficTypeName":"user", - "name":"Test", - "trafficAllocation":100, - "trafficAllocationSeed":217539832, - "seed":52164426, - "status":"ACTIVE", - "killed":true, - "defaultTreatment":"off", - "changeNumber":1504206031141, - "algo":2, - "configurations": { - "off": "{\"f1\":\"v1\"}" - }, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"sample-segment" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"demo" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "testo", + "size": 0 } - ] + ], + "label": "asda starts with [ee, aa] and not in segment segment2" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"employees" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "pp" + }, + "matcherType": "PART_OF_SET", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "pato", + "adil" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "testo", + "size": 0 } - ] + ], + "label": "pp part of [pato, adil]" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"sample-segment" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "eee" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "1", + "2", + "trevorrr" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 20 }, { - "keySelector":{ - "trafficType":"user", - "attribute":"fsdfsd" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a", - "b", - "c", - "d" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 80 }, { - "keySelector":{ - "trafficType":"user", - "attribute":"asdasdasd" - }, - "matcherType":"STARTS_WITH", - "negate":true, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "asdad", - "sa", - "das" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "testo", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":10 + ], + "label": "eee in list [1, 2, ...]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "test_string_without_attr", + "trafficAllocation": 100, + "trafficAllocationSeed": -782597068, + "seed": -1682478887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1504805281437, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Segment3" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "something" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":90 - } - ], - "label":"in segment sample-segment and fsdfsd in list [a, b, ...] and asdasdasd does not start with [asdad, sa, ...]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Test_Save_1", - "trafficAllocation":100, - "trafficAllocationSeed":-257595325, - "seed":-665945237, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1503956389520, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "1", - "12", - "123", - "23" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" + ], + "label": "in segment Segment3 and in list [something]" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Test", + "trafficAllocation": 100, + "trafficAllocationSeed": 217539832, + "seed": 52164426, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1504206031141, + "algo": 2, + "configurations": { + "off": "{\"f1\":\"v1\"}" }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "asd" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample-segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"Segment3" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "demo" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":0 - }, - { - "treatment":"off", - "size":100 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample-segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "user", + "attribute": "fsdfsd" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a", + "b", + "c", + "d" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "user", + "attribute": "asdasdasd" + }, + "matcherType": "STARTS_WITH", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "asdad", + "sa", + "das" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"v1", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"TEST", - "trafficAllocation":100, - "trafficAllocationSeed":-673356676, - "seed":-511119211, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1503942404754, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 10 + }, + { + "treatment": "off", + "size": 90 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment sample-segment and fsdfsd in list [a, b, ...] and asdasdasd does not start with [asdad, sa, ...]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Test_Save_1", + "trafficAllocation": 100, + "trafficAllocationSeed": -257595325, + "seed": -665945237, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1503956389520, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "1", + "12", + "123", + "23" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_1", - "trafficAllocation":100, - "trafficAllocationSeed":987354894, - "seed":1292874260, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1503356075822, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":"atrib" - }, - "matcherType":"BETWEEN", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":{ - "dataType":"NUMBER", - "start":1474990940, - "end":1474990949 - }, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":95 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "asd" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":5 - } - ], - "label":"atrib between 1474990940 and 1474990949" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":90 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Segment3" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":10 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"nico_tests", - "trafficAllocation":100, - "trafficAllocationSeed":1409699192, - "seed":-1997241870, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1501791316810, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "nico_test_browser__key" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"employees" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "v1", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "TEST", + "trafficAllocation": 100, + "trafficAllocationSeed": -673356676, + "seed": -511119211, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1503942404754, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment employees" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo2222", - "trafficAllocation":100, - "trafficAllocationSeed":1164474086, - "seed":1270508512, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1501012403336, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "aasd" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment2" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_1", + "trafficAllocation": 100, + "trafficAllocationSeed": 987354894, + "seed": 1292874260, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1503356075822, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "atrib" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": "NUMBER", + "start": 1474990940, + "end": 1474990949 + }, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 95 + }, + { + "treatment": "off", + "size": 5 } - ] + ], + "label": "atrib between 1474990940 and 1474990949" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "ddddd" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 90 + }, + { + "treatment": "off", + "size": 10 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"Segment3" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "nico_tests", + "trafficAllocation": 100, + "trafficAllocationSeed": 1409699192, + "seed": -1997241870, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1501791316810, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test_browser__key" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "ppp", - "ppppp" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"pesto", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"test_copy" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment employees" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo2222", + "trafficAllocation": 100, + "trafficAllocationSeed": 1164474086, + "seed": 1270508512, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1501012403336, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "aasd" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"pesto", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"off", - "size":0 - }, - { - "treatment":"on", - "size":100 - }, - { - "treatment":"pesto", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "ddddd" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"arse", - "size":0 - }, - { - "treatment":"zzzzick", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"Tagging", - "trafficAllocation":100, - "trafficAllocationSeed":1910132597, - "seed":-311493896, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500590774768, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":50 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Segment3" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":50 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Welcome_Page_UI", - "trafficAllocation":100, - "trafficAllocationSeed":1848523960, - "seed":1608586361, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500577256901, - "algo":2, - "configurations": { - "off": "{\"the_emojis\":\"\uD83D\uDE01 -- áéíóúöÖüÜÏëç\"}" - }, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "ppp", + "ppppp" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"test", - "name":"pato_test_3", - "trafficAllocation":100, - "trafficAllocationSeed":458647735, - "seed":95677506, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500510847849, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"test", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "pesto", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "test_copy" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo23", - "trafficAllocation":100, - "trafficAllocationSeed":-689658216, - "seed":1711156051, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500064145947, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"sdsd" - }, - "matcherType":"EQUAL_TO_BOOLEAN", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":true, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "pesto", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"sdsd is true" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo909090", - "trafficAllocation":100, - "trafficAllocationSeed":-1196467266, - "seed":-1998101827, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500039488369, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"a" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"v" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "on", + "size": 100 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"asdadas" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a", - "b", - "c", - "d" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "pesto", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"sds" - }, - "matcherType":"CONTAINS_STRING", - "negate":true, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a", - "c", - "d" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "arse", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"xcvxv" - }, - "matcherType":"EQUAL_TO", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":{ - "dataType":"NUMBER", - "value":122 - }, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "zzzzick", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Tagging", + "trafficAllocation": 100, + "trafficAllocationSeed": 1910132597, + "seed": -311493896, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500590774768, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"a in list [a] and v in list [a] and asdadas in list [a, b, ...] and sds does not contain [a, c, ...] and xcvxv = 122" + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Welcome_Page_UI", + "trafficAllocation": 100, + "trafficAllocationSeed": 1848523960, + "seed": 1608586361, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500577256901, + "algo": 2, + "configurations": { + "off": "{\"the_emojis\":\"\uD83D\uDE01 -- áéíóúöÖüÜÏëç\"}" }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "test", + "name": "pato_test_3", + "trafficAllocation": 100, + "trafficAllocationSeed": 458647735, + "seed": 95677506, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500510847849, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "test", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo22", - "trafficAllocation":100, - "trafficAllocationSeed":1223277820, - "seed":-1152948537, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1499721434259, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo23", + "trafficAllocation": 100, + "trafficAllocationSeed": -689658216, + "seed": 1711156051, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500064145947, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "sdsd" + }, + "matcherType": "EQUAL_TO_BOOLEAN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": true, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"test-net", - "trafficAllocation":100, - "trafficAllocationSeed":-2038196969, - "seed":-862203077, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1499718635999, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "sdsd is true" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo909090", + "trafficAllocation": 100, + "trafficAllocationSeed": -1196467266, + "seed": -1998101827, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500039488369, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "a" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "v" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "asdadas" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a", + "b", + "c", + "d" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "sds" + }, + "matcherType": "CONTAINS_STRING", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a", + "c", + "d" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "xcvxv" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 122 + }, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"test_dep_2", - "trafficAllocation":100, - "trafficAllocationSeed":-806171485, - "seed":922684950, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1499707910800, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"Identify_UI", - "treatments":[ - "on" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "a in list [a] and v in list [a] and asdadas in list [a, b, ...] and sds does not contain [a, c, ...] and xcvxv = 122" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in split Identify_UI treatment [on]" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"Definition_As_Of_Clickable_UI", - "treatments":[ - "off" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":50 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo22", + "trafficAllocation": 100, + "trafficAllocationSeed": 1223277820, + "seed": -1152948537, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1499721434259, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":50 - } - ], - "label":"in split Definition_As_Of_Clickable_UI treatment [off]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Definition_As_Of_Clickable_UI", - "trafficAllocation":100, - "trafficAllocationSeed":-198035199, - "seed":-151947071, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1498168847351, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "tito", - "trevor" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "test-net", + "trafficAllocation": 100, + "trafficAllocationSeed": -2038196969, + "seed": -862203077, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1499718635999, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "test_dep_2", + "trafficAllocation": 100, + "trafficAllocationSeed": -806171485, + "seed": 922684950, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1499707910800, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "Identify_UI", + "treatments": [ + "on" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Identify_UI", - "trafficAllocation":100, - "trafficAllocationSeed":-139516103, - "seed":1543172523, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1498078888450, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "in split Identify_UI treatment [on]" }, - "partitions":[ - { - "treatment":"on", - "size":100 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "Definition_As_Of_Clickable_UI", + "treatments": [ + "off" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"test_definition_as_of", - "trafficAllocation":100, - "trafficAllocationSeed":1025823325, - "seed":-554248124, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1497289730024, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negatee":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in split Definition_As_Of_Clickable_UI treatment [off]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Definition_As_Of_Clickable_UI", + "trafficAllocation": 100, + "trafficAllocationSeed": -198035199, + "seed": -151947071, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1498168847351, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "tito", + "trevor" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"Test-jw-go", - "trafficAllocation":100, - "trafficAllocationSeed":768122971, - "seed":1539205707, - "status":"ACTIVE", - "defaultTreatment":"off", - "changeNumber":1496339112852, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "test1" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Identify_UI", + "trafficAllocation": 100, + "trafficAllocationSeed": -139516103, + "seed": 1543172523, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1498078888450, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_7", - "trafficAllocation":100, - "trafficAllocationSeed":-1340337178, - "seed":-1091938685, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593464885, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "test_definition_as_of", + "trafficAllocation": 100, + "trafficAllocationSeed": 1025823325, + "seed": -554248124, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1497289730024, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negatee": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_6", - "trafficAllocation":100, - "trafficAllocationSeed":-1202331834, - "seed":-48445256, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593448028, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Test-jw-go", + "trafficAllocation": 100, + "trafficAllocationSeed": 768122971, + "seed": 1539205707, + "status": "ACTIVE", + "defaultTreatment": "off", + "changeNumber": 1496339112852, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "test1" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_5", - "trafficAllocation":100, - "trafficAllocationSeed":2119994290, - "seed":-227092192, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593428034, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_4", - "trafficAllocation":100, - "trafficAllocationSeed":1066635158, - "seed":-850704283, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593412226, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_7", + "trafficAllocation": 100, + "trafficAllocationSeed": -1340337178, + "seed": -1091938685, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593464885, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_3", - "trafficAllocation":100, - "trafficAllocationSeed":1252392550, - "seed":971538037, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593352077, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_6", + "trafficAllocation": 100, + "trafficAllocationSeed": -1202331834, + "seed": -48445256, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593448028, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_2", - "trafficAllocation":100, - "trafficAllocationSeed":-285565213, - "seed":-1992295819, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593336752, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_5", + "trafficAllocation": 100, + "trafficAllocationSeed": 2119994290, + "seed": -227092192, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593428034, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_4", + "trafficAllocation": 100, + "trafficAllocationSeed": 1066635158, + "seed": -850704283, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593412226, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"broken_split", - "trafficAllocation":100, - "trafficAllocationSeed":-285565213, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593336752 - } - ], - "since":1506703262916, - "till":1506703262916 + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_3", + "trafficAllocation": 100, + "trafficAllocationSeed": 1252392550, + "seed": 971538037, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593352077, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_2", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593336752, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "broken_split", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593336752 + } + ], + "since": 1506703262916, + "till": 1506703262916 + }, + "rbs": { + "d": [], + "s": 1506703262911, + "t": 1506703262911 + } } \ No newline at end of file diff --git a/src/androidTest/assets/split_changes_flag_set-0.json b/src/androidTest/assets/split_changes_flag_set-0.json index 93be5fda4..9a6c1831b 100644 --- a/src/androidTest/assets/split_changes_flag_set-0.json +++ b/src/androidTest/assets/split_changes_flag_set-0.json @@ -1 +1 @@ -{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602798638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602797638344,"till":1602798638344} +{"ff":{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602798638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602797638344,"till":1602798638344},"rbs":{"d":[],"s":1602798638341,"t":1602798638341}} diff --git a/src/androidTest/assets/split_changes_flag_set-1.json b/src/androidTest/assets/split_changes_flag_set-1.json index 67f617712..14ba0e90d 100644 --- a/src/androidTest/assets/split_changes_flag_set-1.json +++ b/src/androidTest/assets/split_changes_flag_set-1.json @@ -1 +1 @@ -{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602797638344,"algo":2,"configurations":{},"sets":["set_1"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602797638344} +{"ff":{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602797638344,"algo":2,"configurations":{},"sets":["set_1"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602797638344},"rbs":{"d":[],"s":1602796638341,"t":1602796638341}} diff --git a/src/androidTest/assets/split_changes_flag_set-2.json b/src/androidTest/assets/split_changes_flag_set-2.json index 0e7576af6..8d1c23d6a 100644 --- a/src/androidTest/assets/split_changes_flag_set-2.json +++ b/src/androidTest/assets/split_changes_flag_set-2.json @@ -1 +1 @@ -{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_1","set_2"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]},{"trafficTypeName":"client","name":"workm_set_3","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602796638344} +{"ff":{"splits":[{"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_1","set_2"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]},{"trafficTypeName":"client","name":"workm_set_3","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":1602796638344,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_segment"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":0},{"treatment":"free","size":100},{"treatment":"conta","size":0}],"label":"in segment new_segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"client","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"booleanMatcherData":null,"dependencyMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"free","size":0},{"treatment":"conta","size":0}],"label":"default rule"}]}],"since":1602796638344,"till":1602796638344},"rbs":{"d":[],"s":1602796638341,"t":1602796638341}} diff --git a/src/androidTest/assets/split_changes_imp_toggle.json b/src/androidTest/assets/split_changes_imp_toggle.json index 2d4682675..c400fcf60 100644 --- a/src/androidTest/assets/split_changes_imp_toggle.json +++ b/src/androidTest/assets/split_changes_imp_toggle.json @@ -1,124 +1,131 @@ { - "splits": [ - { - "trafficTypeName": "user", - "name": "tracked", - "trafficAllocation": 100, - "trafficAllocationSeed": -285565213, - "seed": -1992295819, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1506703262916, - "algo": 2, - "impressionsDisabled": false, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "splits": [ + { + "trafficTypeName": "user", + "name": "tracked", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "impressionsDisabled": false, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, { - "keySelector": { - "trafficType": "client", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "new_segment" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "free", + "size": 100 + }, + { + "treatment": "conta", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 - }, - { - "treatment": "off", - "size": 0 - }, - { - "treatment": "free", - "size": 100 + ], + "label": "in segment new_segment" + } + ] + }, + { + "trafficTypeName": "user", + "name": "not_tracked", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "impressionsDisabled": true, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "new_segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "conta", - "size": 0 - } - ], - "label": "in segment new_segment" - } - ] - }, - { - "trafficTypeName": "user", - "name": "not_tracked", - "trafficAllocation": 100, - "trafficAllocationSeed": -285565213, - "seed": -1992295819, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1506703262916, - "algo": 2, - "impressionsDisabled": true, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "client", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "new_segment" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "free", + "size": 100 + }, + { + "treatment": "conta", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 - }, - { - "treatment": "off", - "size": 0 - }, - { - "treatment": "free", - "size": 100 - }, - { - "treatment": "conta", - "size": 0 - } - ], - "label": "in segment new_segment" - } - ] - } - ], - "since": 1506703262916, - "till": 1506703262916 -} + ], + "label": "in segment new_segment" + } + ] + } + ], + "since": 1506703262916, + "till": 1506703262916 + }, + "rbs": { + "d": [], + "s": 1506703262911, + "t": 1506703262911 + } +} \ No newline at end of file diff --git a/src/androidTest/assets/split_changes_large_segments-0.json b/src/androidTest/assets/split_changes_large_segments-0.json index 975f211d0..8d78431e5 100644 --- a/src/androidTest/assets/split_changes_large_segments-0.json +++ b/src/androidTest/assets/split_changes_large_segments-0.json @@ -1,49 +1,56 @@ { - "splits": [ - { - "trafficTypeName": "user", - "name": "ls_split", - "trafficAllocation": 100, - "trafficAllocationSeed": -285565213, - "seed": -1992295819, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1506703262916, - "algo": 2, - "conditions": [ - { - "conditionType": "WHITELIST", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "splits": [ + { + "trafficTypeName": "user", + "name": "ls_split", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_LARGE_SEGMENT", + "negate": false, + "userDefinedLargeSegmentMatcherData": { + "largeSegmentName": "large-segment1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": null, - "matcherType": "IN_LARGE_SEGMENT", - "negate": false, - "userDefinedLargeSegmentMatcherData": { - "largeSegmentName": "large-segment1" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "whitelisted segment" - } - ] - } - ], - "since": 1506703262916, - "till": 1506703262916 + ], + "label": "whitelisted segment" + } + ] + } + ], + "since": 1506703262916, + "till": 1506703262916 + }, + "rbs": { + "d": [], + "s": 1506703262911, + "t": 1506703262911 + } } diff --git a/src/androidTest/assets/split_changes_legacy.json b/src/androidTest/assets/split_changes_legacy.json new file mode 100644 index 000000000..f52d7fa54 --- /dev/null +++ b/src/androidTest/assets/split_changes_legacy.json @@ -0,0 +1,121 @@ +{ + "splits": [ + { + "trafficTypeName": "account", + "name": "FACUNDO_TEST", + "trafficAllocation": 59, + "trafficAllocationSeed": -2108186082, + "seed": -1947050785, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "bla" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "visa", + "size": 0 + } + ], + "label": "in segment all" + } + ] + } + ], + "since": -1, + "till": 1506703262916 +} \ No newline at end of file diff --git a/src/androidTest/assets/split_changes_rbs.json b/src/androidTest/assets/split_changes_rbs.json new file mode 100644 index 000000000..253374da7 --- /dev/null +++ b/src/androidTest/assets/split_changes_rbs.json @@ -0,0 +1,142 @@ +{ + "ff": { + "s": 10, + "t": 10, + "d": [ + { + "changeNumber": 10, + "trafficTypeName": "user", + "name": "rbs_split", + "trafficAllocation": 100, + "trafficAllocationSeed": 1828377380, + "seed": -286617921, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_RULE_BASED_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "rbs_test" + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "in rule based segment rbs_test" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "ALL_KEYS", + "negate": false + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ], + "configurations": {}, + "sets": [], + "impressionsDisabled": false + } + ] + }, + "rbs": { + "s": 5, + "t": 5, + "d": [ + { + "changeNumber": 5, + "name": "rbs_test", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded": { + "keys": [ + "key22" + ], + "segments": [ + { + "name": "segment_test", + "type": "standard" + } + ] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + }, + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "regular_segment" + } + } + ] + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/androidTest/assets/split_changes_semver.json b/src/androidTest/assets/split_changes_semver.json index 915178447..bb8e7f31c 100644 --- a/src/androidTest/assets/split_changes_semver.json +++ b/src/androidTest/assets/split_changes_semver.json @@ -1,431 +1,438 @@ { - "splits": [ - { - "trafficTypeName": "user", - "name": "semver_between", - "trafficAllocation": 100, - "trafficAllocationSeed": 1068038034, - "seed": -1053389887, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1675259356568, - "algo": 2, - "configurations": null, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "version" - }, - "matcherType": "BETWEEN_SEMVER", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": null, - "betweenStringMatcherData": { - "start": "1.22.9", - "end": "2.1.0" + "ff": { + "splits": [ + { + "trafficTypeName": "user", + "name": "semver_between", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": null, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "BETWEEN_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": null, + "betweenStringMatcherData": { + "start": "1.22.9", + "end": "2.1.0" + } } - } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "between semver" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "off", + "size": 0 } - ] + ], + "label": "between semver" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "semver_equalto", - "trafficAllocation": 100, - "trafficAllocationSeed": 1068038034, - "seed": -1053389887, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1675259356568, - "algo": 2, - "configurations": null, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": "version" - }, - "matcherType": "EQUAL_TO_SEMVER", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": "1.22.9" + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "semver_equalto", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": null, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.22.9" + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "equal to semver" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "off", + "size": 0 } - ] + ], + "label": "equal to semver" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "semver_greater_or_equalto", - "trafficAllocation": 100, - "trafficAllocationSeed": 1068038034, - "seed": -1053389887, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1675259356568, - "algo": 2, - "configurations": null, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": "version" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO_SEMVER", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": "1.22.9" + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "semver_greater_or_equalto", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": null, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.22.9" + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "greater than or equal to semver" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "off", + "size": 0 } - ] + ], + "label": "greater than or equal to semver" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "semver_inlist", - "trafficAllocation": 100, - "trafficAllocationSeed": 1068038034, - "seed": -1053389887, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1675259356568, - "algo": 2, - "configurations": null, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": "version" - }, - "matcherType": "IN_LIST_SEMVER", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "1.22.9", - "2.1.0" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": null, - "betweenStringMatcherData": null + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "semver_inlist", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": null, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "IN_LIST_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "1.22.9", + "2.1.0" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": null, + "betweenStringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "in list semver" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] + ], + "label": "in list semver" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - }, - { - "trafficTypeName": "user", - "name": "semver_less_or_equalto", - "trafficAllocation": 100, - "trafficAllocationSeed": 1068038034, - "seed": -1053389887, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "changeNumber": 1675259356568, - "algo": 2, - "configurations": null, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": "version" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO_SEMVER", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": "1.22.9" + "treatment": "off", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + ], + "label": "default rule" + } + ] + }, + { + "trafficTypeName": "user", + "name": "semver_less_or_equalto", + "trafficAllocation": 100, + "trafficAllocationSeed": 1068038034, + "seed": -1053389887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1675259356568, + "algo": 2, + "configurations": null, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.22.9" + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "less than or equal to semver" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "booleanMatcherData": null, - "dependencyMatcherData": null, - "stringMatcherData": null + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] + ], + "label": "less than or equal to semver" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "default rule" - } - ] - } - ], - "since": -1, - "till": 1675259356568 -} + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + } + ], + "label": "default rule" + } + ] + } + ], + "since": -1, + "till": 1675259356568 + }, + "rbs": { + "d": [], + "s": -1, + "t": 1675259356561 + } +} \ No newline at end of file diff --git a/src/androidTest/assets/splitchanges_int_test.json b/src/androidTest/assets/splitchanges_int_test.json index cd02900d2..2073118b1 100644 --- a/src/androidTest/assets/splitchanges_int_test.json +++ b/src/androidTest/assets/splitchanges_int_test.json @@ -1,57 +1,62 @@ { - "splits":[ - { - "trafficTypeName":"client", - "name":"test_feature", - "trafficAllocation":100, - "trafficAllocationSeed":-2049557248, - "seed":1188118899, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"yes", - "changeNumber":1567456937865, - "algo":2, - "configurations":{ - - }, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ + "ff": { + "splits": [ + { + "trafficTypeName": "client", + "name": "test_feature", + "trafficAllocation": 100, + "trafficAllocationSeed": -2049557248, + "seed": 1188118899, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "yes", + "changeNumber": 1567456937865, + "algo": 2, + "configurations": {}, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "client", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "si", + "size": 0 + }, { - "keySelector":{ - "trafficType":"client", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "no", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"si", - "size":0 - }, - { - "treatment":"no", - "size":100 - } - ], - "label":"default rule" - } - ] - } - ], - "since":1567456937865, - "till":1567456937865 + ], + "label": "default rule" + } + ] + } + ], + "since": 1567456937865, + "till": 1567456937865 + }, + "rbs": { + "d": [], + "s": 1567456937861, + "t": 1567456937861 + } } \ No newline at end of file diff --git a/src/androidTest/assets/splitchanges_unsupported_matcher.json b/src/androidTest/assets/splitchanges_unsupported_matcher.json index 71167a2da..53379c4d1 100644 --- a/src/androidTest/assets/splitchanges_unsupported_matcher.json +++ b/src/androidTest/assets/splitchanges_unsupported_matcher.json @@ -1,89 +1,95 @@ { - "splits": [ - { - "changeNumber": 1709843458770, - "trafficTypeName": "user", - "name": "feature_flag_for_test", - "trafficAllocation": 100, - "trafficAllocationSeed": -1364119282, - "seed": -605938843, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "algo": 2, - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "splits": [ + { + "changeNumber": 1709843458770, + "trafficTypeName": "user", + "name": "feature_flag_for_test", + "trafficAllocation": 100, + "trafficAllocationSeed": -1364119282, + "seed": -605938843, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "WRONG_MATCHER_TYPE", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "123123" + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "WRONG_MATCHER_TYPE", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": "123123" + "treatment": "off", + "size": 100 } - ] + ], + "label": "wrong matcher type" }, - "partitions": [ - { - "treatment": "on", - "size": 0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "sem" + }, + "matcherType": "MATCHES_STRING", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": "1.2.3" + } + ] }, - { - "treatment": "off", - "size": 100 - } - ], - "label": "wrong matcher type" - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ + { + "treatment": "on", + "size": 100 + }, { - "keySelector": { - "trafficType": "user", - "attribute": "sem" - }, - "matcherType": "MATCHES_STRING", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "dependencyMatcherData": null, - "booleanMatcherData": null, - "stringMatcherData": "1.2.3" + "treatment": "off", + "size": 0 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "sem matches 1.2.3" - } - ], - "configurations": { - }, - "sets": [] - } - ] -} + ], + "label": "sem matches 1.2.3" + } + ], + "configurations": {}, + "sets": [] + } + ] + }, + "rbs": { + "d": [], + "s": -1, + "t": 1709843458770 + } +} \ No newline at end of file diff --git a/src/androidTest/java/fake/SynchronizerSpyImpl.java b/src/androidTest/java/fake/SynchronizerSpyImpl.java index 81a1c8fdd..39ce46ff3 100644 --- a/src/androidTest/java/fake/SynchronizerSpyImpl.java +++ b/src/androidTest/java/fake/SynchronizerSpyImpl.java @@ -46,6 +46,11 @@ public void synchronizeSplits(long since) { mSynchronizer.synchronizeSplits(); } + @Override + public void synchronizeRuleBasedSegments(long changeNumber) { + mSynchronizer.synchronizeRuleBasedSegments(changeNumber); + } + @Override public void synchronizeSplits() { mSynchronizer.synchronizeSplits(); diff --git a/src/androidTest/java/helper/IntegrationHelper.java b/src/androidTest/java/helper/IntegrationHelper.java index 6144c6a7b..df13116fb 100644 --- a/src/androidTest/java/helper/IntegrationHelper.java +++ b/src/androidTest/java/helper/IntegrationHelper.java @@ -40,6 +40,7 @@ import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.lifecycle.SplitLifecycleManager; import io.split.android.client.network.HttpClient; @@ -94,12 +95,18 @@ public static void logSeparator(String tag) { Logger.i(tag, Utils.repeat("-", 200)); } + @Deprecated public static String emptySplitChanges(long since, long till) { return emptySplitChanges(till); } + @Deprecated public static String emptySplitChanges(long till) { - return String.format("{\"splits\":[], \"since\": %d, \"till\": %d }", till, till); + return emptyTargetingRulesChanges(till, till); + } + + public static String emptyTargetingRulesChanges(long flagsTill, long rbsTill) { + return String.format("{\"ff\":{\"splits\":[], \"since\": %d, \"till\": %d},\"rbs\":{\"d\":[],\"s\":%d,\"t\":%d}}", flagsTill, flagsTill, rbsTill, rbsTill); } public static SplitFactory buildFactory(String apiToken, Key key, SplitClientConfig config, @@ -314,12 +321,43 @@ public static String splitKill(String changeNumber, String splitName) { "data:{\"id\":\"-OT-rGuSwz:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:NDIxNjU0NTUyNw==\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MTgyNTg1MTgwNg==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_KILL\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"defaultTreatment\\\":\\\"off\\\",\\\"splitName\\\":\\\"" + splitName + "\\\"}\"}\n"; } + public static String splitChangeWithReferencedRbs(long flagSince, long rbsSince) { + return "{\"ff\":{\"s\":" + flagSince + ",\"t\":" + flagSince + ",\"d\":[]},\"rbs\":{\"s\":" + rbsSince + ",\"t\":" + rbsSince + ",\"d\":[{\"name\":\"new_rbs_test\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[],\"segments\":[]},\"conditions\":[{\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"WHITELIST\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"mdp\",\"tandil\",\"bsas\"]}},{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}}]},{\"name\":\"rbs_test\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[],\"segments\":[]},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"IN_RULE_BASED_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"new_rbs_test\"}}]}}]}]}}"; + } + + public static String rbsChange(String changeNumber, String previousChangeNumber, String compressedPayload) { + return rbsChange(changeNumber, previousChangeNumber, "0", compressedPayload); + } + + public static String rbsChangeGZip(String changeNumber, String previousChangeNumber, String compressedPayload) { + return rbsChange(changeNumber, previousChangeNumber, "1", compressedPayload); + } + + public static String rbsChangeZlib(String changeNumber, String previousChangeNumber, String compressedPayload) { + return rbsChange(changeNumber, previousChangeNumber, "2", compressedPayload); + } + + public static String rbsChange(String changeNumber, String previousChangeNumber, String compressionType, String compressedPayload) { + return "id: 123123\n" + + "event: message\n" + + "data: {\"id\":\"1111\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":" + System.currentTimeMillis() + ",\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_flags\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":" + changeNumber + ",\\\"pcn\\\":" + previousChangeNumber + ",\\\"c\\\":" + compressionType + ",\\\"d\\\":\\\"" + compressedPayload + "\\\"}\"}\n\n"; + } + public static String loadSplitChanges(Context context, String fileName) { - FileHelper fileHelper = new FileHelper(); - String change = fileHelper.loadFileContent(context, fileName); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + String change = getFileContentsAsString(context, fileName); + TargetingRulesChange targetingRulesChange = Json.fromJson(change, TargetingRulesChange.class); + SplitChange parsedChange = targetingRulesChange.getFeatureFlagsChange(); parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange, targetingRulesChange.getRuleBasedSegmentsChange())); + } + + public static String loadLegacySplitChanges(Context context, String fileName) { + return getFileContentsAsString(context, fileName); + } + + private static String getFileContentsAsString(Context context, String fileName) { + FileHelper fileHelper = new FileHelper(); + return fileHelper.loadFileContent(context, fileName); } /** @@ -404,6 +442,22 @@ public static String getSinceFromUri(URI uri) { } } + public static String getRbSinceFromUri(URI uri) { + try { + return parse(uri.getQuery()).get("rbSince"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static String getSpecFromUri(URI uri) { + try { + return parse(uri.getQuery()).get("s"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + static Map parse(String query) throws UnsupportedEncodingException { Map queryPairs = new HashMap<>(); String[] pairs = query.split("&"); @@ -423,6 +477,10 @@ static Map parse(String query) throws UnsupportedEncodingExcepti return queryPairs; } + public static SplitChange getChangeFromJsonString(String json) { + return Json.fromJson(json, TargetingRulesChange.class).getFeatureFlagsChange(); + } + /** * A simple interface to allow us to define the response for a given path */ diff --git a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java index d3f9152f4..7b14d8020 100644 --- a/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java +++ b/src/androidTest/java/io/split/android/client/service/impressions/observer/DedupeIntegrationTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; - import static helper.IntegrationHelper.getSinceFromUri; import android.content.Context; @@ -29,13 +28,11 @@ import fake.LifecycleManagerStub; import fake.SynchronizerSpyImpl; import helper.DatabaseHelper; -import helper.FileHelper; import helper.IntegrationHelper; import helper.TestableSplitConfigBuilder; import io.split.android.client.SplitClient; import io.split.android.client.SplitFactory; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.impressions.Impression; import io.split.android.client.impressions.ImpressionListener; @@ -341,10 +338,6 @@ private SplitFactory initSplitFactory(TestableSplitConfigBuilder builder, HttpCl } private String loadSplitChanges() { - FileHelper fileHelper = new FileHelper(); - String change = fileHelper.loadFileContent(mContext, "split_changes_1.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); - parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return IntegrationHelper.loadSplitChanges(mContext, "split_changes_1.json"); } } diff --git a/src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java b/src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java new file mode 100644 index 000000000..7d25c09d0 --- /dev/null +++ b/src/androidTest/java/tests/database/RuleBasedSegmentDaoTest.java @@ -0,0 +1,100 @@ +package tests.database; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import helper.DatabaseHelper; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; + +public class RuleBasedSegmentDaoTest { + + private SplitRoomDatabase database; + private RuleBasedSegmentDao dao; + + @Before + public void setUp() { + database = DatabaseHelper.getTestDatabase(ApplicationProvider.getApplicationContext()); + dao = database.ruleBasedSegmentDao(); + } + + @After + public void tearDown() { + database.close(); + } + + @Test + public void insertAndGetAll() { + RuleBasedSegmentEntity entity1 = new RuleBasedSegmentEntity("segment1", "body1", getUpdatedAt()); + RuleBasedSegmentEntity entity2 = new RuleBasedSegmentEntity("segment2", "body2", getUpdatedAt()); + + dao.insert(Arrays.asList(entity1, entity2)); + + List allSegments = dao.getAll(); + assertEquals(2, allSegments.size()); + assertTrue(allSegments.stream().anyMatch(e -> e.getName().equals("segment1"))); + assertTrue(allSegments.stream().anyMatch(e -> e.getName().equals("segment2"))); + } + + @Test + public void insertSingleEntity() { + RuleBasedSegmentEntity entity = new RuleBasedSegmentEntity("segment3", "body3", getUpdatedAt()); + dao.insert(entity); + + List allSegments = dao.getAll(); + assertEquals(1, allSegments.size()); + assertEquals("segment3", allSegments.get(0).getName()); + } + + @Test + public void updateEntity() { + RuleBasedSegmentEntity entity = new RuleBasedSegmentEntity("segment4", "body4", getUpdatedAt()); + dao.insert(entity); + + dao.update("segment4", "newSegment4", "newBody4"); + + List allSegments = dao.getAll(); + assertEquals(1, allSegments.size()); + assertEquals("newSegment4", allSegments.get(0).getName()); + assertEquals("newBody4", allSegments.get(0).getBody()); + } + + @Test + public void deleteEntities() { + RuleBasedSegmentEntity entity1 = new RuleBasedSegmentEntity("segment5", "body5", getUpdatedAt()); + RuleBasedSegmentEntity entity2 = new RuleBasedSegmentEntity("segment6", "body6", getUpdatedAt()); + dao.insert(Arrays.asList(entity1, entity2)); + + dao.delete(Arrays.asList("segment5")); + + List allSegments = dao.getAll(); + assertEquals(1, allSegments.size()); + assertEquals("segment6", allSegments.get(0).getName()); + } + + @Test + public void deleteAll() { + RuleBasedSegmentEntity entity1 = new RuleBasedSegmentEntity("segment7", "body7", getUpdatedAt()); + RuleBasedSegmentEntity entity2 = new RuleBasedSegmentEntity("segment8", "body8", getUpdatedAt()); + dao.insert(Arrays.asList(entity1, entity2)); + + dao.deleteAll(); + + List allSegments = dao.getAll(); + assertTrue(allSegments.isEmpty()); + } + + private static long getUpdatedAt() { + return System.currentTimeMillis(); + } +} diff --git a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java index c289ef64e..d8df85100 100644 --- a/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java +++ b/src/androidTest/java/tests/integration/FlagsSpecInRequestTest.java @@ -26,18 +26,15 @@ import fake.HttpResponseMock; import fake.HttpResponseMockDispatcher; import helper.DatabaseHelper; -import helper.FileHelper; import helper.IntegrationHelper; import helper.TestableSplitConfigBuilder; import io.split.android.client.SplitClient; import io.split.android.client.SplitFactory; import io.split.android.client.TestingConfig; -import io.split.android.client.dtos.SplitChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.storage.db.GeneralInfoEntity; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; -import io.split.android.client.utils.Json; import tests.integration.shared.TestingHelper; public class FlagsSpecInRequestTest { @@ -65,7 +62,7 @@ public void queryStringContainsFlagsSpec() throws InterruptedException { testingConfig.setFlagsSpec("1.1"); initSplitFactory(new TestableSplitConfigBuilder(), mHttpClient, testingConfig); - assertEquals("s=1.1&since=-1", mQueryString.get()); + assertEquals("s=1.1&since=-1&rbSince=-1", mQueryString.get()); } @Test @@ -74,7 +71,7 @@ public void nullFlagsSpecValueOmitsQueryParam() throws InterruptedException { testingConfig.setFlagsSpec(null); initSplitFactory(new TestableSplitConfigBuilder(), mHttpClient, testingConfig); - assertEquals("since=-1", mQueryString.get()); + assertEquals("since=-1&rbSince=-1", mQueryString.get()); } @Test @@ -95,7 +92,7 @@ public void newFlagsSpecIsUsedInRequest() throws InterruptedException { testingConfig.setFlagsSpec("1.2"); initSplitFactory(new TestableSplitConfigBuilder(), mHttpClient, testingConfig); - assertEquals("s=1.2&since=-1", mQueryString.get()); + assertEquals("s=1.2&since=-1&rbSince=-1", mQueryString.get()); } @Test @@ -122,7 +119,7 @@ public void authContainsFlagsSpec() throws InterruptedException { TestingConfig testingConfig = new TestingConfig(); initSplitFactory(new TestableSplitConfigBuilder(), mHttpClient, testingConfig); - assertEquals("s=1.2&users=CUSTOMER_ID", mAuthUrl.get().getQuery()); + assertEquals("s=1.3&users=CUSTOMER_ID", mAuthUrl.get().getQuery()); } @Test @@ -197,11 +194,8 @@ private SplitFactory initSplitFactory(TestableSplitConfigBuilder builder, HttpCl } private String loadSplitChanges() { - FileHelper fileHelper = new FileHelper(); - String change = fileHelper.loadFileContent(mContext, "split_changes_1.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); - parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + String changes = IntegrationHelper.loadSplitChanges(mContext, "split_changes_1.json"); + return changes; } private static SplitEntity newSplitEntity(String name, String trafficType, Set sets) { diff --git a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java index a8b64a13e..95de23a06 100644 --- a/src/androidTest/java/tests/integration/InitialChangeNumberTest.java +++ b/src/androidTest/java/tests/integration/InitialChangeNumberTest.java @@ -74,7 +74,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio mIsFirstChangeNumber = false; } return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + changeNumber + ", \"till\":" + (changeNumber) + "}"); + .setBody(IntegrationHelper.emptySplitChanges(changeNumber)); } else if (request.getPath().contains("/events/bulk")) { String trackRequestBody = request.getBody().readUtf8(); diff --git a/src/androidTest/java/tests/integration/IntegrationTest.java b/src/androidTest/java/tests/integration/IntegrationTest.java index 07f8d8c37..1aa8752de 100644 --- a/src/androidTest/java/tests/integration/IntegrationTest.java +++ b/src/androidTest/java/tests/integration/IntegrationTest.java @@ -303,7 +303,7 @@ public void testGetTreatmentFromCache() throws Exception { mRoomDb = DatabaseHelper.getTestDatabase(mContext); mRoomDb.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 10)); - SplitChange change = Json.fromJson(mJsonChanges.get(0), SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString(mJsonChanges.get(0)); List entities = new ArrayList<>(); for (Split split : change.splits) { String splitName = split.name; diff --git a/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java b/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java index 34b5dcc25..e0666789b 100644 --- a/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java +++ b/src/androidTest/java/tests/integration/MySegmentUpdatedTest.java @@ -37,6 +37,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.dtos.UserDefinedSegmentMatcherData; import io.split.android.client.events.SplitEvent; @@ -215,7 +216,7 @@ private void loadSplitChanges() { mJsonChanges = new ArrayList<>(); String jsonChange = fileHelper.loadFileContent(mContext, "splitchanges_int_test.json"); - SplitChange change = Json.fromJson(jsonChange, SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString(jsonChange); Split split = change.splits.get(0); split.changeNumber = change.since + 1; @@ -239,7 +240,7 @@ private void loadSplitChanges() { split.conditions.add(0, inSegmentOneCondition); split.conditions.add(0, inSegmentTwoCondition); - mJsonChanges.add(Json.toJson(change)); + mJsonChanges.add(Json.toJson(TargetingRulesChange.create(change))); } private Condition inSegmentCondition(String name) { diff --git a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java b/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java index ab1dc376b..a42fc79ec 100644 --- a/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java +++ b/src/androidTest/java/tests/integration/MySegmentsServerErrorTest.java @@ -34,6 +34,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.dtos.UserDefinedSegmentMatcherData; import io.split.android.client.events.SplitEvent; @@ -118,7 +119,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio .setBody(change); } return new MockResponse().setResponseCode(200) - .setBody(emptyChanges()); + .setBody(IntegrationHelper.emptySplitChanges(9567456937869L)); } else if (request.getPath().contains("/testImpressions/bulk")) { @@ -208,7 +209,7 @@ private void loadSplitChanges() { mJsonChanges = new ArrayList<>(); String jsonChange = fileHelper.loadFileContent(mContext, "splitchanges_int_test.json"); - SplitChange change = Json.fromJson(jsonChange, SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString(jsonChange); Split split = change.splits.get(0); split.changeNumber = change.since + 1; @@ -232,7 +233,7 @@ private void loadSplitChanges() { split.conditions.add(0, inSegmentOneCondition); split.conditions.add(0, inSegmentTwoCondition); - mJsonChanges.add(Json.toJson(change)); + mJsonChanges.add(Json.toJson(TargetingRulesChange.create(change))); } private Condition inSegmentCondition(String name) { diff --git a/src/androidTest/java/tests/integration/SingleSyncTest.java b/src/androidTest/java/tests/integration/SingleSyncTest.java index 4dd623b3b..13f540006 100644 --- a/src/androidTest/java/tests/integration/SingleSyncTest.java +++ b/src/androidTest/java/tests/integration/SingleSyncTest.java @@ -332,10 +332,6 @@ private HttpStreamResponseMock createStreamResponse(int status, BlockingQueue streamingResponseData) throws IOException { diff --git a/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java b/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java index 562939151..1dea774a7 100644 --- a/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java +++ b/src/androidTest/java/tests/integration/SplitChangesServerErrorTest.java @@ -32,6 +32,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -175,7 +176,7 @@ private void loadSplitChanges() { String jsonChange = fileHelper.loadFileContent(mContext, "splitchanges_int_test.json"); long prevChangeNumber = 0; for (int i = 0; i < 3; i++) { - SplitChange change = Json.fromJson(jsonChange, SplitChange.class); + SplitChange change = Json.fromJson(jsonChange, TargetingRulesChange.class).getFeatureFlagsChange(); if (prevChangeNumber != 0) { change.since = prevChangeNumber + CHANGE_INTERVAL; change.till = prevChangeNumber + CHANGE_INTERVAL; @@ -190,7 +191,7 @@ private void loadSplitChanges() { p1.size = (i < 2 ? 100 : 0); p2.treatment = "off"; p2.size = (i < 2 ? 0 : 100); - mJsonChanges.add(Json.toJson(change)); + mJsonChanges.add(Json.toJson(TargetingRulesChange.create(change))); } } diff --git a/src/androidTest/java/tests/integration/SplitChangesTest.java b/src/androidTest/java/tests/integration/SplitChangesTest.java index 754854d4c..89243574c 100644 --- a/src/androidTest/java/tests/integration/SplitChangesTest.java +++ b/src/androidTest/java/tests/integration/SplitChangesTest.java @@ -31,6 +31,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.events.SplitEvent; import io.split.android.client.impressions.Impression; @@ -99,7 +100,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio mLatchs.get(currReq - 1).countDown(); } return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\": 1567456938865, \"till\": 1567456938865 }"); + .setBody(IntegrationHelper.emptySplitChanges(1567456938865L)); } else if (request.getPath().contains("/testImpressions/bulk")) { @@ -208,7 +209,7 @@ private void loadSplitChanges() { String jsonChange = fileHelper.loadFileContent(mContext, "splitchanges_int_test.json"); long prevChangeNumber = 0; for (int i = 0; i < 4; i++) { - SplitChange change = Json.fromJson(jsonChange, SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString(jsonChange); if (prevChangeNumber != 0) { change.since = prevChangeNumber; change.till = prevChangeNumber + CHANGE_INTERVAL; @@ -223,7 +224,7 @@ private void loadSplitChanges() { p1.size = (even ? 100 : 0); p2.treatment = "off_" + i; p2.size = (even ? 0 : 100); - mJsonChanges.add(Json.toJson(change)); + mJsonChanges.add(Json.toJson(TargetingRulesChange.create(change))); } } diff --git a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java b/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java index a1a165599..3ecb346dd 100644 --- a/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java +++ b/src/androidTest/java/tests/integration/SplitFetchSpecificSplitTest.java @@ -107,6 +107,7 @@ public void testAll() throws Exception { .build(); splitRoomDatabase.clearAllTables(); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, 2)); + splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity("rbsChangeNumber", 4)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_FILTER_QUERY_STRING, expectedQs)); splitRoomDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, System.currentTimeMillis())); SplitClient client; @@ -143,7 +144,7 @@ public void testAll() throws Exception { latch.await(10, TimeUnit.SECONDS); - Assert.assertEquals("since=2" + expectedQs, mReceivedQueryString); + Assert.assertEquals("since=2&rbSince=4" + expectedQs, mReceivedQueryString); splitFactory.destroy(); } diff --git a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java index 548daad4d..c8b04b3ec 100644 --- a/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java +++ b/src/androidTest/java/tests/integration/SplitsTwoDifferentApiKeyTest.java @@ -34,6 +34,7 @@ import io.split.android.client.SplitFactory; import io.split.android.client.TestingConfig; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.SplitRoomDatabase; @@ -224,15 +225,15 @@ private String loadMockedData(String fileName) { } private void loadSplitChanges() { - mSplitChange = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + mSplitChange = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); } private String getSplitChanges(int factoryNumber, int hitNumber) { mSplitChange.splits.get(0).name = "split" + factoryNumber; mSplitChange.since = (hitNumber == 0 ? -1 : CHANGE_NUMBER_BASE + factoryNumber); mSplitChange.till = CHANGE_NUMBER_BASE + factoryNumber; - return Json.toJson(mSplitChange); + return Json.toJson(TargetingRulesChange.create(mSplitChange)); } private void testSplitsUpdate(long changeNumber, int factoryNumber) throws InterruptedException { diff --git a/src/androidTest/java/tests/integration/TrackTest.java b/src/androidTest/java/tests/integration/TrackTest.java index 550aac3dd..968ee3459 100644 --- a/src/androidTest/java/tests/integration/TrackTest.java +++ b/src/androidTest/java/tests/integration/TrackTest.java @@ -262,7 +262,7 @@ public void largeNumberInPropertiesTest() throws InterruptedException, SplitInst } private String emptyChanges() { - return "{\"splits\":[], \"since\": 9567456937869, \"till\": 9567456937869 }"; + return IntegrationHelper.emptySplitChanges(9567456937869L, 9567456937869L); } private Event findEvent(String type, Double value) { diff --git a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java index 949d415aa..ebaeea4fe 100644 --- a/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java +++ b/src/androidTest/java/tests/integration/attributes/AttributesIntegrationTest.java @@ -279,7 +279,7 @@ private List getSplitListFromJson() { FileHelper fileHelper = new FileHelper(); String s = fileHelper.loadFileContent(mContext, "attributes_test_split_change.json"); - SplitChange changes = Json.fromJson(s, SplitChange.class); + SplitChange changes = IntegrationHelper.getChangeFromJsonString(s); return changes.splits; } diff --git a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java b/src/androidTest/java/tests/integration/encryption/EncryptionTest.java index 6cf5fa6fa..747f25b61 100644 --- a/src/androidTest/java/tests/integration/encryption/EncryptionTest.java +++ b/src/androidTest/java/tests/integration/encryption/EncryptionTest.java @@ -8,6 +8,7 @@ import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Ignore; import org.junit.Test; import java.io.IOException; @@ -30,6 +31,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFactory; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.events.SplitEventTask; import io.split.android.client.service.impressions.ImpressionsMode; @@ -102,6 +104,7 @@ public void onPostExecutionView(SplitClient client) { verifyImpressions(testDatabase); } + @Ignore @Test public void impressionsCountAreEncrypted() throws IOException, InterruptedException { SplitRoomDatabase testDatabase = DatabaseHelper.getTestDatabase(mContext); @@ -359,10 +362,10 @@ private Map getResponses() { private String loadSplitChanges() { FileHelper fileHelper = new FileHelper(); String change = fileHelper.loadFileContent(mContext, "split_changes_1.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + SplitChange parsedChange = IntegrationHelper.getChangeFromJsonString(change); parsedChange.splits = parsedChange.splits.stream().filter(s -> s.name.equals("FACUNDO_TEST") || s.name.equals("testing")).collect(Collectors.toList()); parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange)); } } diff --git a/src/androidTest/java/tests/integration/init/InitializationTest.java b/src/androidTest/java/tests/integration/init/InitializationTest.java index 819a0cfce..340b74889 100644 --- a/src/androidTest/java/tests/integration/init/InitializationTest.java +++ b/src/androidTest/java/tests/integration/init/InitializationTest.java @@ -187,10 +187,6 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio } private String loadSplitChanges() { - FileHelper fileHelper = new FileHelper(); - String change = fileHelper.loadFileContent(mContext, "split_changes_1.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); - parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return IntegrationHelper.loadSplitChanges(mContext, "split_changes_1.json"); } } diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java b/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java index f8d876a69..c19195a2b 100644 --- a/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java +++ b/src/androidTest/java/tests/integration/largesegments/LargeSegmentTestHelper.java @@ -23,6 +23,7 @@ import io.split.android.client.SplitClient; import io.split.android.client.SplitFactory; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.Json; @@ -64,8 +65,8 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio return new MockResponse().setResponseCode(500); } - if (request.getRequestUrl().encodedPathSegments().contains("splitChanges")) { - updateEndpointHit("splitChanges"); + if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + updateEndpointHit(IntegrationHelper.ServicePath.SPLIT_CHANGES); return new MockResponse().setResponseCode(200).setBody(splitChangesLargeSegments(1602796638344L, 1602796638344L)); } else if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) { Thread.sleep(mMySegmentsDelay.get()); @@ -92,7 +93,7 @@ public void tearDown() throws IOException { private void initializeLatches() { mLatches = new ConcurrentHashMap<>(); - mLatches.put("splitChanges", new CountDownLatch(1)); + mLatches.put(IntegrationHelper.ServicePath.SPLIT_CHANGES, new CountDownLatch(1)); mLatches.put(IntegrationHelper.ServicePath.MEMBERSHIPS, new CountDownLatch(1)); } @@ -151,10 +152,10 @@ protected SplitClient getReadyClient(String matchingKey, SplitFactory factory) { private String splitChangesLargeSegments(long since, long till) { String change = mFileHelper.loadFileContent(mContext, "split_changes_large_segments-0.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + SplitChange parsedChange = IntegrationHelper.getChangeFromJsonString(change); parsedChange.since = since; parsedChange.till = till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange)); } } diff --git a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java index f5cfe3cdc..30d67a2e3 100644 --- a/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java +++ b/src/androidTest/java/tests/integration/largesegments/LargeSegmentsStreamingTest.java @@ -33,6 +33,7 @@ import io.split.android.client.SplitFactory; import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.Json; @@ -185,11 +186,11 @@ private HttpResponseMockDispatcher buildDispatcher() { private String splitChangesLargeSegments(long since, long till) { String change = mFileHelper.loadFileContent(mContext, "split_changes_large_segments-0.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + SplitChange parsedChange = IntegrationHelper.getChangeFromJsonString(change); parsedChange.since = since; parsedChange.till = till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange)); } private void initializeLatches() { diff --git a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java index e11af1561..98a9d70b7 100644 --- a/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java +++ b/src/androidTest/java/tests/integration/matcher/UnsupportedMatcherTest.java @@ -162,10 +162,6 @@ private SplitFactory initSplitFactory(TestableSplitConfigBuilder builder, HttpCl } private String loadSplitChanges() { - FileHelper fileHelper = new FileHelper(); - String change = fileHelper.loadFileContent(mContext, "splitchanges_unsupported_matcher.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); - parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return IntegrationHelper.loadSplitChanges(mContext, "splitchanges_unsupported_matcher.json"); } } diff --git a/src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java b/src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java new file mode 100644 index 000000000..09bcab6c7 --- /dev/null +++ b/src/androidTest/java/tests/integration/rbs/OutdatedProxyIntegrationTest.java @@ -0,0 +1,194 @@ +package tests.integration.rbs; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static helper.IntegrationHelper.emptyTargetingRulesChanges; +import static helper.IntegrationHelper.getSinceFromUri; +import static helper.IntegrationHelper.getSpecFromUri; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.ServiceEndpoints; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitFactory; +import io.split.android.client.TestingConfig; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.storage.db.GeneralInfoEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import tests.integration.shared.TestingHelper; + +public class OutdatedProxyIntegrationTest { + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + private MockWebServer mWebServer; + private Map mEndpointHits; + private Map mLatches; + private final AtomicBoolean mOutdatedProxy = new AtomicBoolean(false); + private final AtomicBoolean mSimulatedProxyError = new AtomicBoolean(false); + private final AtomicBoolean mRecoveryHit = new AtomicBoolean(false); + + @Before + public void setUp() throws IOException { + mEndpointHits = new ConcurrentHashMap<>(); + mOutdatedProxy.set(false); + initializeLatches(); + + mWebServer = new MockWebServer(); + mWebServer.setDispatcher(new Dispatcher() { + @NonNull + @Override + public MockResponse dispatch(@NonNull RecordedRequest request) { + if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.SPLIT_CHANGES)) { + updateEndpointHit(IntegrationHelper.ServicePath.SPLIT_CHANGES); + float specFromUri = Float.parseFloat(getSpecFromUri(request.getRequestUrl().uri())); + if (mOutdatedProxy.get() && specFromUri > 1.2f) { + mSimulatedProxyError.set(true); + return new MockResponse().setResponseCode(400); + } else if (mOutdatedProxy.get()) { + String body = (getSinceFromUri(request.getRequestUrl().uri()).equals("-1")) ? + IntegrationHelper.loadLegacySplitChanges(mContext, "split_changes_legacy.json") : + emptyTargetingRulesChanges(1506703262916L, -1L); + return new MockResponse().setResponseCode(200) + .setBody(body); + } + + if (!mOutdatedProxy.get()) { + if (request.getRequestUrl().uri().toString().contains("?s=1.3&since=-1&rbSince=-1")) { + mRecoveryHit.set(true); + } + } + + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.loadSplitChanges(mContext, "split_changes_rbs.json")); + } else if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) { + updateEndpointHit(IntegrationHelper.ServicePath.MEMBERSHIPS); + + return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.emptyAllSegments()); + } else { + return new MockResponse().setResponseCode(404); + } + } + }); + mWebServer.start(); + } + + @After + public void tearDown() throws IOException { + mWebServer.shutdown(); + } + + @Test + public void clientIsReadyEvenWhenUsingOutdatedProxy() { + mOutdatedProxy.set(true); + SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), getFactory()); + + assertNotNull(readyClient); + assertFalse(mRecoveryHit.get()); + assertTrue(mSimulatedProxyError.get()); + } + + @Test + public void clientIsReadyWithLatestProxy() { + mOutdatedProxy.set(false); + SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), getFactory()); + + assertNotNull(readyClient); + assertFalse(mRecoveryHit.get() && mOutdatedProxy.get()); + assertFalse(mSimulatedProxyError.get()); + } + + @Test + public void clientRecoversFromOutdatedProxy() { + mOutdatedProxy.set(false); + SplitRoomDatabase database = DatabaseHelper.getTestDatabase(mContext); + database.generalInfoDao().update(new GeneralInfoEntity("lastProxyCheckTimestamp", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(62))); + SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), getFactory(database)); + + assertNotNull(readyClient); + assertTrue(mRecoveryHit.get() && !mOutdatedProxy.get()); + assertFalse(mSimulatedProxyError.get()); + } + + private void initializeLatches() { + mLatches = new ConcurrentHashMap<>(); + mLatches.put(IntegrationHelper.ServicePath.SPLIT_CHANGES, new CountDownLatch(1)); + mLatches.put(IntegrationHelper.ServicePath.MEMBERSHIPS, new CountDownLatch(1)); + } + + private void updateEndpointHit(String splitChanges) { + if (mEndpointHits.containsKey(splitChanges)) { + mEndpointHits.get(splitChanges).getAndIncrement(); + } else { + mEndpointHits.put(splitChanges, new AtomicInteger(1)); + } + + if (mLatches.containsKey(splitChanges)) { + mLatches.get(splitChanges).countDown(); + } + } + + protected SplitFactory getFactory() { + return getFactory(null); + } + + protected SplitFactory getFactory(SplitRoomDatabase database) { + TestableSplitConfigBuilder configBuilder = new TestableSplitConfigBuilder() + .enableDebug() + .serviceEndpoints(ServiceEndpoints.builder() + .apiEndpoint("http://" + mWebServer.getHostName() + ":" + mWebServer.getPort()) + .build()); + + configBuilder.streamingEnabled(false); + configBuilder.ready(10000); + TestingConfig testingConfig = new TestingConfig(); + testingConfig.setFlagsSpec("1.3"); + return IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + IntegrationHelper.dummyUserKey(), + configBuilder.build(), + mContext, + null, database, null, testingConfig, null); + } + + protected SplitClient getReadyClient(String matchingKey, SplitFactory factory) { + CountDownLatch countDownLatch = new CountDownLatch(1); + + SplitClient client = factory.client(matchingKey); + boolean await; + client.on(SplitEvent.SDK_READY, TestingHelper.testTask(countDownLatch)); + try { + await = countDownLatch.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (!await) { + fail("Client is not ready"); + } + + return client; + } +} diff --git a/src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java b/src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java new file mode 100644 index 000000000..6945579d3 --- /dev/null +++ b/src/androidTest/java/tests/integration/rbs/RuleBasedSegmentsIntegrationTest.java @@ -0,0 +1,276 @@ +package tests.integration.rbs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static helper.IntegrationHelper.rbsChange; +import static helper.IntegrationHelper.splitChangeV2; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import fake.HttpClientMock; +import fake.HttpResponseMock; +import fake.HttpResponseMockDispatcher; +import helper.DatabaseHelper; +import helper.IntegrationHelper; +import helper.TestableSplitConfigBuilder; +import io.split.android.client.SplitClient; +import io.split.android.client.SplitClientConfig; +import io.split.android.client.SplitFactory; +import io.split.android.client.api.Key; +import io.split.android.client.events.SplitEvent; +import io.split.android.client.events.SplitEventTask; +import io.split.android.client.storage.db.MySegmentEntity; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.utils.logger.Logger; +import tests.integration.shared.TestingHelper; + +public class RuleBasedSegmentsIntegrationTest { + + private static final String rbsChange0 = getBasicChange("2", "1"); + + private static final String rbsChangeGZip = IntegrationHelper.rbsChangeGZip("2", "1", "H4sIAAAAAAAA/0zOwWrDMBAE0H+Zs75At9CGUhpySSiUYoIij1MTSwraFdQY/XtRU5ccd3jDzoLoAmGRz3JSisJA1GkRWGyejq/vWxhodsMw+uN84/7OizDDgN9+Kj172AVXzgL72RkIL4FRf69q4FPsRx1TbMGC4NR/Mb/kVG6t51M4j5G5Pdw/w6zgrq+cD5zoNeWGH5asK+p/4y/d7Hant+3HAQaRF6eEHdwkrF2tXf0JAAD//9JucZnyAAAA"); + private static final String rbsChangeZLib = IntegrationHelper.rbsChangeZlib("2", "1", "eJxMzsFqwzAQBNB/mbO+QLfQhlIackkolGKCIo9TE0sK2hXUGP17UVOXHHd4w86C6AJhkc9yUorCQNRpEVhsno6v71sYaHbDMPrjfOP+zosww4Dffio9e9gFV84C+9kZCC+BUX+vauBT7EcdU2zBguDUfzG/5FRuredTOI+RuT3cP8Os4K6vnA+c6DXlhh+WrCvqf+Mv3ex2p7ftxwEGkRenhB3cJKxdrV39CQAA//8FrVMM"); + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private SplitRoomDatabase mRoomDb; + private final AtomicInteger mSplitChangesHits = new AtomicInteger(0); + private AtomicReference mCustomSplitChangesResponse; + + @Before + public void setUp() throws Exception { + mRoomDb = DatabaseHelper.getTestDatabase(mContext); + mSplitChangesHits.set(0); + mCustomSplitChangesResponse = new AtomicReference<>(null); + mRoomDb.clearAllTables(); + } + + @Test + public void instantUpdateNotification() throws IOException, InterruptedException { + successfulInstantUpdateTest(rbsChange0, "\"name\":\"rbs_test\""); + } + + @Test + public void instantUpdateNotificationGZip() throws IOException, InterruptedException { + successfulInstantUpdateTest(rbsChangeGZip, "\"name\":\"rbs_test\""); + } + + @Test + public void instantUpdateNotificationZLib() throws IOException, InterruptedException { + successfulInstantUpdateTest(rbsChangeZLib, "\"name\":\"rbs_test\""); + } + + @Test + public void referencedRuleBasedSegmentNotPresentTriggersFetch() throws IOException, InterruptedException { + // {"name":"rbs_test","status":"ACTIVE","trafficTypeName":"user","excluded":{"keys":[],"segments":[]},"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user"},"matcherType":"IN_RULE_BASED_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_rbs_test"}}]}}]} + String data = "eyJuYW1lIjoicmJzX3Rlc3QiLCJzdGF0dXMiOiJBQ1RJVkUiLCJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiZXhjbHVkZWQiOnsia2V5cyI6W10sInNlZ21lbnRzIjpbXX0sImNvbmRpdGlvbnMiOlt7ImNvbmRpdGlvblR5cGUiOiJST0xMT1VUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7ImtleVNlbGVjdG9yIjp7InRyYWZmaWNUeXBlIjoidXNlciJ9LCJtYXRjaGVyVHlwZSI6IklOX1JVTEVfQkFTRURfU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibmV3X3Jic190ZXN0In19XX19XX0="; + mCustomSplitChangesResponse.set(IntegrationHelper.splitChangeWithReferencedRbs(3, 3)); + referencedRbsTest(IntegrationHelper.rbsChange("4", "4", data)); + } + + @Test + public void referencedRuleBasedSegmentInFlagNotPresentTriggersFetch() throws IOException, InterruptedException { + // {"trafficTypeName":"client","name":"workm","trafficAllocation":100,"trafficAllocationSeed":147392224,"seed":524417105,"status":"ACTIVE","killed":false,"defaultTreatment":"on","changeNumber":4,"algo":2,"configurations":{},"sets":["set_3"],"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user"},"matcherType":"IN_RULE_BASED_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"new_rbs_test"}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in rule based segment new_rbs_test"}]} + String data = "eyJ0cmFmZmljVHlwZU5hbWUiOiJjbGllbnQiLCJuYW1lIjoid29ya20iLCJ0cmFmZmljQWxsb2NhdGlvbiI6MTAwLCJ0cmFmZmljQWxsb2NhdGlvblNlZWQiOjE0NzM5MjIyNCwic2VlZCI6NTI0NDE3MTA1LCJzdGF0dXMiOiJBQ1RJVkUiLCJraWxsZWQiOmZhbHNlLCJkZWZhdWx0VHJlYXRtZW50Ijoib24iLCJjaGFuZ2VOdW1iZXIiOjQsImFsZ28iOjIsImNvbmZpZ3VyYXRpb25zIjp7fSwic2V0cyI6WyJzZXRfMyJdLCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJJTl9SVUxFX0JBU0VEX1NFR01FTlQiLCJuZWdhdGUiOmZhbHNlLCJ1c2VyRGVmaW5lZFNlZ21lbnRNYXRjaGVyRGF0YSI6eyJzZWdtZW50TmFtZSI6Im5ld19yYnNfdGVzdCJ9fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvbiIsInNpemUiOjEwMH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjowfV0sImxhYmVsIjoiaW4gcnVsZSBiYXNlZCBzZWdtZW50IG5ld19yYnNfdGVzdCJ9XX0="; + String change = splitChangeV2("2", "1", "0", data); + referencedRbsTest(change); + } + + @Test + public void evaluation() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + mCustomSplitChangesResponse.set(IntegrationHelper.loadSplitChanges(mContext, "split_changes_rbs.json")); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, IntegrationHelper.dummyUserKey()); + if (readyClient == null) { + fail("Client not ready"); + } + + assertEquals("on", readyClient.getTreatment("rbs_split", Map.of("email", "test@split.io"))); + } + + @Test + public void evaluationWithExcludedSegment() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + MySegmentEntity mySegment = new MySegmentEntity(); + mySegment.setSegmentList("segment_test"); + mySegment.setUserKey(IntegrationHelper.dummyUserKey().matchingKey()); + mRoomDb.mySegmentDao().update(mySegment); + mCustomSplitChangesResponse.set(IntegrationHelper.loadSplitChanges(mContext, "split_changes_rbs.json")); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, IntegrationHelper.dummyUserKey()); + if (readyClient == null) { + fail("Client not ready"); + } + + assertEquals("off", readyClient.getTreatment("rbs_split", Map.of("email", "test@split.io"))); + } + + @Test + public void evaluationWithExcludedKey() throws IOException, InterruptedException { + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + String matchingKey = "key22"; + mCustomSplitChangesResponse.set(IntegrationHelper.loadSplitChanges(mContext, "split_changes_rbs.json")); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, new Key(matchingKey)); + if (readyClient == null) { + fail("Client not ready"); + } + + assertEquals("off", readyClient.getTreatment("rbs_split", Map.of("email", "test@split.io"))); + } + + private void referencedRbsTest(String notification) throws IOException, InterruptedException { + // Initialize a factory with RBS enabled + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, IntegrationHelper.dummyUserKey()); + if (readyClient == null) { + fail("Client not ready"); + } + + // Wait for the first change to be processed + Thread.sleep(200); + + CountDownLatch updateLatch = new CountDownLatch(1); + readyClient.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + Thread.sleep(100); + pushToStreaming(streamingData, notification); + mCustomSplitChangesResponse.set(IntegrationHelper.splitChangeWithReferencedRbs(5, 5)); + boolean updateAwaited = updateLatch.await(10, TimeUnit.SECONDS); + if (!updateAwaited) { + fail("SDK_UPDATE not received"); + } + + Thread.sleep(200); + List entities = mRoomDb.ruleBasedSegmentDao().getAll(); + List names = entities.stream().map(RuleBasedSegmentEntity::getName).collect(Collectors.toList()); + assertEquals(2, names.size()); + assertTrue(names.contains("rbs_test") && names.contains("new_rbs_test")); + } + + private void successfulInstantUpdateTest(String rbsChange0, String expectedContents) throws IOException, InterruptedException { + // Initialize a factory with RBS enabled + LinkedBlockingDeque streamingData = new LinkedBlockingDeque<>(); + SplitClient readyClient = getReadyClient(mContext, mRoomDb, streamingData, IntegrationHelper.dummyUserKey()); + if (readyClient == null) { + fail("Client not ready"); + } + + // Wait for the first change to be processed + Thread.sleep(200); + + // Push a split update through the streaming connection + boolean updateProcessed = processUpdate(readyClient, streamingData, rbsChange0, expectedContents); + + assertTrue(updateProcessed); + } + + @Nullable + private SplitClient getReadyClient( + Context context, + SplitRoomDatabase splitRoomDatabase, + BlockingQueue streamingData, Key key) throws IOException, InterruptedException { + SplitClientConfig config = new TestableSplitConfigBuilder() + .trafficType("user") + .streamingEnabled(true) + .enableDebug() + .build(); + CountDownLatch authLatch = new CountDownLatch(1); + Map responses = getStringResponseClosureMap(authLatch); + + HttpResponseMockDispatcher httpResponseMockDispatcher = IntegrationHelper.buildDispatcher(responses, streamingData); + + SplitFactory splitFactory = IntegrationHelper.buildFactory( + IntegrationHelper.dummyApiKey(), + key, + config, + context, + new HttpClientMock(httpResponseMockDispatcher), + splitRoomDatabase, null, null, null); + + CountDownLatch readyLatch = new CountDownLatch(1); + SplitClient client = splitFactory.client(); + client.on(SplitEvent.SDK_READY, new SplitEventTask() { + @Override + public void onPostExecutionView(SplitClient client) { + readyLatch.countDown(); + } + }); + + boolean await = readyLatch.await(5, TimeUnit.SECONDS); + boolean authAwait = authLatch.await(5, TimeUnit.SECONDS); + TestingHelper.pushKeepAlive(streamingData); + + return (await && authAwait) ? client : null; + } + + @NonNull + private Map getStringResponseClosureMap(CountDownLatch authLatch) { + Map responses = new HashMap<>(); + responses.put(IntegrationHelper.ServicePath.SPLIT_CHANGES, (uri, httpMethod, body) -> { + if (mCustomSplitChangesResponse.get() != null) { + return new HttpResponseMock(200, mCustomSplitChangesResponse.get()); + } + + return new HttpResponseMock(200, IntegrationHelper.emptyTargetingRulesChanges(1, 1)); + }); + responses.put(IntegrationHelper.ServicePath.MEMBERSHIPS + "/" + "/CUSTOMER_ID", (uri, httpMethod, body) -> new HttpResponseMock(200, IntegrationHelper.emptyAllSegments())); + responses.put("v2/auth", (uri, httpMethod, body) -> { + authLatch.countDown(); + return new HttpResponseMock(200, IntegrationHelper.streamingEnabledToken()); + }); + return responses; + } + + private boolean processUpdate(SplitClient client, LinkedBlockingDeque streamingData, String change, String... expectedContents) throws InterruptedException { + CountDownLatch updateLatch = new CountDownLatch(1); + client.on(SplitEvent.SDK_UPDATE, TestingHelper.testTask(updateLatch)); + pushToStreaming(streamingData, change); + boolean updateAwaited = updateLatch.await(10, TimeUnit.SECONDS); + List entities = mRoomDb.ruleBasedSegmentDao().getAll(); + if (!updateAwaited) { + fail("SDK_UPDATE not received"); + } + + if (expectedContents == null || expectedContents.length == 0) { + return !entities.isEmpty(); + } + + boolean contentMatches = true; + for (String expected : expectedContents) { + contentMatches = contentMatches && entities.size() == 1 && entities.get(0).getBody().contains(expected); + } + + return contentMatches; + } + + private static void pushToStreaming(LinkedBlockingDeque streamingData, String message) { + try { + streamingData.put(message + "" + "\n"); + Logger.d("Pushed message: " + message); + } catch (InterruptedException ignored) { + } + } + + @NonNull + private static String getBasicChange(String changeNumber, String pcn) { + return rbsChange(changeNumber, pcn, "eyJuYW1lIjoicmJzX3Rlc3QiLCJzdGF0dXMiOiJBQ1RJVkUiLCJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiZXhjbHVkZWQiOnsia2V5cyI6W10sInNlZ21lbnRzIjpbXX0sImNvbmRpdGlvbnMiOlt7Im1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX19XX0="); + } +} diff --git a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java index 81cbc29de..ce8df8024 100644 --- a/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java +++ b/src/androidTest/java/tests/integration/rollout/RolloutCacheManagerIntegrationTest.java @@ -87,7 +87,7 @@ public void repeatedInitWithClearOnInitSetToTrueDoesNotClearIfMinDaysHasNotElaps CountDownLatch readyLatch = new CountDownLatch(1); SplitFactory factory = getSplitFactory(RolloutCacheConfiguration.builder().clearOnInit(true).build()); - Thread.sleep(1000); + Thread.sleep(2000); // Track intermediate values List intermediateFlags = mRoomDb.splitDao().getAll(); @@ -103,12 +103,13 @@ public void repeatedInitWithClearOnInitSetToTrueDoesNotClearIfMinDaysHasNotElaps boolean readyAwait = readyLatch.await(10, TimeUnit.SECONDS); // Destroy factory + Thread.sleep(2000); factory.destroy(); mRequestCountdownLatch = new CountDownLatch(1); preloadDb(null, null, null); SplitFactory factory2 = getSplitFactory(RolloutCacheConfiguration.builder().clearOnInit(true).build()); - Thread.sleep(1000); + Thread.sleep(2000); // Track intermediate values List factory2Flags = mRoomDb.splitDao().getAll(); @@ -300,7 +301,7 @@ private List getSplitListFromJson() { FileHelper fileHelper = new FileHelper(); String s = fileHelper.loadFileContent(mContext, "attributes_test_split_change.json"); - SplitChange changes = Json.fromJson(s, SplitChange.class); + SplitChange changes = IntegrationHelper.getChangeFromJsonString(s); return changes.splits; } diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java index 5507e432b..387b7da80 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsEvaluationTest.java @@ -31,6 +31,7 @@ import io.split.android.client.SplitResult; import io.split.android.client.SyncConfig; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.Json; @@ -163,9 +164,9 @@ private SplitClient getClient( private String loadSplitChangeWithSet(int setsCount) { String change = mFileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + SplitChange parsedChange = IntegrationHelper.getChangeFromJsonString(change); parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange)); } } diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java index da3d18250..49a1ffc1e 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsMultipleFactoryTest.java @@ -35,6 +35,7 @@ import io.split.android.client.SplitFilter; import io.split.android.client.SyncConfig; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.utils.Json; import tests.integration.shared.TestingHelper; @@ -125,10 +126,10 @@ private List getNamesFromDb(String dbName) { private String loadSplitChangeWithSet(int setsCount) { String change = mFileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + SplitChange parsedChange = IntegrationHelper.getChangeFromJsonString(change); parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange)); } private HttpResponseMockDispatcher getDispatcher(int setsCount) { diff --git a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java index 06146259a..7cdf905c5 100644 --- a/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java +++ b/src/androidTest/java/tests/integration/sets/FlagSetsPollingTest.java @@ -34,6 +34,7 @@ import io.split.android.client.SyncConfig; import io.split.android.client.TestingConfig; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.storage.db.SplitEntity; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.utils.Json; @@ -171,7 +172,7 @@ public void queryStringIsBuiltCorrectlyWhenSetsAreConfigured() throws IOExceptio String uri = mSplitChangesUri; assertTrue(awaitFirst); - assertEquals("https://sdk.split.io/api/splitChanges?s=1.1&since=-1&sets=set_2,set_3,set_ww,set_x", uri); + assertEquals("https://sdk.split.io/api/splitChanges?s=1.1&since=-1&rbSince=-1&sets=set_2,set_3,set_ww,set_x", uri); } private SplitFactory createFactory( @@ -226,9 +227,9 @@ private SplitFactory createFactory( private String loadSplitChangeWithSet(int setsCount) { String change = fileHelper.loadFileContent(mContext, "split_changes_flag_set-" + setsCount + ".json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); + SplitChange parsedChange = IntegrationHelper.getChangeFromJsonString(change); parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return Json.toJson(TargetingRulesChange.create(parsedChange)); } } diff --git a/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java b/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java index 65167f866..c899a446c 100644 --- a/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java +++ b/src/androidTest/java/tests/integration/shared/InterdependentSplitsTest.java @@ -11,7 +11,10 @@ import helper.IntegrationHelper; import io.split.android.client.SplitClient; import io.split.android.client.api.Key; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; +import io.split.android.client.utils.Json; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.RecordedRequest; @@ -40,7 +43,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio String splitChange = "{\"splits\":[{\"trafficTypeName\":\"account\",\"name\":\"android_test_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1955610140,\"seed\":-633015570,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733409158,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SPLIT_TREATMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":{\"split\":\"android_test_3\",\"treatments\":[\"on\"]},\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in split android_test_3 treatment [on]\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}],\"since\":1648733409158,\"till\":1648733409158}"; return new MockResponse().setResponseCode(200) - .setBody(splitChange); + .setBody(Json.toJson(TargetingRulesChange.create(Json.fromJson(splitChange, SplitChange.class)))); } else if (request.getPath().contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else { diff --git a/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java b/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java index 7b5423f8d..9c55e1656 100644 --- a/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java +++ b/src/androidTest/java/tests/integration/shared/MySegmentsBeforeSplitsTest.java @@ -10,7 +10,10 @@ import helper.IntegrationHelper; import io.split.android.client.SplitClient; import io.split.android.client.api.Key; +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; +import io.split.android.client.utils.Json; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.RecordedRequest; @@ -34,7 +37,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio String splitChange = "{\"splits\":[{\"trafficTypeName\":\"account\",\"name\":\"android_test_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1955610140,\"seed\":-633015570,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733409158,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SPLIT_TREATMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":{\"split\":\"android_test_3\",\"treatments\":[\"on\"]},\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in split android_test_3 treatment [on]\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]},{\"trafficTypeName\":\"account\",\"name\":\"android_test_3\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-397942789,\"seed\":1852089605,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733496087,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"android_test\"},\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in segment android_test\"}]}],\"since\":1648733409158,\"till\":1648733409158}"; new CountDownLatch(1).await(500, TimeUnit.MILLISECONDS); return new MockResponse().setResponseCode(200) - .setBody(splitChange); + .setBody(Json.toJson(TargetingRulesChange.create(Json.fromJson(splitChange, SplitChange.class)))); } else if (request.getPath().contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else { diff --git a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java index a12642bee..17de1cfb3 100644 --- a/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java +++ b/src/androidTest/java/tests/integration/shared/SharedClientsIntegrationTest.java @@ -261,7 +261,7 @@ private void loadSplitChanges() { } private void insertSplitsIntoDb() { - SplitChange change = Json.fromJson(mJsonChanges.get(0), SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString(mJsonChanges.get(0)); List entities = new ArrayList<>(); for (Split split : change.splits) { String splitName = split.name; @@ -271,6 +271,11 @@ private void insertSplitsIntoDb() { entities.add(entity); } mRoomDb.splitDao().insert(entities); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } private static class ServerMock { diff --git a/src/androidTest/java/tests/integration/streaming/ControlTest.java b/src/androidTest/java/tests/integration/streaming/ControlTest.java index a2eb0cb8e..9be4dd28f 100644 --- a/src/androidTest/java/tests/integration/streaming/ControlTest.java +++ b/src/androidTest/java/tests/integration/streaming/ControlTest.java @@ -35,6 +35,7 @@ import io.split.android.client.SplitFactory; import io.split.android.client.api.Key; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.MySegmentEntity; @@ -273,10 +274,10 @@ private String loadMockedData(String fileName) { private String loadSplitChanges() { String jsonChange = new FileHelper().loadFileContent(mContext, "simple_split.json"); - SplitChange change = Json.fromJson(jsonChange, SplitChange.class); + SplitChange change = Json.fromJson(jsonChange, TargetingRulesChange.class).getFeatureFlagsChange(); change.since = 500; change.till = 500; - return Json.toJson(change); + return Json.toJson(TargetingRulesChange.create(change)); } private void pushMySegmentMessage() { diff --git a/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java b/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java index d816ee747..ad01c035b 100644 --- a/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java +++ b/src/androidTest/java/tests/integration/streaming/ImpressionsCountTest.java @@ -37,6 +37,7 @@ import io.split.android.client.dtos.KeyImpression; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; @@ -228,14 +229,14 @@ public HttpStreamResponseMock getStreamResponse(URI uri) { } private void loadSplitChanges() { - mSplitChange = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + mSplitChange = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); - SplitChange c1 = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + SplitChange c1 = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); - SplitChange c2 = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + SplitChange c2 = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); mSplitChange.splits.get(0).name = "SPLIT_1"; Split split = c1.splits.get(0); @@ -249,7 +250,7 @@ private void loadSplitChanges() { private String getSplitChanges() { mSplitChange.splits.get(0).changeNumber = CHANGE_NUMBER; mSplitChange.till = CHANGE_NUMBER; - return Json.toJson(mSplitChange); + return Json.toJson(TargetingRulesChange.create(mSplitChange)); } private String loadMockedData(String fileName) { diff --git a/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java b/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java index 07ac91545..6985d990c 100644 --- a/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java +++ b/src/androidTest/java/tests/integration/streaming/SdkUpdateStreamingTest.java @@ -35,6 +35,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.GeneralInfoEntity; @@ -407,8 +408,8 @@ private void pushInitialId() { } private String getChanges(String treatment, long since, long till) { - SplitChange change = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); change.since = since; change.till = till; Split split = change.splits.get(0); @@ -420,7 +421,7 @@ private String getChanges(String treatment, long since, long till) { partition.size = 0; } } - return Json.toJson(change); + return Json.toJson(TargetingRulesChange.create(change)); } private void loadChanges() { diff --git a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java index 45b0369d5..86da7e83e 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsKillProcessTest.java @@ -35,6 +35,7 @@ import io.split.android.client.api.Key; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.GeneralInfoEntity; @@ -183,7 +184,7 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { mSplitsUpdateLatch.countDown(); return createResponse(200, getSplitChanges(mSplitChangesHitCount)); } - String data = IntegrationHelper.emptySplitChanges(-1, CHANGE_NUMBER - 1000); + String data = IntegrationHelper.emptyTargetingRulesChanges(CHANGE_NUMBER - 1000, -1); return createResponse(200, data); } else if (uri.getPath().contains("/auth")) { Logger.i("** SSE Auth hit"); @@ -221,13 +222,13 @@ private void pushMessage(String fileName) { } private void loadSplitChanges() { - SplitChange change = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); Split split = change.splits.get(0); split.name = "test_feature_1"; - mSplitChange = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + mSplitChange = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); mSplitChange.splits.add(split); } @@ -244,7 +245,7 @@ private String getSplitChanges(int hit) { split.defaultTreatment = "off"; mSplitChange.since = CHANGE_NUMBER; mSplitChange.till = CHANGE_NUMBER; - return Json.toJson(mSplitChange); + return Json.toJson(TargetingRulesChange.create(mSplitChange)); } private Split parseEntity(SplitEntity entity) { diff --git a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java index 3675b4612..b662b14e2 100644 --- a/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java +++ b/src/androidTest/java/tests/integration/streaming/SplitsSyncProcessTest.java @@ -34,6 +34,7 @@ import io.split.android.client.api.Key; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.GeneralInfoEntity; @@ -173,7 +174,7 @@ public HttpResponseMock getResponse(URI uri, HttpMethod method, String body) { mSplitsUpdateLatch.countDown(); return createResponse(200, getSplitChanges(mSplitChangesHitCount - 1)); } - String data = IntegrationHelper.emptySplitChanges(CHANGE_NUMBER - 1000, CHANGE_NUMBER - 1000); + String data = IntegrationHelper.emptyTargetingRulesChanges(CHANGE_NUMBER - 1000, -1); return createResponse(200, data); } else if (uri.getPath().contains("/auth")) { Logger.i("** SSE Auth hit"); @@ -214,15 +215,15 @@ private void pushMessage(String fileName) { } private void loadSplitChanges() { - mSplitChange = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + mSplitChange = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); } private String getSplitChanges(int hit) { mSplitChange.splits.get(0).changeNumber = CHANGE_NUMBER; mSplitChange.since = CHANGE_NUMBER; mSplitChange.till = CHANGE_NUMBER; - return Json.toJson(mSplitChange); + return Json.toJson(TargetingRulesChange.create(mSplitChange)); } private Split parseEntity(SplitEntity entity) { diff --git a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java index 8f9e2490a..7aea63f99 100644 --- a/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java +++ b/src/androidTest/java/tests/integration/telemetry/TelemetryIntegrationTest.java @@ -119,7 +119,7 @@ public MockResponse dispatch(RecordedRequest request) { } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody(IntegrationHelper.emptySplitChanges(changeNumber + 1000)); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("metrics/usage")) { @@ -219,7 +219,7 @@ public MockResponse dispatch(RecordedRequest request) { } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody(IntegrationHelper.emptySplitChanges(changeNumber + 1000)); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("metrics/usage")) { @@ -272,7 +272,7 @@ public MockResponse dispatch(RecordedRequest request) { } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody(IntegrationHelper.emptySplitChanges(changeNumber + 1000)); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("metrics/usage")) { @@ -375,7 +375,7 @@ private List getSplitListFromJson() { FileHelper fileHelper = new FileHelper(); String s = fileHelper.loadFileContent(mContext, "splitchanges_int_test.json"); - SplitChange changes = Json.fromJson(s, SplitChange.class); + SplitChange changes = IntegrationHelper.getChangeFromJsonString(s); return changes.splits; } @@ -393,7 +393,7 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio } else if (path.contains("/splitChanges")) { long changeNumber = -1; return new MockResponse().setResponseCode(200) - .setBody("{\"splits\":[], \"since\":" + (changeNumber + 1000) + ", \"till\":" + (changeNumber + 1000) + "}"); + .setBody(IntegrationHelper.emptySplitChanges(changeNumber + 1000)); } else if (path.contains("/events/bulk")) { return new MockResponse().setResponseCode(200); } else if (path.contains("/metrics")) { diff --git a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java index 0cb14611d..be477b42a 100644 --- a/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java +++ b/src/androidTest/java/tests/integration/toggle/ImpressionsToggleTest.java @@ -278,10 +278,6 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio } private String loadSplitChanges() { - FileHelper fileHelper = new FileHelper(); - String change = fileHelper.loadFileContent(mContext, "split_changes_imp_toggle.json"); - SplitChange parsedChange = Json.fromJson(change, SplitChange.class); - parsedChange.since = parsedChange.till; - return Json.toJson(parsedChange); + return IntegrationHelper.loadSplitChanges(mContext, "split_changes_imp_toggle.json"); } } diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt index 16b224947..e40b4ab39 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeDebugTest.kt @@ -7,6 +7,7 @@ import helper.* import io.split.android.client.SplitClient import io.split.android.client.SplitFactory import io.split.android.client.dtos.SplitChange +import io.split.android.client.dtos.TargetingRulesChange import io.split.android.client.events.SplitEvent import io.split.android.client.events.SplitEventTask import io.split.android.client.network.HttpMethod @@ -234,7 +235,7 @@ class UserConsentModeDebugTest { mChangeHit+=1 return getSplitsMockResponse("") } - return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999, 99999999)) + return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999)) } else if (uri.path.contains("/testImpressions/bulk")) { if (!mImpPosted) { mImpPosted = true @@ -267,8 +268,8 @@ class UserConsentModeDebugTest { private fun loadSplitChanges(): String? { val fileHelper = FileHelper() val change = fileHelper.loadFileContent(mContext, "split_changes_1.json") - val parsedChange = Json.fromJson(change, SplitChange::class.java) + val parsedChange = Json.fromJson(change, TargetingRulesChange::class.java).featureFlagsChange parsedChange.since = parsedChange.till - return Json.toJson(parsedChange) + return Json.toJson(TargetingRulesChange.create(parsedChange)) } } diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt index fd8e7b4cc..ca04b93b4 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeNoneTest.kt @@ -7,6 +7,7 @@ import helper.* import io.split.android.client.SplitClient import io.split.android.client.SplitFactory import io.split.android.client.dtos.SplitChange +import io.split.android.client.dtos.TargetingRulesChange import io.split.android.client.events.SplitEvent import io.split.android.client.events.SplitEventTask import io.split.android.client.network.HttpMethod @@ -240,7 +241,7 @@ class UserConsentModeNoneTest { } return HttpResponseMock( 200, - IntegrationHelper.emptySplitChanges(99999999, 99999999) + IntegrationHelper.emptySplitChanges(99999999) ) } else if (uri.path.contains("/testImpressions/bulk")) { HttpResponseMock(200) @@ -274,8 +275,8 @@ class UserConsentModeNoneTest { private fun loadSplitChanges(): String? { val fileHelper = FileHelper() val change = fileHelper.loadFileContent(mContext, "split_changes_1.json") - val parsedChange = Json.fromJson(change, SplitChange::class.java) + val parsedChange = Json.fromJson(change, TargetingRulesChange::class.java).featureFlagsChange parsedChange.since = parsedChange.till - return Json.toJson(parsedChange) + return Json.toJson(TargetingRulesChange.create(parsedChange)) } } diff --git a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt index ea7580626..904694c7e 100644 --- a/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt +++ b/src/androidTest/java/tests/integration/userconsent/UserConsentModeOptimizedTest.kt @@ -7,6 +7,7 @@ import helper.* import io.split.android.client.SplitClient import io.split.android.client.SplitFactory import io.split.android.client.dtos.SplitChange +import io.split.android.client.dtos.TargetingRulesChange import io.split.android.client.events.SplitEvent import io.split.android.client.events.SplitEventTask import io.split.android.client.network.HttpMethod @@ -246,7 +247,7 @@ class UserConsentModeOptimizedTest { mChangeHit+=1 return getSplitsMockResponse("") } - return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999, 99999999)) + return HttpResponseMock(200, IntegrationHelper.emptySplitChanges(99999999)) } else if (uri.path.contains("/testImpressions/bulk")) { if (!mImpPosted) { mImpPosted = true @@ -279,8 +280,8 @@ class UserConsentModeOptimizedTest { private fun loadSplitChanges(): String? { val fileHelper = FileHelper() val change = fileHelper.loadFileContent(mContext, "split_changes_1.json") - val parsedChange = Json.fromJson(change, SplitChange::class.java) + val parsedChange = Json.fromJson(change, TargetingRulesChange::class.java).featureFlagsChange parsedChange.since = parsedChange.till - return Json.toJson(parsedChange) + return Json.toJson(TargetingRulesChange.create(parsedChange)) } } diff --git a/src/androidTest/java/tests/service/SdkUpdatePollingTest.java b/src/androidTest/java/tests/service/SdkUpdatePollingTest.java index ee20e6bd6..e04afdbe4 100644 --- a/src/androidTest/java/tests/service/SdkUpdatePollingTest.java +++ b/src/androidTest/java/tests/service/SdkUpdatePollingTest.java @@ -36,6 +36,7 @@ import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEvent; import io.split.android.client.network.HttpMethod; import io.split.android.client.storage.db.GeneralInfoEntity; @@ -347,8 +348,8 @@ private void pushInitialId() { } private String getChanges(String treatment, long since, long till) { - SplitChange change = Json.fromJson( - loadMockedData("splitchanges_int_test.json"), SplitChange.class); + SplitChange change = IntegrationHelper.getChangeFromJsonString( + loadMockedData("splitchanges_int_test.json")); change.since = since; change.till = till; Split split = change.splits.get(0); @@ -360,7 +361,7 @@ private String getChanges(String treatment, long since, long till) { partition.size = 0; } } - return Json.toJson(change); + return Json.toJson(TargetingRulesChange.create(change)); } private void loadChanges() { diff --git a/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java b/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java index 14a713f0d..91361df4c 100644 --- a/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java +++ b/src/androidTest/java/tests/storage/GeneralInfoStorageTest.java @@ -41,10 +41,10 @@ public void setSplitsUpdateTimestamp() { } @Test - public void setChangeNumber() { - long initialValue = mGeneralInfoStorage.getChangeNumber(); - mGeneralInfoStorage.setChangeNumber(100L); - long finalValue = mGeneralInfoStorage.getChangeNumber(); + public void setFlagsChangeNumber() { + long initialValue = mGeneralInfoStorage.getFlagsChangeNumber(); + mGeneralInfoStorage.setFlagsChangeNumber(100L); + long finalValue = mGeneralInfoStorage.getFlagsChangeNumber(); assertEquals(-1L, initialValue); assertEquals(100L, finalValue); @@ -89,4 +89,14 @@ public void setRolloutCacheLastClearTimestamp() { assertEquals(0L, initialValue); assertEquals(100L, finalValue); } + + @Test + public void setRbsChangeNumber() { + long initialValue = mGeneralInfoStorage.getRbsChangeNumber(); + mGeneralInfoStorage.setRbsChangeNumber(100L); + long finalValue = mGeneralInfoStorage.getRbsChangeNumber(); + + assertEquals(-1L, initialValue); + assertEquals(100L, finalValue); + } } diff --git a/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt b/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt index 25364dae2..072ad7365 100644 --- a/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt +++ b/src/androidTest/java/tests/storage/cipher/CBCCipherTest.kt @@ -7,6 +7,7 @@ import helper.MOCK_DATA_SPLIT import io.split.android.client.storage.cipher.CBCCipher import junit.framework.TestCase.assertEquals import org.junit.Before +import org.junit.Ignore import org.junit.Test import kotlin.test.DefaultAsserter.assertNotEquals @@ -37,7 +38,7 @@ class CBCCipherTest { @Test fun testLongTextEncryption() { StringBuilder().apply { - for (i in 0..1000) { + for (i in 0..100) { append(MOCK_DATA_LONG_TEXT) } }.run { diff --git a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java b/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java index bf593bbf9..a98e6a0e2 100644 --- a/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java +++ b/src/androidTest/java/tests/workmanager/WorkManagerWrapperTest.java @@ -128,7 +128,7 @@ public void scheduleWorkSchedulesSplitsJob() { .putBoolean("shouldRecordTelemetry", true) .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) .putString("configuredFilterType", SplitFilter.Type.BY_SET.queryStringField()) - .putString("flagsSpec", "1.2") + .putString("flagsSpec", "1.3") .putString("certificatePins", certificatePinsJson()) .build(); @@ -255,7 +255,7 @@ public void schedulingWithoutCertificatePinning() { .putBoolean("shouldRecordTelemetry", true) .putStringArray("configuredFilterValues", new String[]{"set_1", "set_2"}) .putString("configuredFilterType", SplitFilter.Type.BY_SET.queryStringField()) - .putString("flagsSpec", "1.2") + .putString("flagsSpec", "1.3") .build(); PeriodicWorkRequest expectedRequest = new PeriodicWorkRequest diff --git a/src/main/java/io/split/android/client/SplitClientConfig.java b/src/main/java/io/split/android/client/SplitClientConfig.java index d9f55df7c..00af53115 100644 --- a/src/main/java/io/split/android/client/SplitClientConfig.java +++ b/src/main/java/io/split/android/client/SplitClientConfig.java @@ -1118,7 +1118,7 @@ public Builder impressionsDedupeTimeInterval(long impressionsDedupeTimeInterval) * @param rolloutCacheConfiguration Configuration object * @return This builder */ - Builder rolloutCacheConfiguration(@NonNull RolloutCacheConfiguration rolloutCacheConfiguration) { + public Builder rolloutCacheConfiguration(@NonNull RolloutCacheConfiguration rolloutCacheConfiguration) { if (rolloutCacheConfiguration == null) { Logger.w("Rollout cache configuration is null. Setting to default value."); mRolloutCacheConfiguration = RolloutCacheConfiguration.builder().build(); diff --git a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java index 2e7109b08..d2ab82416 100644 --- a/src/main/java/io/split/android/client/SplitClientFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitClientFactoryImpl.java @@ -36,7 +36,6 @@ public class SplitClientFactoryImpl implements SplitClientFactory { private final SplitFactory mSplitFactory; private final SplitClientContainer mClientContainer; private final SplitClientConfig mConfig; - private final SyncManager mSyncManager; private final TelemetrySynchronizer mTelemetrySynchronizer; private final SplitStorageContainer mStorageContainer; @@ -58,11 +57,11 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, @NonNull KeyValidator keyValidator, @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTrackerProvider, @NonNull ImpressionListener.FederatedImpressionListener customerImpressionListener, - @Nullable FlagSetsFilter flagSetsFilter) { + @Nullable FlagSetsFilter flagSetsFilter, + @NonNull SplitParser splitParser) { mSplitFactory = checkNotNull(splitFactory); mClientContainer = checkNotNull(clientContainer); mConfig = checkNotNull(config); - mSyncManager = checkNotNull(syncManager); mStorageContainer = checkNotNull(storageContainer); mTelemetrySynchronizer = checkNotNull(telemetrySynchronizer); @@ -73,7 +72,7 @@ public SplitClientFactoryImpl(@NonNull SplitFactory splitFactory, validationLogger, splitTaskExecutor, mStorageContainer.getPersistentAttributesStorage()); - mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); + mSplitParser = splitParser; mSplitValidator = new SplitValidatorImpl(); SplitsStorage splitsStorage = mStorageContainer.getSplitsStorage(); mTreatmentManagerFactory = new TreatmentManagerFactoryImpl( diff --git a/src/main/java/io/split/android/client/SplitFactoryHelper.java b/src/main/java/io/split/android/client/SplitFactoryHelper.java index 770cc3249..2c1c33d95 100644 --- a/src/main/java/io/split/android/client/SplitFactoryHelper.java +++ b/src/main/java/io/split/android/client/SplitFactoryHelper.java @@ -45,6 +45,7 @@ import io.split.android.client.service.sseclient.ReconnectBackoffCounter; import io.split.android.client.service.sseclient.SseJwtParser; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; +import io.split.android.client.service.sseclient.notifications.InstantUpdateChangeNotification; import io.split.android.client.service.sseclient.notifications.MySegmentsV2PayloadDecoder; import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationProcessor; @@ -84,7 +85,9 @@ import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.db.StorageFactory; import io.split.android.client.storage.events.PersistentEventsStorage; +import io.split.android.client.storage.general.GeneralInfoStorage; import io.split.android.client.storage.impressions.PersistentImpressionsStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.telemetry.TelemetrySynchronizerImpl; @@ -169,6 +172,7 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, StorageFactory.getPersistentEventsStorage(splitRoomDatabase, splitCipher); PersistentImpressionsStorage persistentImpressionsStorage = StorageFactory.getPersistentImpressionsStorage(splitRoomDatabase, splitCipher); + GeneralInfoStorage generalInfoStorage = StorageFactory.getGeneralInfoStorage(splitRoomDatabase); return new SplitStorageContainer( splitsStorage, StorageFactory.getMySegmentsStorage(splitRoomDatabase, splitCipher), @@ -184,7 +188,8 @@ SplitStorageContainer buildStorageContainer(UserConsent userConsentStatus, StorageFactory.getPersistentAttributesStorage(splitRoomDatabase, splitCipher), getTelemetryStorage(shouldRecordTelemetry, telemetryStorage), StorageFactory.getImpressionsObserverCachePersistentStorage(splitRoomDatabase, observerCacheExpirationPeriod, impressionsObserverExecutor), - StorageFactory.getGeneralInfoStorage(splitRoomDatabase)); + generalInfoStorage, + StorageFactory.getPersistentRuleBasedSegmentStorage(splitRoomDatabase, splitCipher, generalInfoStorage)); } SplitApiFacade buildApiFacade(SplitClientConfig splitClientConfig, @@ -350,7 +355,7 @@ public StreamingComponents buildStreamingComponents(@NonNull SplitTaskExecutor s return new StreamingComponents(); } - BlockingQueue splitsUpdateNotificationQueue = new LinkedBlockingDeque<>(); + BlockingQueue splitsUpdateNotificationQueue = new LinkedBlockingDeque<>(); NotificationParser notificationParser = new NotificationParser(); NotificationProcessor notificationProcessor = new NotificationProcessor(splitTaskExecutor, splitTaskFactory, @@ -416,13 +421,15 @@ SplitUpdatesWorker getSplitUpdatesWorker(SplitClientConfig config, SplitTaskExecutor splitTaskExecutor, SplitTaskFactory splitTaskFactory, Synchronizer mSynchronizer, - BlockingQueue splitsUpdateNotificationQueue, + BlockingQueue splitsUpdateNotificationQueue, SplitsStorage splitsStorage, + RuleBasedSegmentStorage ruleBasedSegmentStorage, CompressionUtilProvider compressionProvider) { if (config.syncEnabled()) { return new SplitUpdatesWorker(mSynchronizer, splitsUpdateNotificationQueue, splitsStorage, + ruleBasedSegmentStorage, compressionProvider, splitTaskExecutor, splitTaskFactory); diff --git a/src/main/java/io/split/android/client/SplitFactoryImpl.java b/src/main/java/io/split/android/client/SplitFactoryImpl.java index d2630edbc..4aea0bb13 100644 --- a/src/main/java/io/split/android/client/SplitFactoryImpl.java +++ b/src/main/java/io/split/android/client/SplitFactoryImpl.java @@ -254,6 +254,7 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp mSynchronizer, streamingComponents.getSplitsUpdateNotificationQueue(), mStorageContainer.getSplitsStorage(), + mStorageContainer.getRuleBasedSegmentStorage(), compressionProvider), streamingComponents.getSyncGuardian()); @@ -284,12 +285,15 @@ private SplitFactoryImpl(@NonNull String apiToken, @NonNull Key key, @NonNull Sp streamingComponents.getNotificationProcessor(), streamingComponents.getSseAuthenticator(), mStorageContainer, mSyncManager, compressionProvider); + SplitParser splitParser = new SplitParser(mStorageContainer.getParserCommons()); + mClientContainer = new SplitClientContainerImpl( mDefaultClientKey.matchingKey(), this, config, mSyncManager, telemetrySynchronizer, mStorageContainer, mSplitTaskExecutor, splitApiFacade, validationLogger, keyValidator, customerImpressionListener, streamingComponents.getPushNotificationManager(), componentsRegister, workManagerWrapper, - mEventsTrackerProvider, flagSetsFilter); + mEventsTrackerProvider, flagSetsFilter, splitParser); + mDestroyer = new Runnable() { public void run() { @@ -370,10 +374,9 @@ public void run() { // Initialize default client client(); - SplitParser mSplitParser = new SplitParser(mStorageContainer.getMySegmentsStorageContainer(), mStorageContainer.getMyLargeSegmentsStorageContainer()); mManager = new SplitManagerImpl( mStorageContainer.getSplitsStorage(), - new SplitValidatorImpl(), mSplitParser); + new SplitValidatorImpl(), splitParser); } @NonNull diff --git a/src/main/java/io/split/android/client/dtos/Excluded.java b/src/main/java/io/split/android/client/dtos/Excluded.java new file mode 100644 index 000000000..caf436d12 --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/Excluded.java @@ -0,0 +1,30 @@ +package io.split.android.client.dtos; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashSet; +import java.util.Set; + +public class Excluded { + + @SerializedName("keys") + private Set mKeys; + + @SerializedName("segments") + private Set mSegments; + + public Set getSegments() { + return mSegments; + } + + public Set getKeys() { + return mKeys; + } + + public static Excluded createEmpty() { + Excluded excluded = new Excluded(); + excluded.mKeys = new HashSet<>(); + excluded.mSegments = new HashSet<>(); + return excluded; + } +} diff --git a/src/main/java/io/split/android/client/dtos/ExcludedSegment.java b/src/main/java/io/split/android/client/dtos/ExcludedSegment.java new file mode 100644 index 000000000..89c76f1e5 --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/ExcludedSegment.java @@ -0,0 +1,79 @@ +package io.split.android.client.dtos; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.google.gson.annotations.SerializedName; + +public class ExcludedSegment { + + private static final String TYPE_LARGE = "large"; + private static final String TYPE_STANDARD = "standard"; + private static final String TYPE_RULE_BASED = "rule-based"; + + @SerializedName("type") + private String mType; + + @SerializedName("name") + private String mName; + + public ExcludedSegment() {} + + private ExcludedSegment(String name, String type) { + mName = name; + mType = type; + } + + @VisibleForTesting + public static ExcludedSegment standard(String name) { + return new ExcludedSegment(name, TYPE_STANDARD); + } + + @VisibleForTesting + public static ExcludedSegment large(String name) { + return new ExcludedSegment(name, TYPE_LARGE); + } + + public static ExcludedSegment ruleBased(String name) { + return new ExcludedSegment(name, TYPE_RULE_BASED); + } + + public String getName() { + return mName; + } + + public boolean isStandard() { + return TYPE_STANDARD.equals(mType); + } + + public boolean isLarge() { + return TYPE_LARGE.equals(mType); + } + + public boolean isRuleBased() { + return TYPE_RULE_BASED.equals(mType); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof ExcludedSegment)) { + return false; + } + ExcludedSegment other = (ExcludedSegment) obj; + return mName.equals(other.mName) && mType.equals(other.mType); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + mName.hashCode(); + result = 31 * result + mType.hashCode(); + return result; + } +} diff --git a/src/main/java/io/split/android/client/dtos/Helper.java b/src/main/java/io/split/android/client/dtos/Helper.java new file mode 100644 index 000000000..a56ca82c2 --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/Helper.java @@ -0,0 +1,26 @@ +package io.split.android.client.dtos; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Helper { + + public static Set getReferencedRuleBasedSegments(List conditions) { + Set referencedSegmentNames = new HashSet<>(); + + if (conditions == null) { + return referencedSegmentNames; + } + + for (Condition condition : conditions) { + for (Matcher matcher : condition.matcherGroup.matchers) { + if (matcher.matcherType == MatcherType.IN_RULE_BASED_SEGMENT) { + referencedSegmentNames.add(matcher.userDefinedSegmentMatcherData.segmentName); + } + } + } + + return referencedSegmentNames; + } +} diff --git a/src/main/java/io/split/android/client/dtos/MatcherType.java b/src/main/java/io/split/android/client/dtos/MatcherType.java index 580e97f2e..75d5325b7 100644 --- a/src/main/java/io/split/android/client/dtos/MatcherType.java +++ b/src/main/java/io/split/android/client/dtos/MatcherType.java @@ -60,5 +60,8 @@ public enum MatcherType { @SerializedName("BETWEEN_SEMVER") BETWEEN_SEMVER, @SerializedName("IN_LIST_SEMVER") - IN_LIST_SEMVER + IN_LIST_SEMVER, + + @SerializedName("IN_RULE_BASED_SEGMENT") + IN_RULE_BASED_SEGMENT, } diff --git a/src/main/java/io/split/android/client/dtos/RuleBasedSegment.java b/src/main/java/io/split/android/client/dtos/RuleBasedSegment.java new file mode 100644 index 000000000..8fe887f64 --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/RuleBasedSegment.java @@ -0,0 +1,59 @@ +package io.split.android.client.dtos; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class RuleBasedSegment { + + @SerializedName("name") + private final String mName; + + @SerializedName("trafficTypeName") + private final String mTrafficTypeName; + + @SerializedName("changeNumber") + private final long mChangeNumber; + + @SerializedName("status") + private final Status mStatus; + + @SerializedName("conditions") + private final List mConditions; + + @SerializedName("excluded") + private final Excluded mExcluded; + + public RuleBasedSegment(String name, String trafficTypeName, long changeNumber, Status status, List conditions, Excluded excluded) { + mName = name; + mTrafficTypeName = trafficTypeName; + mChangeNumber = changeNumber; + mStatus = status; + mConditions = conditions; + mExcluded = excluded; + } + + public String getName() { + return mName; + } + + public String getTrafficTypeName() { + return mTrafficTypeName; + } + + public long getChangeNumber() { + return mChangeNumber; + } + + public Status getStatus() { + return mStatus; + } + + public List getConditions() { + return mConditions; + } + + public Excluded getExcluded() { + return mExcluded; + } +} diff --git a/src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java b/src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java new file mode 100644 index 000000000..7d01cd4bf --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/RuleBasedSegmentChange.java @@ -0,0 +1,46 @@ +package io.split.android.client.dtos; + +import androidx.annotation.VisibleForTesting; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; + +public class RuleBasedSegmentChange { + + @SerializedName("s") + private long since; + + @SerializedName("t") + private long till; + + @SerializedName("d") + private List segments; + + public long getSince() { + return since; + } + + public long getTill() { + return till; + } + + public List getSegments() { + return segments; + } + + @VisibleForTesting + public static RuleBasedSegmentChange createEmpty() { + return create(-1, -1, new ArrayList<>()); + } + + @VisibleForTesting + public static RuleBasedSegmentChange create(long since, long till, List segments) { + RuleBasedSegmentChange ruleBasedSegmentChange = new RuleBasedSegmentChange(); + ruleBasedSegmentChange.segments = segments; + ruleBasedSegmentChange.since = since; + ruleBasedSegmentChange.till = till; + return ruleBasedSegmentChange; + } +} diff --git a/src/main/java/io/split/android/client/dtos/SplitChange.java b/src/main/java/io/split/android/client/dtos/SplitChange.java index e9a64d3cd..2c7b7f05f 100644 --- a/src/main/java/io/split/android/client/dtos/SplitChange.java +++ b/src/main/java/io/split/android/client/dtos/SplitChange.java @@ -1,9 +1,25 @@ package io.split.android.client.dtos; +import androidx.annotation.VisibleForTesting; + +import com.google.gson.annotations.SerializedName; + import java.util.List; public class SplitChange { + @SerializedName(value = "d", alternate = "splits") public List splits; + @SerializedName(value = "s", alternate = "since") public long since; + @SerializedName(value = "t", alternate = "till") public long till; + + @VisibleForTesting + public static SplitChange create(long since, long till, List splits) { + SplitChange splitChange = new SplitChange(); + splitChange.since = since; + splitChange.till = till; + splitChange.splits = splits; + return splitChange; + } } diff --git a/src/main/java/io/split/android/client/dtos/TargetingRulesChange.java b/src/main/java/io/split/android/client/dtos/TargetingRulesChange.java new file mode 100644 index 000000000..23cfe911c --- /dev/null +++ b/src/main/java/io/split/android/client/dtos/TargetingRulesChange.java @@ -0,0 +1,30 @@ +package io.split.android.client.dtos; + +import com.google.gson.annotations.SerializedName; + +public class TargetingRulesChange { + @SerializedName("ff") + private SplitChange ff; + + @SerializedName("rbs") + private RuleBasedSegmentChange rbs; + + public SplitChange getFeatureFlagsChange() { + return ff; + } + + public RuleBasedSegmentChange getRuleBasedSegmentsChange() { + return rbs; + } + + public static TargetingRulesChange create(SplitChange splitChange) { + return create(splitChange, RuleBasedSegmentChange.createEmpty()); + } + + public static TargetingRulesChange create(SplitChange splitChange, RuleBasedSegmentChange ruleBasedSegmentChange) { + TargetingRulesChange targetingRulesChange = new TargetingRulesChange(); + targetingRulesChange.ff = splitChange; + targetingRulesChange.rbs = ruleBasedSegmentChange; + return targetingRulesChange; + } +} diff --git a/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java b/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java index f00f463b5..3d3af8bf3 100644 --- a/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java +++ b/src/main/java/io/split/android/client/events/EventsManagerCoordinator.java @@ -34,6 +34,7 @@ protected void triggerEventsWhenAreAvailable() { mTriggered.add(event); switch (event) { case SPLITS_UPDATED: + case RULE_BASED_SEGMENTS_UPDATED: case SPLITS_FETCHED: case SPLITS_LOADED_FROM_STORAGE: case SPLIT_KILLED_NOTIFICATION: diff --git a/src/main/java/io/split/android/client/events/SplitEventsManager.java b/src/main/java/io/split/android/client/events/SplitEventsManager.java index c443aa8e2..9fc54670b 100644 --- a/src/main/java/io/split/android/client/events/SplitEventsManager.java +++ b/src/main/java/io/split/android/client/events/SplitEventsManager.java @@ -135,6 +135,7 @@ protected void triggerEventsWhenAreAvailable() { case SPLITS_UPDATED: case MY_SEGMENTS_UPDATED: case MY_LARGE_SEGMENTS_UPDATED: + case RULE_BASED_SEGMENTS_UPDATED: if (isTriggered(SplitEvent.SDK_READY)) { trigger(SplitEvent.SDK_UPDATE); return; diff --git a/src/main/java/io/split/android/client/events/SplitInternalEvent.java b/src/main/java/io/split/android/client/events/SplitInternalEvent.java index eaa25767d..ab070f2f6 100644 --- a/src/main/java/io/split/android/client/events/SplitInternalEvent.java +++ b/src/main/java/io/split/android/client/events/SplitInternalEvent.java @@ -16,4 +16,5 @@ public enum SplitInternalEvent { ATTRIBUTES_LOADED_FROM_STORAGE, ENCRYPTION_MIGRATION_DONE, MY_LARGE_SEGMENTS_UPDATED, + RULE_BASED_SEGMENTS_UPDATED, } diff --git a/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java b/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java index 64304916b..a10e387d7 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostMySegmentsStorageContainer.java @@ -18,6 +18,11 @@ public long getUniqueAmount() { return mEmptyMySegmentsStorage.getAll().size(); } + @Override + public void loadLocal() { + // no-op + } + @Override public void clear() { // No-op diff --git a/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java b/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java new file mode 100644 index 000000000..137b1f6b0 --- /dev/null +++ b/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorage.java @@ -0,0 +1,44 @@ +package io.split.android.client.localhost; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.engine.experiments.ParsedRuleBasedSegment; + +public class LocalhostRuleBasedSegmentsStorage implements RuleBasedSegmentStorage { + + @Nullable + @Override + public ParsedRuleBasedSegment get(String segmentName, String matchingKey) { + return null; + } + + @Override + public boolean update(@NonNull Set toAdd, @NonNull Set toRemove, long changeNumber) { + return false; + } + + @Override + public long getChangeNumber() { + return -1; + } + + @Override + public boolean contains(@NonNull Set segmentNames) { + return false; + } + + @Override + public void loadLocal() { + // no-op + } + + @Override + public void clear() { + // no-op + } +} diff --git a/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java b/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java new file mode 100644 index 000000000..9c4ae9e77 --- /dev/null +++ b/src/main/java/io/split/android/client/localhost/LocalhostRuleBasedSegmentsStorageProvider.java @@ -0,0 +1,20 @@ +package io.split.android.client.localhost; + +import androidx.annotation.NonNull; + +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProvider; + +class LocalhostRuleBasedSegmentsStorageProvider implements RuleBasedSegmentStorageProvider { + + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; + + LocalhostRuleBasedSegmentsStorageProvider(@NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage) { + mRuleBasedSegmentStorage = ruleBasedSegmentStorage; + } + + @Override + public RuleBasedSegmentStorage get() { + return mRuleBasedSegmentStorage; + } +} diff --git a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java b/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java index 25092466b..a472a5c00 100644 --- a/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java +++ b/src/main/java/io/split/android/client/localhost/LocalhostSplitFactory.java @@ -34,6 +34,7 @@ import io.split.android.client.validators.AttributesValidatorImpl; import io.split.android.client.validators.SplitValidatorImpl; import io.split.android.client.validators.ValidationMessageLoggerImpl; +import io.split.android.engine.experiments.ParserCommons; import io.split.android.engine.experiments.SplitParser; /** @@ -66,7 +67,9 @@ public LocalhostSplitFactory(String key, Context context, EventsManagerCoordinator eventsManagerCoordinator = new EventsManagerCoordinator(); FileStorage fileStorage = new FileStorage(context.getCacheDir(), ServiceConstants.LOCALHOST_FOLDER); SplitsStorage splitsStorage = new LocalhostSplitsStorage(mLocalhostFileName, context, fileStorage, eventsManagerCoordinator); - SplitParser splitParser = new SplitParser(new LocalhostMySegmentsStorageContainer(), new LocalhostMySegmentsStorageContainer()); + + SplitParser splitParser = getSplitParser(); + SplitTaskExecutorImpl taskExecutor = new SplitTaskExecutorImpl(); AttributesManagerFactory attributesManagerFactory = new AttributesManagerFactoryImpl(new AttributesValidatorImpl(), new ValidationMessageLoggerImpl()); @@ -102,6 +105,15 @@ public LocalhostSplitFactory(String key, Context context, Logger.i("Android SDK initialized!"); } + @NonNull + private static SplitParser getSplitParser() { + ParserCommons parserCommons = new ParserCommons( + new LocalhostMySegmentsStorageContainer(), + new LocalhostMySegmentsStorageContainer()); + parserCommons.setRuleBasedSegmentStorage(new LocalhostRuleBasedSegmentsStorage()); + return new SplitParser(parserCommons); + } + @VisibleForTesting LocalhostSplitFactory(@NonNull SplitsStorage splitsStorage, @NonNull SplitParser splitParser, diff --git a/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java b/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java index ac9a86105..60453013b 100644 --- a/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java +++ b/src/main/java/io/split/android/client/network/HttpStreamRequestImpl.java @@ -88,9 +88,7 @@ public void addHeader(String name, String value) { public void close() { try { Logger.d("Closing streaming connection"); - if (mConnection != null) { - mConnection.disconnect(); - } + disconnect(); } catch (Exception e) { Logger.d("Unknown error closing connection: " + e.getLocalizedMessage()); } finally { @@ -119,24 +117,16 @@ private HttpStreamResponse getRequest() throws HttpException { response = handleAuthentication(response); } } catch (MalformedURLException e) { - if (mConnection != null) { - mConnection.disconnect(); - } + disconnect(); throw new HttpException("URL is malformed: " + e.getLocalizedMessage()); } catch (ProtocolException e) { - if (mConnection != null) { - mConnection.disconnect(); - } + disconnect(); throw new HttpException("Http method not allowed: " + e.getLocalizedMessage()); } catch (SSLPeerUnverifiedException e) { - if (mConnection != null) { - mConnection.disconnect(); - } + disconnect(); throw new HttpException("SSL peer not verified: " + e.getLocalizedMessage(), HttpStatus.INTERNAL_NON_RETRYABLE.getCode()); } catch (IOException e) { - if (mConnection != null) { - mConnection.disconnect(); - } + disconnect(); throw new HttpException("Something happened while retrieving data: " + e.getLocalizedMessage()); } @@ -187,4 +177,10 @@ private HttpStreamResponse buildResponse(HttpURLConnection connection) throws IO return new HttpStreamResponseImpl(responseCode); } + + private void disconnect() { + if (mConnection != null) { + mConnection.disconnect(); + } + } } diff --git a/src/main/java/io/split/android/client/service/ServiceFactory.java b/src/main/java/io/split/android/client/service/ServiceFactory.java index 9af8f5093..74a9411c8 100644 --- a/src/main/java/io/split/android/client/service/ServiceFactory.java +++ b/src/main/java/io/split/android/client/service/ServiceFactory.java @@ -8,7 +8,7 @@ import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.network.HttpClient; import io.split.android.client.network.SdkTargetPath; import io.split.android.client.service.events.EventsRequestBodySerializer; @@ -23,7 +23,7 @@ import io.split.android.client.service.impressions.unique.MTK; import io.split.android.client.service.impressions.unique.MTKRequestBodySerializer; import io.split.android.client.service.mysegments.AllSegmentsResponseParser; -import io.split.android.client.service.splits.SplitChangeResponseParser; +import io.split.android.client.service.rules.TargetingRulesResponseParser; import io.split.android.client.service.sseauthentication.SseAuthenticationResponseParser; import io.split.android.client.telemetry.TelemetryConfigBodySerializer; import io.split.android.client.telemetry.TelemetryStatsBodySerializer; @@ -33,14 +33,14 @@ @RestrictTo(RestrictTo.Scope.LIBRARY) public class ServiceFactory { - public static HttpFetcher getSplitsFetcher( + public static HttpFetcher getSplitsFetcher( HttpClient httpClient, String endPoint, String splitFilterQueryString) throws URISyntaxException { return new HttpFetcherImpl<>(httpClient, SdkTargetPath.splitChanges(endPoint, splitFilterQueryString), - new SplitChangeResponseParser()); + new TargetingRulesResponseParser()); } public static HttpFetcher getMySegmentsFetcher( diff --git a/src/main/java/io/split/android/client/service/SplitApiFacade.java b/src/main/java/io/split/android/client/service/SplitApiFacade.java index bad8763c6..9f5c93b78 100644 --- a/src/main/java/io/split/android/client/service/SplitApiFacade.java +++ b/src/main/java/io/split/android/client/service/SplitApiFacade.java @@ -9,7 +9,7 @@ import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpRecorder; import io.split.android.client.service.http.mysegments.MySegmentsFetcherFactory; @@ -20,7 +20,7 @@ import io.split.android.client.telemetry.model.Stats; public class SplitApiFacade { - private final HttpFetcher mSplitFetcher; + private final HttpFetcher mSplitFetcher; private final MySegmentsFetcherFactory mMySegmentsFetcherFactory; private final HttpFetcher mSseAuthenticationFetcher; private final HttpRecorder> mEventsRecorder; @@ -30,7 +30,7 @@ public class SplitApiFacade { private final HttpRecorder mTelemetryConfigRecorder; private final HttpRecorder mTelemetryStatsRecorder; - public SplitApiFacade(@NonNull HttpFetcher splitFetcher, + public SplitApiFacade(@NonNull HttpFetcher splitFetcher, @NonNull MySegmentsFetcherFactory mySegmentsFetcherFactory, @NonNull HttpFetcher sseAuthenticationFetcher, @NonNull HttpRecorder> eventsRecorder, @@ -50,7 +50,7 @@ public SplitApiFacade(@NonNull HttpFetcher splitFetcher, mTelemetryStatsRecorder = checkNotNull(telemetryStatsRecorder); } - public HttpFetcher getSplitFetcher() { + public HttpFetcher getSplitFetcher() { return mSplitFetcher; } diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java b/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java index 989557cd9..b98aa4ffd 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskFactory.java @@ -1,11 +1,14 @@ package io.split.android.client.service.executor; +import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.dtos.Split; import io.split.android.client.service.CleanUpDatabaseTask; import io.split.android.client.service.events.EventsRecorderTask; import io.split.android.client.service.impressions.ImpressionsTaskFactory; +import io.split.android.client.service.rules.LoadRuleBasedSegmentsTask; import io.split.android.client.service.splits.FilterSplitsInCacheTask; import io.split.android.client.service.splits.LoadSplitsTask; +import io.split.android.client.service.rules.RuleBasedSegmentInPlaceUpdateTask; import io.split.android.client.service.splits.SplitInPlaceUpdateTask; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.service.splits.SplitsSyncTask; @@ -23,9 +26,11 @@ public interface SplitTaskFactory extends TelemetryTaskFactory, ImpressionsTaskF LoadSplitsTask createLoadSplitsTask(); + LoadRuleBasedSegmentsTask createLoadRuleBasedSegmentsTask(); + SplitKillTask createSplitKillTask(Split split); - SplitsUpdateTask createSplitsUpdateTask(long since); + SplitsUpdateTask createSplitsUpdateTask(Long since, Long rbsSince); SplitInPlaceUpdateTask createSplitsUpdateTask(Split featureFlag, long since); @@ -34,4 +39,6 @@ public interface SplitTaskFactory extends TelemetryTaskFactory, ImpressionsTaskF CleanUpDatabaseTask createCleanUpDatabaseTask(long maxTimestamp); EncryptionMigrationTask createEncryptionMigrationTask(String sdkKey, SplitRoomDatabase splitRoomDatabase, boolean encryptionEnabled, SplitCipher splitCipher); + + RuleBasedSegmentInPlaceUpdateTask createRuleBasedSegmentUpdateTask(RuleBasedSegment ruleBasedSegment, long changeNumber); } diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java b/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java index 934f9ac75..fe770bc20 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskFactoryImpl.java @@ -16,6 +16,7 @@ import io.split.android.client.SplitClientConfig; import io.split.android.client.SplitFilter; import io.split.android.client.TestingConfig; +import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.dtos.Split; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.service.CleanUpDatabaseTask; @@ -31,8 +32,11 @@ import io.split.android.client.service.impressions.unique.SaveUniqueImpressionsTask; import io.split.android.client.service.impressions.unique.UniqueKeysRecorderTask; import io.split.android.client.service.impressions.unique.UniqueKeysRecorderTaskConfig; +import io.split.android.client.service.rules.LoadRuleBasedSegmentsTask; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.FilterSplitsInCacheTask; import io.split.android.client.service.splits.LoadSplitsTask; +import io.split.android.client.service.rules.RuleBasedSegmentInPlaceUpdateTask; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitInPlaceUpdateTask; import io.split.android.client.service.splits.SplitKillTask; @@ -48,6 +52,7 @@ import io.split.android.client.storage.cipher.SplitCipher; import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; import io.split.android.client.telemetry.storage.TelemetryStorage; @@ -62,6 +67,7 @@ public class SplitTaskFactoryImpl implements SplitTaskFactory { private final ISplitEventsManager mEventsManager; private final TelemetryTaskFactory mTelemetryTaskFactory; private final SplitChangeProcessor mSplitChangeProcessor; + private final RuleBasedSegmentChangeProcessor mRuleBasedSegmentChangeProcessor; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; private final List mFilters; @@ -83,6 +89,8 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, mFlagsSpecFromConfig = flagsSpecFromConfig; mEventsManager = eventsManager; mSplitChangeProcessor = new SplitChangeProcessor(filters, flagSetsFilter); + mRuleBasedSegmentChangeProcessor = new RuleBasedSegmentChangeProcessor(); + RuleBasedSegmentStorageProducer ruleBasedSegmentStorageProducer = mSplitsStorageContainer.getRuleBasedSegmentStorage(); TelemetryStorage telemetryStorage = mSplitsStorageContainer.getTelemetryStorage(); mTelemetryRuntimeProducer = telemetryStorage; @@ -90,6 +98,9 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, mSplitsSyncHelper = new SplitsSyncHelper(mSplitApiFacade.getSplitFetcher(), mSplitsStorageContainer.getSplitsStorage(), mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, + ruleBasedSegmentStorageProducer, + mSplitsStorageContainer.getGeneralInfoStorage(), mTelemetryRuntimeProducer, new ReconnectBackoffCounter(1, testingConfig.getCdnBackoffTime()), flagsSpecFromConfig); @@ -97,8 +108,12 @@ public SplitTaskFactoryImpl(@NonNull SplitClientConfig splitClientConfig, mSplitsSyncHelper = new SplitsSyncHelper(mSplitApiFacade.getSplitFetcher(), mSplitsStorageContainer.getSplitsStorage(), mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, + ruleBasedSegmentStorageProducer, + mSplitsStorageContainer.getGeneralInfoStorage(), mTelemetryRuntimeProducer, - flagsSpecFromConfig); + flagsSpecFromConfig, + false); } mFilters = (filters == null) ? new ArrayList<>() : new ArrayList<>(filters.values()); @@ -129,6 +144,7 @@ public ImpressionsRecorderTask createImpressionsRecorderTask() { @Override public SplitsSyncTask createSplitsSyncTask(boolean checkCacheExpiration) { return SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorageContainer.getSplitsStorage(), + mSplitsStorageContainer.getRuleBasedSegmentStorage(), mSplitsFilterQueryStringFromConfig, mEventsManager, mSplitsStorageContainer.getTelemetryStorage()); } @@ -137,14 +153,19 @@ public LoadSplitsTask createLoadSplitsTask() { return new LoadSplitsTask(mSplitsStorageContainer.getSplitsStorage(), mSplitsFilterQueryStringFromConfig, mFlagsSpecFromConfig); } + @Override + public LoadRuleBasedSegmentsTask createLoadRuleBasedSegmentsTask() { + return new LoadRuleBasedSegmentsTask(mSplitsStorageContainer.getRuleBasedSegmentStorage()); + } + @Override public SplitKillTask createSplitKillTask(Split split) { return new SplitKillTask(mSplitsStorageContainer.getSplitsStorage(), split, mEventsManager); } @Override - public SplitsUpdateTask createSplitsUpdateTask(long since) { - return new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorageContainer.getSplitsStorage(), since, mEventsManager); + public SplitsUpdateTask createSplitsUpdateTask(Long since, Long rbsSince) { + return new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorageContainer.getSplitsStorage(), mSplitsStorageContainer.getRuleBasedSegmentStorage(), since, rbsSince, mEventsManager); } @Override @@ -211,6 +232,11 @@ public EncryptionMigrationTask createEncryptionMigrationTask(String sdkKey, Spli return new EncryptionMigrationTask(sdkKey, splitRoomDatabase, encryptionEnabled, splitCipher); } + @Override + public RuleBasedSegmentInPlaceUpdateTask createRuleBasedSegmentUpdateTask(RuleBasedSegment ruleBasedSegment, long changeNumber) { + return new RuleBasedSegmentInPlaceUpdateTask(mSplitsStorageContainer.getRuleBasedSegmentStorage(), mRuleBasedSegmentChangeProcessor, mEventsManager, ruleBasedSegment, changeNumber); + } + @NonNull private TelemetryTaskFactory initializeTelemetryTaskFactory(@NonNull SplitClientConfig splitClientConfig, @Nullable Map filters, TelemetryStorage telemetryStorage) { final TelemetryTaskFactory mTelemetryTaskFactory; diff --git a/src/main/java/io/split/android/client/service/executor/SplitTaskType.java b/src/main/java/io/split/android/client/service/executor/SplitTaskType.java index 601b79b7f..7842fea6d 100644 --- a/src/main/java/io/split/android/client/service/executor/SplitTaskType.java +++ b/src/main/java/io/split/android/client/service/executor/SplitTaskType.java @@ -8,5 +8,6 @@ public enum SplitTaskType { MY_SEGMENTS_UPDATE, LOAD_LOCAL_ATTRIBUTES, TELEMETRY_CONFIG_TASK, TELEMETRY_STATS_TASK, SAVE_UNIQUE_KEYS_TASK, UNIQUE_KEYS_RECORDER_TASK, - MY_LARGE_SEGMENTS_UPDATE, + MY_LARGE_SEGMENTS_UPDATE, LOAD_LOCAL_RULE_BASED_SEGMENTS, + RULE_BASED_SEGMENT_SYNC, } diff --git a/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java b/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java index 38c445713..d45e36f6f 100644 --- a/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java +++ b/src/main/java/io/split/android/client/service/http/HttpFetcherImpl.java @@ -8,6 +8,8 @@ import java.net.URI; import java.util.Map; +import io.split.android.android_client.BuildConfig; +import io.split.android.client.ServiceEndpoints; import io.split.android.client.network.HttpClient; import io.split.android.client.network.HttpException; import io.split.android.client.network.HttpMethod; @@ -56,6 +58,9 @@ public T execute(@NonNull Map params, } if (!response.isSuccess()) { int httpStatus = response.getHttpStatus(); + + checkOutdatedProxyError(httpStatus, builtUri, params); + throw new HttpFetcherException(mTarget.toString(), "http return code " + httpStatus, httpStatus); } @@ -73,4 +78,16 @@ public T execute(@NonNull Map params, } return responseData; } + + private void checkOutdatedProxyError(int httpStatus, @Nullable URI builtUri, Map params) throws HttpFetcherException { + int proxyErrorStatus = HttpStatus.BAD_REQUEST.getCode(); + boolean sdkEndpointOverridden = builtUri != null && + builtUri.getHost() != null && + ServiceEndpoints.EndpointValidator.sdkEndpointIsOverridden(builtUri.getHost()); + boolean isLatestSpec = params != null && BuildConfig.FLAGS_SPEC.equals(params.get("s")); + + if (httpStatus == proxyErrorStatus && sdkEndpointOverridden && isLatestSpec) { + throw new HttpFetcherException(mTarget.toString(), "Proxy is outdated", HttpStatus.INTERNAL_PROXY_OUTDATED.getCode()); + } + } } diff --git a/src/main/java/io/split/android/client/service/http/HttpStatus.java b/src/main/java/io/split/android/client/service/http/HttpStatus.java index 2ae1c9a86..33dede76e 100644 --- a/src/main/java/io/split/android/client/service/http/HttpStatus.java +++ b/src/main/java/io/split/android/client/service/http/HttpStatus.java @@ -6,8 +6,10 @@ public enum HttpStatus { URI_TOO_LONG(414, "URI Too Long"), FORBIDDEN(403, "Forbidden"), + BAD_REQUEST(400, "Bad request"), - INTERNAL_NON_RETRYABLE(9009, "Non retryable"); + INTERNAL_NON_RETRYABLE(9009, "Non retryable"), + INTERNAL_PROXY_OUTDATED(9010, "Split Proxy outdated"); private final int mCode; private final String mDescription; @@ -49,4 +51,8 @@ public static boolean isNotRetryable(HttpStatus httpStatus) { public static boolean isNotRetryable(Integer code) { return isNotRetryable(fromCode(code)); } + + public static boolean isProxyOutdated(HttpStatus status) { + return status == HttpStatus.INTERNAL_PROXY_OUTDATED; + } } diff --git a/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java b/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java new file mode 100644 index 000000000..ba8731c4a --- /dev/null +++ b/src/main/java/io/split/android/client/service/rules/LoadRuleBasedSegmentsTask.java @@ -0,0 +1,32 @@ +package io.split.android.client.service.rules; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.utils.logger.Logger; + +public class LoadRuleBasedSegmentsTask implements SplitTask { + + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; + + public LoadRuleBasedSegmentsTask(@NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage) { + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); + } + + @NonNull + @Override + public SplitTaskExecutionInfo execute() { + try { + mRuleBasedSegmentStorage.loadLocal(); + return SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_RULE_BASED_SEGMENTS); + } catch (Exception e) { + Logger.e("Error loading rule based segments: " + e.getLocalizedMessage()); + return SplitTaskExecutionInfo.error(SplitTaskType.LOAD_LOCAL_RULE_BASED_SEGMENTS); + } + } +} diff --git a/src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java b/src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java new file mode 100644 index 000000000..376c5c9ad --- /dev/null +++ b/src/main/java/io/split/android/client/service/rules/ProcessedRuleBasedSegmentChange.java @@ -0,0 +1,38 @@ +package io.split.android.client.service.rules; + +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; + +public class ProcessedRuleBasedSegmentChange { + private final Set mActive; + private final Set mArchived; + private final long mChangeNumber; + private final long mUpdateTimestamp; + + public ProcessedRuleBasedSegmentChange(Set active, + Set archived, + long changeNumber, + long updateTimestamp) { + mActive = active; + mArchived = archived; + mChangeNumber = changeNumber; + mUpdateTimestamp = updateTimestamp; + } + + public Set getActive() { + return mActive; + } + + public Set getArchived() { + return mArchived; + } + + public long getChangeNumber() { + return mChangeNumber; + } + + public long getUpdateTimestamp() { + return mUpdateTimestamp; + } +} diff --git a/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java b/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java new file mode 100644 index 000000000..2f6d62fc5 --- /dev/null +++ b/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessor.java @@ -0,0 +1,30 @@ +package io.split.android.client.service.rules; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.dtos.Status; + +public class RuleBasedSegmentChangeProcessor { + + public ProcessedRuleBasedSegmentChange process(List segments, long changeNumber) { + Set toAdd = new HashSet<>(); + Set toRemove = new HashSet<>(); + for (RuleBasedSegment segment : segments) { + if (segment.getStatus() == Status.ACTIVE) { + toAdd.add(segment); + } else { + toRemove.add(segment); + } + } + + return new ProcessedRuleBasedSegmentChange(toAdd, toRemove, changeNumber, System.currentTimeMillis()); + } + + public ProcessedRuleBasedSegmentChange process(RuleBasedSegment segment, long changeNumber) { + return process(Collections.singletonList(segment), changeNumber); + } +} diff --git a/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java b/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java new file mode 100644 index 000000000..2e592e9bc --- /dev/null +++ b/src/main/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTask.java @@ -0,0 +1,55 @@ +package io.split.android.client.service.rules; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.utils.logger.Logger; + +public class RuleBasedSegmentInPlaceUpdateTask implements SplitTask { + + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; + private final long mChangeNumber; + private final RuleBasedSegment mRuleBasedSegment; + private final RuleBasedSegmentChangeProcessor mChangeProcessor; + private final ISplitEventsManager mEventsManager; + + public RuleBasedSegmentInPlaceUpdateTask(@NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage, + @NonNull RuleBasedSegmentChangeProcessor changeProcessor, + @NonNull ISplitEventsManager eventsManager, + @NonNull RuleBasedSegment ruleBasedSegment, + long changeNumber) { + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); + mRuleBasedSegment = checkNotNull(ruleBasedSegment); + mChangeProcessor = checkNotNull(changeProcessor); + mEventsManager = eventsManager; + mChangeNumber = changeNumber; + } + + @NonNull + @Override + public SplitTaskExecutionInfo execute() { + try { + ProcessedRuleBasedSegmentChange processedChange = mChangeProcessor.process(mRuleBasedSegment, mChangeNumber); + boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber); + + if (triggerSdkUpdate) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + } + + Logger.v("Updated rule based segment"); + return SplitTaskExecutionInfo.success(SplitTaskType.RULE_BASED_SEGMENT_SYNC); + } catch (Exception ex) { + Logger.e("Could not update rule based segment"); + + return SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC); + } + } +} diff --git a/src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java b/src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java new file mode 100644 index 000000000..8b330fd39 --- /dev/null +++ b/src/main/java/io/split/android/client/service/rules/TargetingRulesResponseParser.java @@ -0,0 +1,69 @@ +package io.split.android.client.service.rules; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +import java.io.StringReader; + +import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; +import io.split.android.client.service.http.HttpResponseParser; +import io.split.android.client.service.http.HttpResponseParserException; +import io.split.android.client.utils.Json; + +public class TargetingRulesResponseParser implements HttpResponseParser { + + @Override + public TargetingRulesChange parse(String responseData) throws HttpResponseParserException { + try { + if (responseData == null || responseData.isEmpty()) { + return null; + } + + if (isNewDto(responseData)) { + // New DTO: parse as TargetingRulesChange + return Json.fromJson(responseData, TargetingRulesChange.class); + } else { + // Legacy DTO: parse as SplitChange, wrap as TargetingRulesChange + SplitChange splitChange = Json.fromJson(responseData, SplitChange.class); + if (splitChange == null) { + return null; + } + return TargetingRulesChange.create(splitChange); + } + } catch (Exception e) { + throw new HttpResponseParserException("Error parsing splitChanges http response: " + e.getLocalizedMessage()); + } + } + + private boolean isNewDto(String json) throws Exception { + try (JsonReader reader = new JsonReader(new StringReader(json))) { + reader.setLenient(true); + if (reader.peek() == JsonToken.BEGIN_OBJECT) { + reader.beginObject(); + if (reader.hasNext()) { + String name = reader.nextName(); + if (newFieldNameIsPresent(name)) { + return true; + } + + reader.skipValue(); + while (reader.hasNext()) { + name = reader.nextName(); + if (newFieldNameIsPresent(name)) { + return true; + } + reader.skipValue(); + } + } + } else { + throw new HttpResponseParserException("Error parsing splitChanges http response: not a JSON object"); + } + return false; + } + } + + private static boolean newFieldNameIsPresent(String name) { + return "ff".equals(name) || "rbs".equals(name); + } +} diff --git a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java index f5546470e..49ca1d512 100644 --- a/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java +++ b/src/main/java/io/split/android/client/service/splits/LoadSplitsTask.java @@ -1,5 +1,7 @@ package io.split.android.client.service.splits; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -9,8 +11,6 @@ import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.utils.logger.Logger; -import static io.split.android.client.utils.Utils.checkNotNull; - /** * This task is responsible for loading the feature flags, saved filter & saved flags spec values * from the persistent storage into the in-memory storage. diff --git a/src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java b/src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java new file mode 100644 index 000000000..47b61c4dc --- /dev/null +++ b/src/main/java/io/split/android/client/service/splits/OutdatedSplitProxyHandler.java @@ -0,0 +1,167 @@ +package io.split.android.client.service.splits; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.VisibleForTesting; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.logger.Logger; + +/** + * Handles proxy spec fallback and recovery. + * + *

This class manages the state machine that determines which spec version (latest or legacy) should be used + * to communicate with the Split Proxy, based on observed proxy compatibility errors. + * It ensures that the SDK can automatically fall back to a legacy spec when the proxy is outdated, periodically + * attempt recovery, and return to normal operation if the proxy is upgraded.

+ * + *

State Machine:

+ *
    + *
  • NONE: Normal operation, using latest spec. Default state.
  • + *
  • FALLBACK: Entered when a proxy error is detected with the latest spec. SDK uses legacy spec and omits RB_SINCE param.
  • + *
  • RECOVERY: Entered after fallback interval elapses. SDK attempts to use latest spec again. If successful, returns to NONE.
  • + *
+ *

Transitions:

+ *
    + *
  • NONE --(proxy error w/ latest spec)--> FALLBACK
  • + *
  • FALLBACK --(interval elapsed)--> RECOVERY
  • + *
  • RECOVERY --(success w/ latest spec)--> NONE
  • + *
  • RECOVERY --(proxy error)--> FALLBACK
  • + *
  • FALLBACK --(generic 400)--> FALLBACK (error surfaced, no state change)
  • + *
+ *

Only an explicit proxy outdated error triggers fallback. Generic 400s do not.

+ * + */ +public class OutdatedSplitProxyHandler { + + private static final String PREVIOUS_SPEC = "1.2"; + + private final String mLatestSpec; + private final String mPreviousSpec; + private final boolean mForBackgroundSync; + private final long mProxyCheckIntervalMillis; + + private final AtomicLong mLastProxyCheckTimestamp = new AtomicLong(0L); + private final GeneralInfoStorage mGeneralInfoStorage; + private final AtomicReference mCurrentProxyHandlingType = new AtomicReference<>(ProxyHandlingType.NONE); + + OutdatedSplitProxyHandler(String flagSpec, boolean forBackgroundSync, GeneralInfoStorage generalInfoStorage, long proxyCheckIntervalMillis) { + this(flagSpec, PREVIOUS_SPEC, forBackgroundSync, generalInfoStorage, proxyCheckIntervalMillis); + } + + /** + * Constructs an OutdatedSplitProxyHandler instance with a custom proxy check interval. + * + * @param flagSpec the latest spec version + * @param previousSpec the previous spec version + * @param forBackgroundSync whether this instance is for background sync + * @param generalInfoStorage the general info storage + * @param proxyCheckIntervalMillis the custom proxy check interval + */ + @VisibleForTesting + OutdatedSplitProxyHandler(String flagSpec, String previousSpec, boolean forBackgroundSync, GeneralInfoStorage generalInfoStorage, long proxyCheckIntervalMillis) { + mLatestSpec = flagSpec; + mPreviousSpec = previousSpec; + mForBackgroundSync = forBackgroundSync; + mProxyCheckIntervalMillis = proxyCheckIntervalMillis; + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + /** + * Tracks a proxy error and updates the state machine accordingly. + */ + void trackProxyError() { + if (mForBackgroundSync) { + Logger.i("Background sync fetch; skipping proxy handling"); + updateHandlingType(ProxyHandlingType.NONE); + } else { + updateLastProxyCheckTimestamp(System.currentTimeMillis()); + updateHandlingType(ProxyHandlingType.FALLBACK); + } + } + + /** + * Performs a periodic proxy check to attempt recovery. + */ + void performProxyCheck() { + if (mForBackgroundSync) { + updateHandlingType(ProxyHandlingType.NONE); + } + + long lastProxyCheckTimestamp = getLastProxyCheckTimestamp(); + + if (lastProxyCheckTimestamp == 0L) { + updateHandlingType(ProxyHandlingType.NONE); + } else if (System.currentTimeMillis() - lastProxyCheckTimestamp > mProxyCheckIntervalMillis) { + Logger.i("Time since last check elapsed. Attempting recovery with latest spec: " + mLatestSpec); + updateHandlingType(ProxyHandlingType.RECOVERY); + } else { + Logger.v("Have used proxy fallback mode; time since last check has not elapsed. Using previous spec"); + updateHandlingType(ProxyHandlingType.FALLBACK); + } + } + + void resetProxyCheckTimestamp() { + updateLastProxyCheckTimestamp(0L); + } + + /** + * Returns the current spec version based on the state machine. + * + * @return the current spec version + */ + String getCurrentSpec() { + if (mCurrentProxyHandlingType.get() == ProxyHandlingType.FALLBACK) { + return mPreviousSpec; + } + + return mLatestSpec; + } + + /** + * Indicates whether the SDK is in fallback mode. + * + * @return true if in fallback mode, false otherwise + */ + boolean isFallbackMode() { + return mCurrentProxyHandlingType.get() == ProxyHandlingType.FALLBACK; + } + + /** + * Indicates whether the SDK is in recovery mode. + * + * @return true if in recovery mode, false otherwise + */ + boolean isRecoveryMode() { + return mCurrentProxyHandlingType.get() == ProxyHandlingType.RECOVERY; + } + + private void updateHandlingType(ProxyHandlingType proxyHandlingType) { + mCurrentProxyHandlingType.set(proxyHandlingType); + } + + private long getLastProxyCheckTimestamp() { + mLastProxyCheckTimestamp.compareAndSet(0L, mGeneralInfoStorage.getLastProxyUpdateTimestamp()); + return mLastProxyCheckTimestamp.get(); + } + + private void updateLastProxyCheckTimestamp(long newTimestamp) { + mLastProxyCheckTimestamp.set(newTimestamp); + mGeneralInfoStorage.setLastProxyUpdateTimestamp(newTimestamp); + } + + /** + * Enum representing the proxy handling types. + */ + private enum ProxyHandlingType { + // No action + NONE, + // Switch to previous spec + FALLBACK, + // Attempt recovery + RECOVERY, + } +} diff --git a/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java b/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java new file mode 100644 index 000000000..7c20cf88e --- /dev/null +++ b/src/main/java/io/split/android/client/service/splits/RuleBasedSegmentInPlaceUpdateTask.java @@ -0,0 +1,57 @@ +package io.split.android.client.service.splits; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.utils.logger.Logger; + +public class RuleBasedSegmentInPlaceUpdateTask implements SplitTask { + + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; + private final long mChangeNumber; + private final RuleBasedSegment mRuleBasedSegment; + private final RuleBasedSegmentChangeProcessor mChangeProcessor; + private final ISplitEventsManager mEventsManager; + + public RuleBasedSegmentInPlaceUpdateTask(@NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage, + @NonNull RuleBasedSegmentChangeProcessor changeProcessor, + @NonNull ISplitEventsManager eventsManager, + @NonNull RuleBasedSegment ruleBasedSegment, + long changeNumber) { + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); + mRuleBasedSegment = checkNotNull(ruleBasedSegment); + mChangeProcessor = checkNotNull(changeProcessor); + mEventsManager = eventsManager; + mChangeNumber = changeNumber; + } + + @NonNull + @Override + public SplitTaskExecutionInfo execute() { + try { + ProcessedRuleBasedSegmentChange processedChange = mChangeProcessor.process(mRuleBasedSegment, mChangeNumber); + boolean triggerSdkUpdate = mRuleBasedSegmentStorage.update(processedChange.getActive(), processedChange.getArchived(), mChangeNumber); + + if (triggerSdkUpdate) { + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + } + + Logger.v("Updated rule based segment"); + return SplitTaskExecutionInfo.success(SplitTaskType.RULE_BASED_SEGMENT_SYNC); + } catch (Exception ex) { + Logger.e("Could not update rule based segment"); + + return SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC); + } + } +} diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java index 17e136fb7..e13f5a85b 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncHelper.java @@ -12,7 +12,9 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import io.split.android.client.dtos.RuleBasedSegmentChange; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTaskExecutionInfo; @@ -20,8 +22,12 @@ import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.http.HttpStatus; +import io.split.android.client.service.rules.ProcessedRuleBasedSegmentChange; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.sseclient.BackoffCounter; import io.split.android.client.service.sseclient.ReconnectBackoffCounter; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -31,60 +37,108 @@ public class SplitsSyncHelper { private static final String SINCE_PARAM = "since"; private static final String TILL_PARAM = "till"; + private static final String RBS_SINCE_PARAM = "rbSince"; private static final int ON_DEMAND_FETCH_BACKOFF_MAX_WAIT = ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_WAIT; + private static final long DEFAULT_PROXY_CHECK_INTERVAL_MILLIS = TimeUnit.HOURS.toMillis(1); - private final HttpFetcher mSplitFetcher; + private final HttpFetcher mSplitFetcher; private final SplitsStorage mSplitsStorage; private final SplitChangeProcessor mSplitChangeProcessor; + private final RuleBasedSegmentChangeProcessor mRuleBasedSegmentChangeProcessor; + private final RuleBasedSegmentStorageProducer mRuleBasedSegmentStorage; private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; private final BackoffCounter mBackoffCounter; - private final String mFlagsSpec; + private final OutdatedSplitProxyHandler mOutdatedSplitProxyHandler; - public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, + public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, @NonNull SplitsStorage splitsStorage, @NonNull SplitChangeProcessor splitChangeProcessor, + @NonNull RuleBasedSegmentChangeProcessor ruleBasedSegmentChangeProcessor, + @NonNull RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, + @NonNull GeneralInfoStorage generalInfoStorage, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, - @Nullable String flagsSpec) { + @Nullable String flagsSpec, + boolean forBackgroundSync) { this(splitFetcher, splitsStorage, splitChangeProcessor, + ruleBasedSegmentChangeProcessor, + ruleBasedSegmentStorage, + generalInfoStorage, telemetryRuntimeProducer, new ReconnectBackoffCounter(1, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT), - flagsSpec); + flagsSpec, + forBackgroundSync, + DEFAULT_PROXY_CHECK_INTERVAL_MILLIS); } - @VisibleForTesting - public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, + public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, @NonNull SplitsStorage splitsStorage, @NonNull SplitChangeProcessor splitChangeProcessor, + @NonNull RuleBasedSegmentChangeProcessor ruleBasedSegmentChangeProcessor, + @NonNull RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, + @NonNull GeneralInfoStorage generalInfoStorage, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, @NonNull BackoffCounter backoffCounter, @Nullable String flagsSpec) { + this(splitFetcher, + splitsStorage, + splitChangeProcessor, + ruleBasedSegmentChangeProcessor, + ruleBasedSegmentStorage, + generalInfoStorage, + telemetryRuntimeProducer, + backoffCounter, + flagsSpec, + false, + DEFAULT_PROXY_CHECK_INTERVAL_MILLIS); + } + + @VisibleForTesting + public SplitsSyncHelper(@NonNull HttpFetcher splitFetcher, + @NonNull SplitsStorage splitsStorage, + @NonNull SplitChangeProcessor splitChangeProcessor, + @NonNull RuleBasedSegmentChangeProcessor ruleBasedSegmentChangeProcessor, + @NonNull RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, + @NonNull GeneralInfoStorage generalInfoStorage, + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, + @NonNull BackoffCounter backoffCounter, + @Nullable String flagsSpec, + boolean forBackgroundSync, + long proxyCheckIntervalMillis) { mSplitFetcher = checkNotNull(splitFetcher); mSplitsStorage = checkNotNull(splitsStorage); mSplitChangeProcessor = checkNotNull(splitChangeProcessor); + mRuleBasedSegmentChangeProcessor = checkNotNull(ruleBasedSegmentChangeProcessor); + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); mBackoffCounter = checkNotNull(backoffCounter); - mFlagsSpec = flagsSpec; + mOutdatedSplitProxyHandler = new OutdatedSplitProxyHandler(flagsSpec, forBackgroundSync, generalInfoStorage, proxyCheckIntervalMillis); } - public SplitTaskExecutionInfo sync(long till, int onDemandFetchBackoffMaxRetries) { + public SplitTaskExecutionInfo sync(SinceChangeNumbers till, int onDemandFetchBackoffMaxRetries) { return sync(till, false, true, false, onDemandFetchBackoffMaxRetries); } - public SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { + public SplitTaskExecutionInfo sync(SinceChangeNumbers till, boolean clearBeforeUpdate, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { return sync(till, clearBeforeUpdate, false, resetChangeNumber, onDemandFetchBackoffMaxRetries); } - private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { + private SplitTaskExecutionInfo sync(SinceChangeNumbers till, boolean clearBeforeUpdate, boolean avoidCache, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) { try { - boolean successfulSync = attemptSplitSync(till, clearBeforeUpdate, avoidCache, false, resetChangeNumber, onDemandFetchBackoffMaxRetries); + mOutdatedSplitProxyHandler.performProxyCheck(); + if (mOutdatedSplitProxyHandler.isRecoveryMode()) { + clearBeforeUpdate = true; + resetChangeNumber = true; + } - if (!successfulSync) { - attemptSplitSync(till, clearBeforeUpdate, avoidCache, true, resetChangeNumber, onDemandFetchBackoffMaxRetries); + CdnByPassType cdnByPassType = attemptSplitSync(till, clearBeforeUpdate, avoidCache, CdnByPassType.NONE, resetChangeNumber, onDemandFetchBackoffMaxRetries); + + if (cdnByPassType != CdnByPassType.NONE) { + attemptSplitSync(till, clearBeforeUpdate, avoidCache, cdnByPassType, resetChangeNumber, onDemandFetchBackoffMaxRetries); } } catch (HttpFetcherException e) { - logError("Network error while fetching feature flags" + e.getLocalizedMessage()); + logError("Network error while fetching feature flags - " + e.getLocalizedMessage()); mTelemetryRuntimeProducer.recordSyncError(OperationType.SPLITS, e.getHttpStatus()); HttpStatus httpStatus = HttpStatus.fromCode(e.getHttpStatus()); @@ -97,6 +151,14 @@ private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolea Collections.singletonMap(SplitTaskExecutionInfo.DO_NOT_RETRY, true)); } + if (HttpStatus.isProxyOutdated(httpStatus)) { + try { + mOutdatedSplitProxyHandler.trackProxyError(); + } catch (Exception e1) { + logError("Unexpected while handling outdated proxy " + e1.getLocalizedMessage()); + } + } + return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); } catch (Exception e) { logError("Unexpected while fetching feature flags" + e.getLocalizedMessage()); @@ -104,32 +166,48 @@ private SplitTaskExecutionInfo sync(long till, boolean clearBeforeUpdate, boolea } Logger.d("Feature flags have been updated"); + + if (mOutdatedSplitProxyHandler.isRecoveryMode()) { + Logger.i("Resetting proxy check timestamp due to successful recovery"); + mOutdatedSplitProxyHandler.resetProxyCheckTimestamp(); + } + return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); + } + + private SplitTaskExecutionInfo handleOutdatedProxy(SinceChangeNumbers till, boolean ignoredAvoidCache, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) throws Exception { + + return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); } /** - * @param till target changeNumber - * @param clearBeforeUpdate whether to clear splits storage before updating it - * @param avoidCache whether to send no-cache header to api - * @param withCdnBypass whether to add additional query param to bypass CDN + * @param targetChangeNumber target changeNumber + * @param clearBeforeUpdate whether to clear splits storage before updating it + * @param avoidCache whether to send no-cache header to api + * @param withCdnBypass whether to add additional query param to bypass CDN * @param onDemandFetchBackoffMaxRetries max backoff retries for CDN bypass * @return whether sync finished successfully */ - private boolean attemptSplitSync(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean withCdnBypass, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) throws Exception { + private CdnByPassType attemptSplitSync(SinceChangeNumbers targetChangeNumber, boolean clearBeforeUpdate, boolean avoidCache, CdnByPassType withCdnBypass, boolean resetChangeNumber, int onDemandFetchBackoffMaxRetries) throws Exception { int remainingAttempts = onDemandFetchBackoffMaxRetries; mBackoffCounter.resetCounter(); while (true) { remainingAttempts--; - long changeNumber = fetchUntil(till, clearBeforeUpdate, avoidCache, withCdnBypass, resetChangeNumber); + SinceChangeNumbers retrievedChangeNumber = fetchUntil(targetChangeNumber, clearBeforeUpdate, avoidCache, withCdnBypass, resetChangeNumber); resetChangeNumber = false; - if (till <= changeNumber) { - return true; + if (targetChangeNumber.getFlagsSince() <= retrievedChangeNumber.getFlagsSince() && + targetChangeNumber.getRbsSince() != null && retrievedChangeNumber.getRbsSince() != null && targetChangeNumber.getRbsSince() <= retrievedChangeNumber.getRbsSince()) { + return CdnByPassType.NONE; } if (remainingAttempts <= 0) { - return false; + if (targetChangeNumber.getFlagsSince() <= retrievedChangeNumber.getFlagsSince()) { + return CdnByPassType.RBS; + } else { + return CdnByPassType.FLAGS; + } } try { @@ -142,47 +220,63 @@ private boolean attemptSplitSync(long till, boolean clearBeforeUpdate, boolean a } } - private long fetchUntil(long till, boolean clearBeforeUpdate, boolean avoidCache, boolean withCdnByPass, boolean resetChangeNumber) throws Exception { + private SinceChangeNumbers fetchUntil(SinceChangeNumbers till, boolean clearBeforeUpdate, boolean avoidCache, CdnByPassType withCdnByPass, boolean resetChangeNumber) throws Exception { boolean shouldClearBeforeUpdate = clearBeforeUpdate; - long newTill = till; + SinceChangeNumbers newTill = till; while (true) { long changeNumber = (resetChangeNumber) ? -1 : mSplitsStorage.getTill(); + long rbsChangeNumber = (resetChangeNumber) ? -1 : mRuleBasedSegmentStorage.getChangeNumber(); resetChangeNumber = false; - if (newTill < changeNumber) { - return changeNumber; + if ((newTill.getFlagsSince() < changeNumber) && ((newTill.getRbsSince() == null) || (newTill.getRbsSince() < rbsChangeNumber))) { + return new SinceChangeNumbers(changeNumber, rbsChangeNumber); } - SplitChange splitChange = fetchSplits(changeNumber, avoidCache, withCdnByPass); - updateStorage(shouldClearBeforeUpdate, splitChange); + TargetingRulesChange targetingRulesChange = fetchSplits(new SinceChangeNumbers(changeNumber, rbsChangeNumber), avoidCache, withCdnByPass); + SplitChange splitChange = targetingRulesChange.getFeatureFlagsChange(); + RuleBasedSegmentChange ruleBasedSegmentChange = targetingRulesChange.getRuleBasedSegmentsChange(); + updateStorage(shouldClearBeforeUpdate, splitChange, ruleBasedSegmentChange); shouldClearBeforeUpdate = false; - newTill = splitChange.till; - if (splitChange.till == splitChange.since) { - return splitChange.till; + newTill = new SinceChangeNumbers(splitChange.till, ruleBasedSegmentChange.getTill()); + if (splitChange.till == splitChange.since && ruleBasedSegmentChange.getTill() == ruleBasedSegmentChange.getSince()) { + return new SinceChangeNumbers(splitChange.till, ruleBasedSegmentChange.getTill()); } } } - private SplitChange fetchSplits(long till, boolean avoidCache, boolean withCdnByPass) throws HttpFetcherException { + private TargetingRulesChange fetchSplits(SinceChangeNumbers till, boolean avoidCache, CdnByPassType cdnByPassType) throws HttpFetcherException { Map params = new LinkedHashMap<>(); - if (mFlagsSpec != null && !mFlagsSpec.trim().isEmpty()) { - params.put(FLAGS_SPEC_PARAM, mFlagsSpec); + String flagsSpec = mOutdatedSplitProxyHandler.getCurrentSpec(); + if (flagsSpec != null && !flagsSpec.trim().isEmpty()) { + params.put(FLAGS_SPEC_PARAM, flagsSpec); + } + params.put(SINCE_PARAM, till.getFlagsSince()); + if (!mOutdatedSplitProxyHandler.isFallbackMode() && till.getRbsSince() != null) { + params.put(RBS_SINCE_PARAM, till.getRbsSince()); } - params.put(SINCE_PARAM, till); - if (withCdnByPass) { - params.put(TILL_PARAM, till); + if (cdnByPassType == CdnByPassType.RBS) { + params.put(TILL_PARAM, till.getRbsSince()); + } else if (cdnByPassType == CdnByPassType.FLAGS) { + params.put(TILL_PARAM, till.getFlagsSince()); } return mSplitFetcher.execute(params, getHeaders(avoidCache)); } - private void updateStorage(boolean clearBeforeUpdate, SplitChange splitChange) { + private void updateStorage(boolean clearBeforeUpdate, SplitChange splitChange, RuleBasedSegmentChange ruleBasedSegmentChange) { if (clearBeforeUpdate) { mSplitsStorage.clear(); + mRuleBasedSegmentStorage.clear(); } mSplitsStorage.update(mSplitChangeProcessor.process(splitChange)); + updateRbsStorage(ruleBasedSegmentChange); + } + + private void updateRbsStorage(RuleBasedSegmentChange ruleBasedSegmentChange) { + ProcessedRuleBasedSegmentChange change = mRuleBasedSegmentChangeProcessor.process(ruleBasedSegmentChange.getSegments(), ruleBasedSegmentChange.getTill()); + mRuleBasedSegmentStorage.update(change.getActive(), change.getArchived(), change.getChangeNumber()); } private void logError(String message) { @@ -195,4 +289,46 @@ private void logError(String message) { } return null; } + + public static class SinceChangeNumbers { + private final long mFlagsSince; + @Nullable + private final Long mRbsSince; + + public SinceChangeNumbers(long flagsSince, @Nullable Long rbsSince) { + mFlagsSince = flagsSince; + mRbsSince = rbsSince; + } + + public long getFlagsSince() { + return mFlagsSince; + } + + @Nullable + public Long getRbsSince() { + return mRbsSince; + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof SinceChangeNumbers && + mFlagsSince == ((SinceChangeNumbers) obj).mFlagsSince && + (mRbsSince == null && ((SinceChangeNumbers) obj).mRbsSince == null); + } + + @NonNull + @Override + public String toString() { + return "{" + + "ff=" + mFlagsSince + + ", rbs=" + mRbsSince + + '}'; + } + } + + private enum CdnByPassType { + NONE, + FLAGS, + RBS, + } } diff --git a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java index 8a7a7bc6f..2cb35e578 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsSyncTask.java @@ -12,6 +12,7 @@ import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.synchronizer.SplitsChangeChecker; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -21,6 +22,7 @@ public class SplitsSyncTask implements SplitTask { private final String mSplitsFilterQueryStringFromConfig; private final SplitsStorage mSplitsStorage; + private final RuleBasedSegmentStorageProducer mRuleBasedSegmentStorage; private final SplitsSyncHelper mSplitsSyncHelper; @Nullable private final ISplitEventsManager mEventsManager; // Should only be null on background sync @@ -30,21 +32,24 @@ public class SplitsSyncTask implements SplitTask { public static SplitsSyncTask build(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull SplitsStorage splitsStorage, + @NonNull RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, String splitsFilterQueryString, @NonNull ISplitEventsManager eventsManager, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - return new SplitsSyncTask(splitsSyncHelper, splitsStorage, splitsFilterQueryString, telemetryRuntimeProducer, eventsManager, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + return new SplitsSyncTask(splitsSyncHelper, splitsStorage, ruleBasedSegmentStorage, splitsFilterQueryString, telemetryRuntimeProducer, eventsManager, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } public static SplitTask buildForBackground(@NonNull SplitsSyncHelper splitsSyncHelper, - @NonNull SplitsStorage splitsStorage, + @NonNull SplitsStorage splitsStorage, + @NonNull RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, String splitsFilterQueryString, - @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - return new SplitsSyncTask(splitsSyncHelper, splitsStorage, splitsFilterQueryString, telemetryRuntimeProducer, null, 1); + @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { + return new SplitsSyncTask(splitsSyncHelper, splitsStorage, ruleBasedSegmentStorage, splitsFilterQueryString, telemetryRuntimeProducer, null, 1); } private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull SplitsStorage splitsStorage, + @NonNull RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, String splitsFilterQueryString, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer, @Nullable ISplitEventsManager eventsManager, @@ -52,6 +57,7 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, mSplitsStorage = checkNotNull(splitsStorage); mSplitsSyncHelper = checkNotNull(splitsSyncHelper); + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); mSplitsFilterQueryStringFromConfig = splitsFilterQueryString; mEventsManager = eventsManager; mChangeChecker = new SplitsChangeChecker(); @@ -63,6 +69,7 @@ private SplitsSyncTask(@NonNull SplitsSyncHelper splitsSyncHelper, @NonNull public SplitTaskExecutionInfo execute() { long storedChangeNumber = mSplitsStorage.getTill(); + long storedRbsChangeNumber = mRuleBasedSegmentStorage.getChangeNumber(); boolean splitsFilterHasChanged = splitsFilterHasChanged(mSplitsStorage.getSplitsFilterQueryString()); @@ -72,7 +79,7 @@ public SplitTaskExecutionInfo execute() { } long startTime = System.currentTimeMillis(); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(storedChangeNumber, + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(new SplitsSyncHelper.SinceChangeNumbers(storedChangeNumber, storedRbsChangeNumber), splitsFilterHasChanged, splitsFilterHasChanged, mOnDemandFetchBackoffMaxRetries); mTelemetryRuntimeProducer.recordSyncLatency(OperationType.SPLITS, System.currentTimeMillis() - startTime); @@ -88,7 +95,7 @@ public SplitTaskExecutionInfo execute() { private void notifyInternalEvent(long storedChangeNumber) { if (mEventsManager != null) { SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; - if (mChangeChecker.splitsHaveChanged(storedChangeNumber, mSplitsStorage.getTill())) { + if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill())) { event = SplitInternalEvent.SPLITS_UPDATED; } diff --git a/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java b/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java index 1deca7b5c..8f0a7cf61 100644 --- a/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java +++ b/src/main/java/io/split/android/client/service/splits/SplitsUpdateTask.java @@ -3,6 +3,7 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.split.android.client.events.ISplitEventsManager; @@ -13,24 +14,33 @@ import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.synchronizer.SplitsChangeChecker; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.utils.logger.Logger; public class SplitsUpdateTask implements SplitTask { private final SplitsStorage mSplitsStorage; - private final Long mChangeNumber; + @Nullable + private Long mChangeNumber; + @Nullable + private Long mRbsChangeNumber; private final SplitsSyncHelper mSplitsSyncHelper; private final ISplitEventsManager mEventsManager; private SplitsChangeChecker mChangeChecker; + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; public SplitsUpdateTask(SplitsSyncHelper splitsSyncHelper, SplitsStorage splitsStorage, - long since, - ISplitEventsManager eventsManager) { + RuleBasedSegmentStorage ruleBasedSegmentStorage, + @Nullable Long since, + @Nullable Long rbsSince, + @NonNull ISplitEventsManager eventsManager) { mSplitsStorage = checkNotNull(splitsStorage); + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); mSplitsSyncHelper = checkNotNull(splitsSyncHelper); mChangeNumber = since; + mRbsChangeNumber = rbsSince; mEventsManager = checkNotNull(eventsManager); mChangeChecker = new SplitsChangeChecker(); } @@ -40,21 +50,26 @@ public SplitsUpdateTask(SplitsSyncHelper splitsSyncHelper, public SplitTaskExecutionInfo execute() { if (mChangeNumber == null || mChangeNumber == 0) { - Logger.e("Could not update split. Invalid change number " + mChangeNumber); - return SplitTaskExecutionInfo.error(SplitTaskType.SPLITS_SYNC); + mChangeNumber = mSplitsStorage.getTill(); + } + + if (mRbsChangeNumber == null || mRbsChangeNumber == 0) { + mRbsChangeNumber = mRuleBasedSegmentStorage.getChangeNumber(); } long storedChangeNumber = mSplitsStorage.getTill(); - if (mChangeNumber <= storedChangeNumber) { - Logger.d("Received change number is previous than stored one. " + + long storedRbsChangeNumber = mRuleBasedSegmentStorage.getChangeNumber(); + if (mChangeNumber <= storedChangeNumber && mRbsChangeNumber <= storedRbsChangeNumber) { + Logger.d("Received change numbers are previous than stored ones. " + "Avoiding update."); return SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC); } - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(mChangeNumber, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(new SplitsSyncHelper.SinceChangeNumbers(mChangeNumber, mRbsChangeNumber), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); if (result.getStatus() == SplitTaskExecutionStatus.SUCCESS) { SplitInternalEvent event = SplitInternalEvent.SPLITS_FETCHED; - if (mChangeChecker.splitsHaveChanged(storedChangeNumber, mSplitsStorage.getTill())) { + if (mChangeChecker.changeNumberIsNewer(storedChangeNumber, mSplitsStorage.getTill()) || + mChangeChecker.changeNumberIsNewer(storedRbsChangeNumber, mRuleBasedSegmentStorage.getChangeNumber())) { event = SplitInternalEvent.SPLITS_UPDATED; } mEventsManager.notifyInternalEvent(event); diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java new file mode 100644 index 000000000..cff8533d7 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/InstantUpdateChangeNotification.java @@ -0,0 +1,62 @@ +package io.split.android.client.service.sseclient.notifications; + +import androidx.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +import io.split.android.client.common.CompressionType; + +public abstract class InstantUpdateChangeNotification extends IncomingNotification { + + @SerializedName("changeNumber") + private long changeNumber; + + @SerializedName("pcn") + @Nullable + private Long previousChangeNumber; + + @SerializedName("d") + @Nullable + private String data; + + @SerializedName("c") + @Nullable + private Integer compressionType; + + InstantUpdateChangeNotification() { + + } + + InstantUpdateChangeNotification(long changeNumber) { + this.changeNumber = changeNumber; + } + + public long getChangeNumber() { + return changeNumber; + } + + @Nullable + public Long getPreviousChangeNumber() { + return previousChangeNumber; + } + + @Nullable + public String getData() { + return data; + } + + @Nullable + public CompressionType getCompressionType() { + if (compressionType != null) { + if (compressionType == 0) { + return CompressionType.NONE; + } else if (compressionType == 1) { + return CompressionType.GZIP; + } else if (compressionType == 2) { + return CompressionType.ZLIB; + } + } + + return null; + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java index 3a4dbb8bc..d00b0341a 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationParser.java @@ -37,7 +37,7 @@ public IncomingNotification parseIncoming(String jsonData) throws JsonSyntaxExce Logger.e("Error parsing notification: " + e.getLocalizedMessage()); return null; } catch (Exception e) { - Logger.e("Unexpected error while parsing incomming notification: " + e.getLocalizedMessage()); + Logger.e("Unexpected error while parsing incoming notification: " + e.getLocalizedMessage()); return null; } @@ -50,6 +50,11 @@ public SplitsChangeNotification parseSplitUpdate(String jsonData) throws JsonSyn return Json.fromJson(jsonData, SplitsChangeNotification.class); } + @NonNull + public RuleBasedSegmentChangeNotification parseRuleBasedSegmentUpdate(String notificationJson) { + return Json.fromJson(notificationJson, RuleBasedSegmentChangeNotification.class); + } + @NonNull public SplitKillNotification parseSplitKill(String jsonData) throws JsonSyntaxException { return Json.fromJson(jsonData, SplitKillNotification.class); diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java index 6eef5c597..a5b2ff7fc 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationProcessor.java @@ -23,14 +23,14 @@ public class NotificationProcessor implements MySegmentsNotificationProcessorReg private final NotificationParser mNotificationParser; private final SplitTaskExecutor mSplitTaskExecutor; private final SplitTaskFactory mSplitTaskFactory; - private final BlockingQueue mSplitsUpdateNotificationsQueue; + private final BlockingQueue mSplitsUpdateNotificationsQueue; private final ConcurrentMap mMembershipsNotificationProcessors; public NotificationProcessor( @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull SplitTaskFactory splitTaskFactory, @NonNull NotificationParser notificationParser, - @NonNull BlockingQueue splitsUpdateNotificationsQueue) { + @NonNull BlockingQueue splitsUpdateNotificationsQueue) { mSplitTaskExecutor = checkNotNull(splitTaskExecutor); mSplitTaskFactory = checkNotNull(splitTaskFactory); mNotificationParser = checkNotNull(notificationParser); @@ -45,6 +45,9 @@ public void process(IncomingNotification incomingNotification) { case SPLIT_UPDATE: processSplitUpdate(mNotificationParser.parseSplitUpdate(notificationJson)); break; + case RULE_BASED_SEGMENT_UPDATE: + processRuleBasedSegmentUpdate(mNotificationParser.parseRuleBasedSegmentUpdate(notificationJson)); + break; case SPLIT_KILL: processSplitKill(mNotificationParser.parseSplitKill(notificationJson)); break; @@ -75,7 +78,12 @@ public void unregisterMembershipsProcessor(String matchingKey) { } private void processSplitUpdate(SplitsChangeNotification notification) { - Logger.d("Received split change notification"); + Logger.d("Received feature flag change notification"); + mSplitsUpdateNotificationsQueue.offer(notification); + } + + private void processRuleBasedSegmentUpdate(RuleBasedSegmentChangeNotification notification) { + Logger.d("Received rule based segment change notification"); mSplitsUpdateNotificationsQueue.offer(notification); } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java index fb38d5859..c02a4fcfd 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/NotificationType.java @@ -18,4 +18,7 @@ public enum NotificationType { MEMBERSHIPS_LS_UPDATE, @SerializedName("MEMBERSHIPS_MS_UPDATE") MEMBERSHIPS_MS_UPDATE, + + @SerializedName("RB_SEGMENT_UPDATE") + RULE_BASED_SEGMENT_UPDATE, } diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java new file mode 100644 index 000000000..5eb1e68a4 --- /dev/null +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/RuleBasedSegmentChangeNotification.java @@ -0,0 +1,8 @@ +package io.split.android.client.service.sseclient.notifications; + +public class RuleBasedSegmentChangeNotification extends InstantUpdateChangeNotification { + + public RuleBasedSegmentChangeNotification(long changeNumber) { + super(changeNumber); + } +} diff --git a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java b/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java index 3c22888fe..912a612bc 100644 --- a/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java +++ b/src/main/java/io/split/android/client/service/sseclient/notifications/SplitsChangeNotification.java @@ -1,62 +1,8 @@ package io.split.android.client.service.sseclient.notifications; -import androidx.annotation.Nullable; - -import com.google.gson.annotations.SerializedName; - -import io.split.android.client.common.CompressionType; - -public class SplitsChangeNotification extends IncomingNotification { - - @SerializedName("changeNumber") - private long changeNumber; - - @SerializedName("pcn") - @Nullable - private Long previousChangeNumber; - - @SerializedName("d") - @Nullable - private String data; - - @SerializedName("c") - @Nullable - private Integer compressionType; - - public SplitsChangeNotification() { - - } +public class SplitsChangeNotification extends InstantUpdateChangeNotification { public SplitsChangeNotification(long changeNumber) { - this.changeNumber = changeNumber; - } - - public long getChangeNumber() { - return changeNumber; - } - - @Nullable - public Long getPreviousChangeNumber() { - return previousChangeNumber; - } - - @Nullable - public String getData() { - return data; - } - - @Nullable - public CompressionType getCompressionType() { - if (compressionType != null) { - if (compressionType == 0) { - return CompressionType.NONE; - } else if (compressionType == 1) { - return CompressionType.GZIP; - } else if (compressionType == 2) { - return CompressionType.ZLIB; - } - } - - return null; + super(changeNumber); } } diff --git a/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java b/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java index ef932609a..44f4a1c1b 100644 --- a/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java +++ b/src/main/java/io/split/android/client/service/sseclient/reactor/SplitUpdatesWorker.java @@ -9,14 +9,18 @@ import java.util.concurrent.BlockingQueue; import io.split.android.client.common.CompressionUtilProvider; +import io.split.android.client.dtos.Helper; +import io.split.android.client.dtos.RuleBasedSegment; import io.split.android.client.dtos.Split; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; -import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; +import io.split.android.client.service.sseclient.notifications.InstantUpdateChangeNotification; +import io.split.android.client.service.sseclient.notifications.NotificationType; import io.split.android.client.service.synchronizer.Synchronizer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.utils.Base64Util; import io.split.android.client.utils.CompressionUtil; @@ -28,24 +32,26 @@ public class SplitUpdatesWorker extends UpdateWorker { /*** * This class will be in charge of update splits when a new notification arrived. */ - - private final BlockingQueue mNotificationsQueue; + private final BlockingQueue mNotificationsQueue; private final Synchronizer mSynchronizer; private final SplitsStorage mSplitsStorage; + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; private final CompressionUtilProvider mCompressionUtilProvider; private final SplitTaskExecutor mSplitTaskExecutor; private final SplitTaskFactory mSplitTaskFactory; private final Base64Decoder mBase64Decoder; public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, - @NonNull BlockingQueue notificationsQueue, + @NonNull BlockingQueue notificationsQueue, @NonNull SplitsStorage splitsStorage, + @NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage, @NonNull CompressionUtilProvider compressionUtilProvider, @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull SplitTaskFactory splitTaskFactory) { this(synchronizer, notificationsQueue, splitsStorage, + ruleBasedSegmentStorage, compressionUtilProvider, splitTaskExecutor, splitTaskFactory, @@ -54,8 +60,9 @@ public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, @VisibleForTesting public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, - @NonNull BlockingQueue notificationsQueue, + @NonNull BlockingQueue notificationsQueue, @NonNull SplitsStorage splitsStorage, + @NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage, @NonNull CompressionUtilProvider compressionUtilProvider, @NonNull SplitTaskExecutor splitTaskExecutor, @NonNull SplitTaskFactory splitTaskFactory, @@ -64,6 +71,7 @@ public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, mSynchronizer = checkNotNull(synchronizer); mNotificationsQueue = checkNotNull(notificationsQueue); mSplitsStorage = checkNotNull(splitsStorage); + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); mCompressionUtilProvider = checkNotNull(compressionUtilProvider); mSplitTaskExecutor = checkNotNull(splitTaskExecutor); mSplitTaskFactory = checkNotNull(splitTaskFactory); @@ -73,17 +81,19 @@ public SplitUpdatesWorker(@NonNull Synchronizer synchronizer, @Override protected void onWaitForNotificationLoop() throws InterruptedException { try { - SplitsChangeNotification notification = mNotificationsQueue.take(); - Logger.d("A new notification to update feature flags has been received"); + InstantUpdateChangeNotification notification = mNotificationsQueue.take(); + String type = notification.getType() == NotificationType.SPLIT_UPDATE ? "feature flags" : + "rule based segments"; + Logger.d("A new notification to update " + type + " has been received"); - long storageChangeNumber = mSplitsStorage.getTill(); + long storageChangeNumber = getStorageChangeNumber(notification.getType()); if (notification.getChangeNumber() <= storageChangeNumber) { - Logger.d("Notification change number is lower than the current one. Ignoring notification"); + Logger.d("Notification for " + type + " change number (" + notification.getChangeNumber() + ") is lower than the current one (" + storageChangeNumber + "). Ignoring notification"); return; } if (isLegacyNotification(notification) || isInvalidChangeNumber(notification, storageChangeNumber)) { - handleLegacyNotification(notification.getChangeNumber()); + handleLegacyNotification(notification); } else { handleNotification(notification); } @@ -93,50 +103,86 @@ protected void onWaitForNotificationLoop() throws InterruptedException { } } - private static boolean isInvalidChangeNumber(SplitsChangeNotification notification, long storageChangeNumber) { + private static boolean isInvalidChangeNumber(InstantUpdateChangeNotification notification, long storageChangeNumber) { return notification.getPreviousChangeNumber() == null || notification.getPreviousChangeNumber() == 0 || storageChangeNumber != notification.getPreviousChangeNumber(); } - private static boolean isLegacyNotification(SplitsChangeNotification notification) { + private static boolean isLegacyNotification(InstantUpdateChangeNotification notification) { return notification.getData() == null || notification.getCompressionType() == null; } - private void handleLegacyNotification(long changeNumber) { - mSynchronizer.synchronizeSplits(changeNumber); - Logger.d("Enqueuing polling task"); + private long getStorageChangeNumber(NotificationType type) { + return (type == NotificationType.RULE_BASED_SEGMENT_UPDATE) ? + mRuleBasedSegmentStorage.getChangeNumber() : + mSplitsStorage.getTill(); } - private void handleNotification(SplitsChangeNotification notification) { + private void handleNotification(InstantUpdateChangeNotification notification) { String decompressed = decompressData(notification.getData(), mCompressionUtilProvider.get(notification.getCompressionType())); if (decompressed == null) { - handleLegacyNotification(notification.getChangeNumber()); + handleLegacyNotification(notification); return; } try { + inPlaceUpdate(notification, decompressed); + } catch (Exception e) { + Logger.e("Could not parse instant update notification"); + handleLegacyNotification(notification); + } + } + + private void inPlaceUpdate(InstantUpdateChangeNotification notification, String decompressed) { + SplitTaskExecutionListener executionListener = new SplitTaskExecutionListener() { + @Override + public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { + if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { + handleLegacyNotification(notification); + } + } + }; + + if (notification.getType() == NotificationType.RULE_BASED_SEGMENT_UPDATE) { + RuleBasedSegment ruleBasedSegment = Json.fromJson(decompressed, RuleBasedSegment.class); + inPlaceRbsUpdate(notification, ruleBasedSegment, notification.getChangeNumber(), executionListener); + } else { Split split = Json.fromJson(decompressed, Split.class); + inPlaceSplitsUpdate(notification, split, notification.getChangeNumber(), executionListener); + } + } - mSplitTaskExecutor.submit( - mSplitTaskFactory.createSplitsUpdateTask(split, notification.getChangeNumber()), - new SplitTaskExecutionListener() { - @Override - public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { - if (taskInfo.getStatus() == SplitTaskExecutionStatus.ERROR) { - handleLegacyNotification(notification.getChangeNumber()); - } - } - }); - } catch (Exception e) { - Logger.e("Could not parse feature flag"); - handleLegacyNotification(notification.getChangeNumber()); + private void inPlaceRbsUpdate(InstantUpdateChangeNotification notification, RuleBasedSegment ruleBasedSegment, long changeNumber, SplitTaskExecutionListener executionListener) { + if (mRuleBasedSegmentStorage.contains(Helper.getReferencedRuleBasedSegments(ruleBasedSegment.getConditions()))) { + mSplitTaskExecutor.submit(mSplitTaskFactory.createRuleBasedSegmentUpdateTask(ruleBasedSegment, changeNumber), executionListener); + } else { + Logger.d("Referenced rule based segment not found in storage. Forcing sync"); + handleLegacyNotification(notification); + } + } + + private void inPlaceSplitsUpdate(InstantUpdateChangeNotification notification, Split split, long changeNumber, SplitTaskExecutionListener executionListener) { + if (mRuleBasedSegmentStorage.contains(Helper.getReferencedRuleBasedSegments(split.conditions))) { + mSplitTaskExecutor.submit(mSplitTaskFactory.createSplitsUpdateTask(split, changeNumber), executionListener); + } else { + Logger.d("Referenced rule based segment not found in storage. Forcing sync"); + handleLegacyNotification(notification); } } + private void handleLegacyNotification(InstantUpdateChangeNotification notification) { + if (notification.getType() == NotificationType.RULE_BASED_SEGMENT_UPDATE) { + mSynchronizer.synchronizeRuleBasedSegments(notification.getChangeNumber()); + } else { + mSynchronizer.synchronizeSplits(notification.getChangeNumber()); + } + Logger.d("Enqueuing polling task"); + } + @Nullable private String decompressData(String data, CompressionUtil compressionUtil) { try { diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java index 97ccf6b75..fa682d765 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/BackoffCounterTimer.java @@ -1,5 +1,7 @@ package io.split.android.client.service.sseclient.sseclient; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import io.split.android.client.service.executor.SplitTask; @@ -9,12 +11,10 @@ import io.split.android.client.service.sseclient.BackoffCounter; import io.split.android.client.utils.logger.Logger; -import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; - public class BackoffCounterTimer implements SplitTaskExecutionListener { - private SplitTaskExecutor mTaskExecutor; - private BackoffCounter mBackoffCounter; + private final SplitTaskExecutor mTaskExecutor; + private final BackoffCounter mBackoffCounter; private SplitTask mTask; String mTaskId; diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java index dd2f47071..c8b967d9a 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseHandler.java @@ -75,7 +75,6 @@ public void handleIncomingMessage(Map values) { if (incomingNotification == null) { return; } - switch (incomingNotification.getType()) { case CONTROL: handleControlNotification(incomingNotification); @@ -85,6 +84,7 @@ public void handleIncomingMessage(Map values) { break; case SPLIT_KILL: case SPLIT_UPDATE: + case RULE_BASED_SEGMENT_UPDATE: case MEMBERSHIPS_MS_UPDATE: case MEMBERSHIPS_LS_UPDATE: if (mNotificationManagerKeeper.isStreamingActive()) { diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java index 3f408eaae..88980ccfe 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/SseRefreshTokenTimer.java @@ -1,5 +1,7 @@ package io.split.android.client.service.sseclient.sseclient; +import static io.split.android.client.utils.Utils.checkNotNull; + import androidx.annotation.NonNull; import io.split.android.client.service.executor.SplitTask; @@ -12,8 +14,6 @@ import io.split.android.client.service.sseclient.feedbackchannel.PushStatusEvent.EventType; import io.split.android.client.utils.logger.Logger; -import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; - public class SseRefreshTokenTimer implements SplitTaskExecutionListener { private final static int RECONNECT_TIME_BEFORE_TOKEN_EXP_IN_SECONDS = 600; SplitTaskExecutor mTaskExecutor; @@ -36,7 +36,7 @@ public void schedule(long issueAtTime, long expirationTime) { @NonNull @Override public SplitTaskExecutionInfo execute() { - Logger.d("Informing sse token expired throught pushing retryable error."); + Logger.d("Informing sse token expired through pushing retryable error."); mBroadcasterChannel.pushMessage(new PushStatusEvent(EventType.PUSH_RETRYABLE_ERROR)); return SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK); } diff --git a/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java b/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java index fc05691aa..5117fd973 100644 --- a/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java +++ b/src/main/java/io/split/android/client/service/sseclient/sseclient/StreamingComponents.java @@ -3,15 +3,15 @@ import java.util.concurrent.BlockingQueue; import io.split.android.client.service.sseclient.feedbackchannel.PushManagerEventBroadcaster; +import io.split.android.client.service.sseclient.notifications.InstantUpdateChangeNotification; import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationProcessor; -import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.synchronizer.SyncGuardian; public class StreamingComponents { private PushNotificationManager mPushNotificationManager; - private BlockingQueue mSplitsUpdateNotificationQueue; + private BlockingQueue mSplitsUpdateNotificationQueue; private PushManagerEventBroadcaster mPushManagerEventBroadcaster; private NotificationParser mNotificationParser; private NotificationProcessor mNotificationProcessor; @@ -22,7 +22,7 @@ public StreamingComponents() { } public StreamingComponents(PushNotificationManager pushNotificationManager, - BlockingQueue splitsUpdateNotificationQueue, + BlockingQueue splitsUpdateNotificationQueue, NotificationParser notificationParser, NotificationProcessor notificationProcessor, SseAuthenticator sseAuthenticator, @@ -41,7 +41,7 @@ public PushNotificationManager getPushNotificationManager() { return mPushNotificationManager; } - public BlockingQueue getSplitsUpdateNotificationQueue() { + public BlockingQueue getSplitsUpdateNotificationQueue() { return mSplitsUpdateNotificationQueue; } diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java index 6b48eeccd..9c67ba5ba 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizer.java @@ -6,7 +6,7 @@ public interface FeatureFlagsSynchronizer { void loadAndSynchronize(); - void synchronize(long since); + void synchronize(Long since, Long rbsSince); void synchronize(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java index d29d98094..b7675e727 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImpl.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -87,6 +88,7 @@ public void taskExecuted(@NonNull SplitTaskExecutionInfo taskInfo) { public void loadAndSynchronize() { List enqueued = new ArrayList<>(); enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createFilterSplitsInCacheTask(), null)); + enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createLoadRuleBasedSegmentsTask(), null)); enqueued.add(new SplitTaskBatchItem(mSplitTaskFactory.createLoadSplitsTask(), mLoadLocalSplitsListener)); enqueued.add(new SplitTaskBatchItem(() -> { synchronize(); @@ -96,9 +98,9 @@ public void loadAndSynchronize() { } @Override - public void synchronize(long since) { + public void synchronize(Long since, Long rbsSince) { if (mIsSynchronizing.get()) { - mSplitsUpdateRetryTimer.setTask(mSplitTaskFactory.createSplitsUpdateTask(since), mSplitsSyncListener); + mSplitsUpdateRetryTimer.setTask(mSplitTaskFactory.createSplitsUpdateTask(since, rbsSince), mSplitsSyncListener); mSplitsUpdateRetryTimer.start(); } } @@ -130,8 +132,10 @@ public void stopSynchronization() { @Override public void submitLoadingTask(SplitTaskExecutionListener listener) { - mTaskExecutor.submit(mSplitTaskFactory.createLoadSplitsTask(), - listener); + mTaskExecutor.executeSerially(Arrays.asList( + new SplitTaskBatchItem(mSplitTaskFactory.createLoadRuleBasedSegmentsTask(), null), + new SplitTaskBatchItem(mSplitTaskFactory.createLoadSplitsTask(), + listener))); } private void scheduleSplitsFetcherTask() { diff --git a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java index 4f3f2a5d1..c2a6ce4ac 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/RolloutCacheManagerImpl.java @@ -41,7 +41,8 @@ public RolloutCacheManagerImpl(@NonNull SplitClientConfig splitClientConfig, encryptionMigrationTask, storageContainer.getSplitsStorage(), storageContainer.getMySegmentsStorageContainer(), - storageContainer.getMyLargeSegmentsStorageContainer()); + storageContainer.getMyLargeSegmentsStorageContainer(), + storageContainer.getRuleBasedSegmentStorage()); } @VisibleForTesting diff --git a/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java b/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java index 4d57dee9d..3410b4f41 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SplitsChangeChecker.java @@ -1,7 +1,7 @@ package io.split.android.client.service.synchronizer; public class SplitsChangeChecker { - public boolean splitsHaveChanged(long oldChangeNumber, long newChangeNumber) { + public boolean changeNumberIsNewer(long oldChangeNumber, long newChangeNumber) { return oldChangeNumber < newChangeNumber; } } \ No newline at end of file diff --git a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java index b16331b83..68bff6404 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java +++ b/src/main/java/io/split/android/client/service/synchronizer/Synchronizer.java @@ -16,6 +16,8 @@ public interface Synchronizer extends SplitLifecycleAware { void synchronizeSplits(); + void synchronizeRuleBasedSegments(long changeNumber); + void synchronizeMySegments(); void startPeriodicFetching(); diff --git a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index 930f5bd9b..ddfd906e8 100644 --- a/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -14,7 +14,6 @@ import io.split.android.client.dtos.Event; import io.split.android.client.events.ISplitEventsManager; import io.split.android.client.impressions.DecoratedImpression; -import io.split.android.client.impressions.Impression; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionListener; @@ -156,7 +155,12 @@ public void loadAndSynchronizeSplits() { @Override public void synchronizeSplits(long since) { - mFeatureFlagsSynchronizer.synchronize(since); + mFeatureFlagsSynchronizer.synchronize(since, null); + } + + @Override + public void synchronizeRuleBasedSegments(long changeNumber) { + mFeatureFlagsSynchronizer.synchronize(null, changeNumber); } @Override diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java b/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java index dd572e48a..e234f487f 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/FetcherProvider.java @@ -2,7 +2,7 @@ import java.net.URISyntaxException; -import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.network.HttpClient; import io.split.android.client.service.ServiceFactory; import io.split.android.client.service.http.HttpFetcher; @@ -17,7 +17,7 @@ class FetcherProvider { mEndpoint = endpoint; } - public HttpFetcher provideFetcher(String splitsFilterQueryString) throws URISyntaxException { + public HttpFetcher provideFetcher(String splitsFilterQueryString) throws URISyntaxException { return ServiceFactory.getSplitsFetcher(mHttpClient, mEndpoint, splitsFilterQueryString); } } diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java index d5f3f6d27..0d69c59f6 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorker.java @@ -6,6 +6,7 @@ import androidx.annotation.WorkerThread; import androidx.work.WorkerParameters; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.workmanager.SplitWorker; public class SplitsSyncWorker extends SplitWorker { @@ -21,6 +22,7 @@ public SplitsSyncWorker(@NonNull Context context, new StorageProvider(getDatabase(), params.apiKey(), params.encryptionEnabled(), params.shouldRecordTelemetry()), new FetcherProvider(getHttpClient(), getEndPoint()), new SplitChangeProcessorProvider().provideSplitChangeProcessor(params.configuredFilterType(), params.configuredFilterValues()), + new RuleBasedSegmentChangeProcessor(), new SyncHelperProvider(), params.flagsSpec()); diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java index 9d0a8a026..7ef5daaf4 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilder.java @@ -3,9 +3,12 @@ import java.net.URISyntaxException; import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.splits.SplitsSyncTask; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.utils.logger.Logger; @@ -18,18 +21,21 @@ class SplitsSyncWorkerTaskBuilder { private final StorageProvider mStorageProvider; private final FetcherProvider mFetcherProvider; private final SplitChangeProcessor mSplitChangeProcessor; + private final RuleBasedSegmentChangeProcessor mRuleBasedSegmentChangeProcessor; private final SyncHelperProvider mSplitsSyncHelperProvider; private final String mFlagsSpec; SplitsSyncWorkerTaskBuilder(StorageProvider storageProvider, FetcherProvider fetcherProvider, SplitChangeProcessor splitChangeProcessor, + RuleBasedSegmentChangeProcessor ruleBasedSegmentChangeProcessor, SyncHelperProvider splitsSyncHelperProvider, String flagsSpec) { mStorageProvider = storageProvider; mFetcherProvider = fetcherProvider; mSplitsSyncHelperProvider = splitsSyncHelperProvider; mSplitChangeProcessor = splitChangeProcessor; + mRuleBasedSegmentChangeProcessor = ruleBasedSegmentChangeProcessor; mFlagsSpec = flagsSpec; } @@ -37,17 +43,23 @@ SplitTask getTask() { try { SplitsStorage splitsStorage = mStorageProvider.provideSplitsStorage(); TelemetryStorage telemetryStorage = mStorageProvider.provideTelemetryStorage(); + RuleBasedSegmentStorageProducer ruleBasedSegmentStorageProducer = mStorageProvider.provideRuleBasedSegmentStorage(); + GeneralInfoStorage generalInfoStorage = mStorageProvider.provideGeneralInfoStorage(); String splitsFilterQueryString = splitsStorage.getSplitsFilterQueryString(); SplitsSyncHelper splitsSyncHelper = mSplitsSyncHelperProvider.provideSplitsSyncHelper( mFetcherProvider.provideFetcher(splitsFilterQueryString), splitsStorage, mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, + ruleBasedSegmentStorageProducer, + generalInfoStorage, telemetryStorage, mFlagsSpec); return SplitsSyncTask.buildForBackground(splitsSyncHelper, splitsStorage, + ruleBasedSegmentStorageProducer, splitsFilterQueryString, telemetryStorage); } catch (URISyntaxException e) { diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java b/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java index 75a2bef5f..d60e81967 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/StorageProvider.java @@ -1,26 +1,28 @@ package io.split.android.client.service.workmanager.splits; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.cipher.SplitCipherFactory; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.db.StorageFactory; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; class StorageProvider { private final SplitRoomDatabase mDatabase; - private final String mApiKey; - private final boolean mEncryptionEnabled; private final boolean mShouldRecordTelemetry; + private final SplitCipher mCipher; StorageProvider(SplitRoomDatabase database, String apiKey, boolean encryptionEnabled, boolean shouldRecordTelemetry) { mDatabase = database; - mApiKey = apiKey; - mEncryptionEnabled = encryptionEnabled; + mCipher = SplitCipherFactory.create(apiKey, encryptionEnabled); mShouldRecordTelemetry = shouldRecordTelemetry; } SplitsStorage provideSplitsStorage() { - SplitsStorage splitsStorageForWorker = StorageFactory.getSplitsStorageForWorker(mDatabase, mApiKey, mEncryptionEnabled); + SplitsStorage splitsStorageForWorker = StorageFactory.getSplitsStorage(mDatabase, mCipher); splitsStorageForWorker.loadLocal(); // call loadLocal to populate storage with DB data return splitsStorageForWorker; @@ -29,4 +31,15 @@ SplitsStorage provideSplitsStorage() { TelemetryStorage provideTelemetryStorage() { return StorageFactory.getTelemetryStorage(mShouldRecordTelemetry); } + + RuleBasedSegmentStorageProducer provideRuleBasedSegmentStorage() { + RuleBasedSegmentStorageProducer ruleBasedSegmentStorageForWorker = StorageFactory.getRuleBasedSegmentStorageForWorker(mDatabase, mCipher); + ruleBasedSegmentStorageForWorker.loadLocal(); // call loadLocal to populate storage with DB data + + return ruleBasedSegmentStorageForWorker; + } + + GeneralInfoStorage provideGeneralInfoStorage() { + return StorageFactory.getGeneralInfoStorage(mDatabase); + } } diff --git a/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java b/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java index 427b690e5..2fd411dbd 100644 --- a/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java +++ b/src/main/java/io/split/android/client/service/workmanager/splits/SyncHelperProvider.java @@ -1,21 +1,33 @@ package io.split.android.client.service.workmanager.splits; -import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.service.http.HttpFetcher; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; class SyncHelperProvider { - SplitsSyncHelper provideSplitsSyncHelper(HttpFetcher splitsFetcher, SplitsStorage splitsStorage, - SplitChangeProcessor mSplitChangeProcessor, - TelemetryStorage telemetryStorage, - String mFlagsSpec) { - return new SplitsSyncHelper(splitsFetcher, splitsStorage, - mSplitChangeProcessor, + SplitsSyncHelper provideSplitsSyncHelper(HttpFetcher splitsFetcher, + SplitsStorage splitsStorage, + SplitChangeProcessor splitChangeProcessor, + RuleBasedSegmentChangeProcessor ruleBasedSegmentChangeProcessor, + RuleBasedSegmentStorageProducer ruleBasedSegmentStorage, + GeneralInfoStorage generalInfoStorage, + TelemetryStorage telemetryStorage, + String mFlagsSpec) { + return new SplitsSyncHelper(splitsFetcher, + splitsStorage, + splitChangeProcessor, + ruleBasedSegmentChangeProcessor, + ruleBasedSegmentStorage, + generalInfoStorage, telemetryStorage, - mFlagsSpec); + mFlagsSpec, + true); } } diff --git a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java index 6a4f28e97..43d81074b 100644 --- a/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java +++ b/src/main/java/io/split/android/client/shared/SplitClientContainerImpl.java @@ -36,6 +36,7 @@ import io.split.android.client.telemetry.TelemetrySynchronizer; import io.split.android.client.validators.KeyValidator; import io.split.android.client.validators.ValidationMessageLogger; +import io.split.android.engine.experiments.SplitParser; public final class SplitClientContainerImpl extends BaseSplitClientContainer { @@ -73,7 +74,8 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, @NonNull ClientComponentsRegister clientComponentsRegister, @NonNull MySegmentsWorkManagerWrapper workManagerWrapper, @NonNull SplitFactoryImpl.EventsTrackerProvider eventsTrackerProvider, - @Nullable FlagSetsFilter flagSetsFilter) { + @Nullable FlagSetsFilter flagSetsFilter, + @NonNull SplitParser splitParser) { mDefaultMatchingKey = checkNotNull(defaultMatchingKey); mPushNotificationManager = pushNotificationManager; mStreamingEnabled = config.streamingEnabled(); @@ -92,7 +94,8 @@ public SplitClientContainerImpl(@NonNull String defaultMatchingKey, keyValidator, eventsTrackerProvider, customerImpressionListener, - flagSetsFilter + flagSetsFilter, + splitParser ); mClientComponentsRegister = checkNotNull(clientComponentsRegister); mSplitTaskExecutor = checkNotNull(splitTaskExecutor); diff --git a/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java b/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java index 912fafa97..bc7dfcb5c 100644 --- a/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java +++ b/src/main/java/io/split/android/client/storage/RolloutDefinitionsCache.java @@ -2,5 +2,7 @@ public interface RolloutDefinitionsCache { + void loadLocal(); + void clear(); } diff --git a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java index 8efd4c2ef..8c756db7e 100644 --- a/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java +++ b/src/main/java/io/split/android/client/storage/cipher/ApplyCipherTask.java @@ -28,6 +28,8 @@ import io.split.android.client.storage.db.attributes.AttributesEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; import io.split.android.client.utils.logger.Logger; public class ApplyCipherTask implements SplitTask { @@ -52,13 +54,14 @@ public SplitTaskExecutionInfo execute() { @Override public void run() { updateAttributes(mSplitDatabase.attributesDao()); - updateSplits(mSplitDatabase.splitDao(), mSplitDatabase.generalInfoDao()); + updateSplits(mSplitDatabase, mSplitDatabase.generalInfoDao()); updateSegments(mSplitDatabase.mySegmentDao()); updateLargeSegments(mSplitDatabase.myLargeSegmentDao()); updateImpressions(mSplitDatabase.impressionDao()); updateEvents(mSplitDatabase.eventDao()); updateImpressionsCount(mSplitDatabase.impressionsCountDao()); updateUniqueKeys(mSplitDatabase.uniqueKeysDao()); + updateRuleBasedSegment(mSplitDatabase.ruleBasedSegmentDao()); } }); @@ -68,6 +71,29 @@ public void run() { } } + private void updateRuleBasedSegment(RuleBasedSegmentDao ruleBasedSegmentDao) { + List items = ruleBasedSegmentDao.getAll(); + + if (items == null) { + return; + } + + for (RuleBasedSegmentEntity item : items) { + String name = item.getName(); + String fromName = mFromCipher.decrypt(name); + String fromBody = mFromCipher.decrypt(item.getBody()); + + String toName = mToCipher.encrypt(fromName); + String toBody = mToCipher.encrypt(fromBody); + + if (toName != null && toBody != null) { + ruleBasedSegmentDao.update(name, toName, toBody); + } else { + Logger.e("Error applying cipher to rule based segment storage"); + } + } + } + private void updateAttributes(AttributesDao attributesDao) { List items = attributesDao.getAll(); @@ -96,7 +122,7 @@ private void updateUniqueKeys(UniqueKeysDao uniqueKeysDao) { String toUserKey = mToCipher.encrypt(fromUserKey); String toFeatureList = mToCipher.encrypt(fromFeatureList); - if (toFeatureList != null) { + if (toUserKey != null && toFeatureList != null) { item.setUserKey(toUserKey); item.setFeatureList(toFeatureList); uniqueKeysDao.insert(item); @@ -189,9 +215,10 @@ private void updateEvents(EventDao eventDao) { } } - private void updateSplits(SplitDao dao, GeneralInfoDao generalInfoDao) { + private void updateSplits(SplitRoomDatabase splitDatabase, GeneralInfoDao generalInfoDao) { + SplitDao dao = splitDatabase.splitDao(); List items = dao.getAll(); - + splitDatabase.getSplitQueryDao().invalidate(); for (SplitEntity item : items) { String name = item.getName(); String fromName = mFromCipher.decrypt(name); @@ -208,7 +235,7 @@ private void updateSplits(SplitDao dao, GeneralInfoDao generalInfoDao) { } GeneralInfoEntity trafficTypesEntity = generalInfoDao.getByName(GeneralInfoEntity.TRAFFIC_TYPES_MAP); - if (trafficTypesEntity != null) { + if (trafficTypesEntity != null && !trafficTypesEntity.getStringValue().isEmpty()) { String fromTrafficTypes = mFromCipher.decrypt(trafficTypesEntity.getStringValue()); String toTrafficTypes = mToCipher.encrypt(fromTrafficTypes); if (toTrafficTypes != null) { @@ -219,7 +246,7 @@ private void updateSplits(SplitDao dao, GeneralInfoDao generalInfoDao) { } GeneralInfoEntity flagSetsEntity = generalInfoDao.getByName(GeneralInfoEntity.FLAG_SETS_MAP); - if (flagSetsEntity != null) { + if (flagSetsEntity != null && !flagSetsEntity.getStringValue().isEmpty()) { String fromFlagSets = mFromCipher.decrypt(flagSetsEntity.getStringValue()); String toFlagSets = mToCipher.encrypt(fromFlagSets); if (toFlagSets != null) { diff --git a/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java b/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java index ca4f1d7b5..d296a1045 100644 --- a/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java +++ b/src/main/java/io/split/android/client/storage/cipher/CBCCipher.java @@ -60,7 +60,7 @@ public String decrypt(String data) { return new String(bytes, CHARSET); } catch (Exception e) { - Logger.e("Error decrypting data: " + e.getMessage()); + Logger.e("Error decrypting data for source: " + data + " - " + e.getMessage()); return null; } finally { mCipherProvider.release(cipher); diff --git a/src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java b/src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java new file mode 100644 index 000000000..e9bffdbf9 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/common/RuleBasedSegmentStorageInitializer.java @@ -0,0 +1,45 @@ +package io.split.android.client.storage.common; + +import androidx.annotation.VisibleForTesting; + +import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.PersistentRuleBasedSegmentStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageImpl; +import io.split.android.engine.experiments.ParserCommons; +import io.split.android.engine.experiments.RuleBasedSegmentParser; + +class RuleBasedSegmentStorageInitializer { + + static Result initialize(MySegmentsStorageContainer mySegmentsStorageContainer, MySegmentsStorageContainer myLargeSegmentsStorageContainer, PersistentRuleBasedSegmentStorage persistentRuleBasedSegmentStorage) { + ParserCommons parserCommons = new ParserCommons( + mySegmentsStorageContainer, + myLargeSegmentsStorageContainer); + return initialize(parserCommons, + new RuleBasedSegmentStorageImpl(persistentRuleBasedSegmentStorage, new RuleBasedSegmentParser(parserCommons))); + } + + @VisibleForTesting + static Result initialize(ParserCommons parserCommons, RuleBasedSegmentStorage ruleBasedSegmentStorage) { + parserCommons.setRuleBasedSegmentStorage(ruleBasedSegmentStorage); + return new Result(ruleBasedSegmentStorage, parserCommons); + } + + static class Result { + private final RuleBasedSegmentStorage mRuleBasedSegmentStorage; + private final ParserCommons mParserCommons; + + Result(RuleBasedSegmentStorage ruleBasedSegmentStorage, ParserCommons parserCommons) { + mRuleBasedSegmentStorage = ruleBasedSegmentStorage; + mParserCommons = parserCommons; + } + + ParserCommons getParserCommons() { + return mParserCommons; + } + + RuleBasedSegmentStorage getRuleBasedSegmentStorage() { + return mRuleBasedSegmentStorage; + } + } +} diff --git a/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java b/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java index da911d720..197395c6b 100644 --- a/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java +++ b/src/main/java/io/split/android/client/storage/common/SplitStorageContainer.java @@ -3,6 +3,7 @@ import static io.split.android.client.utils.Utils.checkNotNull; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import io.split.android.client.service.impressions.observer.PersistentImpressionsObserverCacheStorage; import io.split.android.client.storage.attributes.AttributesStorage; @@ -17,9 +18,12 @@ import io.split.android.client.storage.impressions.PersistentImpressionsUniqueStorage; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.PersistentRuleBasedSegmentStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.PersistentSplitsStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; +import io.split.android.engine.experiments.ParserCommons; public class SplitStorageContainer { @@ -38,6 +42,10 @@ public class SplitStorageContainer { private final PersistentImpressionsUniqueStorage mPersistentImpressionsUniqueStorage; private final PersistentImpressionsObserverCacheStorage mPersistentImpressionsObserverCacheStorage; private final GeneralInfoStorage mGeneralInfoStorage; + @VisibleForTesting + final ParserCommons mParserCommons; + @VisibleForTesting + final RuleBasedSegmentStorage mRuleBasedSegmentStorage; public SplitStorageContainer(@NonNull SplitsStorage splitStorage, @NonNull MySegmentsStorageContainer mySegmentsStorageContainer, @@ -53,7 +61,8 @@ public SplitStorageContainer(@NonNull SplitsStorage splitStorage, @NonNull PersistentAttributesStorage persistentAttributesStorage, @NonNull TelemetryStorage telemetryStorage, @NonNull PersistentImpressionsObserverCacheStorage persistentImpressionsObserverCacheStorage, - @NonNull GeneralInfoStorage generalInfoStorage) { + @NonNull GeneralInfoStorage generalInfoStorage, + @NonNull PersistentRuleBasedSegmentStorage persistentRuleBasedSegmentStorage) { mSplitStorage = checkNotNull(splitStorage); mMySegmentsStorageContainer = checkNotNull(mySegmentsStorageContainer); @@ -70,6 +79,9 @@ public SplitStorageContainer(@NonNull SplitsStorage splitStorage, mPersistentImpressionsUniqueStorage = checkNotNull(persistentImpressionsUniqueStorage); mPersistentImpressionsObserverCacheStorage = checkNotNull(persistentImpressionsObserverCacheStorage); mGeneralInfoStorage = checkNotNull(generalInfoStorage); + RuleBasedSegmentStorageInitializer.Result initializerResult = RuleBasedSegmentStorageInitializer.initialize(mySegmentsStorageContainer, myLargeSegmentsStorageContainer, persistentRuleBasedSegmentStorage); + mParserCommons = initializerResult.getParserCommons(); + mRuleBasedSegmentStorage = initializerResult.getRuleBasedSegmentStorage(); } public SplitsStorage getSplitsStorage() { @@ -143,4 +155,12 @@ public PersistentImpressionsObserverCacheStorage getImpressionsObserverCachePers public GeneralInfoStorage getGeneralInfoStorage() { return mGeneralInfoStorage; } + + public ParserCommons getParserCommons() { + return mParserCommons; + } + + public RuleBasedSegmentStorage getRuleBasedSegmentStorage() { + return mRuleBasedSegmentStorage; + } } diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java b/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java index 784e6f863..82ddcc086 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java +++ b/src/main/java/io/split/android/client/storage/db/SplitQueryDao.java @@ -4,4 +4,6 @@ public interface SplitQueryDao { Map getAllAsMap(); + + void invalidate(); } \ No newline at end of file diff --git a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java b/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java index 42458f16e..215c35a9b 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java +++ b/src/main/java/io/split/android/client/storage/db/SplitQueryDaoImpl.java @@ -15,8 +15,9 @@ public class SplitQueryDaoImpl implements SplitQueryDao { private final SplitRoomDatabase mDatabase; private volatile Map mCachedSplitsMap; private final Object mLock = new Object(); - private boolean mIsInitialized = false; private final Thread mInitializationThread; + private boolean mIsInitialized = false; + private boolean mIsInvalidated = false; public SplitQueryDaoImpl(SplitRoomDatabase mDatabase) { this.mDatabase = mDatabase; @@ -27,7 +28,6 @@ public SplitQueryDaoImpl(SplitRoomDatabase mDatabase) { } catch (Exception ignore) { // Ignore } - long startTime = System.currentTimeMillis(); Map result = loadSplitsMap(); @@ -51,13 +51,13 @@ int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { public Map getAllAsMap() { // Fast path - if the map is already initialized, return it immediately - if (mIsInitialized && !mCachedSplitsMap.isEmpty()) { + if (isValid() && !mCachedSplitsMap.isEmpty()) { return new HashMap<>(mCachedSplitsMap); } // Wait for initialization to complete if it's in progress synchronized (mLock) { - if (mIsInitialized && !mCachedSplitsMap.isEmpty()) { + if (isValid() && !mCachedSplitsMap.isEmpty()) { return new HashMap<>(mCachedSplitsMap); } @@ -66,7 +66,7 @@ public Map getAllAsMap() { try { mLock.wait(5000); // Wait up to 5 seconds - if (mIsInitialized) { + if (isValid()) { return new HashMap<>(mCachedSplitsMap); } } catch (InterruptedException e) { @@ -85,6 +85,22 @@ public Map getAllAsMap() { return new HashMap<>(result); } } + + private boolean isValid() { + return mIsInitialized && !mIsInvalidated; + } + + @Override + public void invalidate() { + synchronized (mLock) { + if (mCachedSplitsMap != null) { + mCachedSplitsMap.clear(); + } + mIsInvalidated = true; + mLock.notifyAll(); + Logger.i("Invalidated preloaded flags"); + } + } /** * Internal method to load the splits map from the database. diff --git a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java index e17ee3db9..e0c54d7dd 100644 --- a/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java +++ b/src/main/java/io/split/android/client/storage/db/SplitRoomDatabase.java @@ -5,16 +5,13 @@ import android.content.Context; -import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.sqlite.db.SupportSQLiteDatabase; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; import io.split.android.client.storage.db.attributes.AttributesDao; import io.split.android.client.storage.db.attributes.AttributesEntity; @@ -23,15 +20,17 @@ import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity; import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao; import io.split.android.client.utils.logger.Logger; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; @Database( entities = { MySegmentEntity.class, SplitEntity.class, EventEntity.class, ImpressionEntity.class, GeneralInfoEntity.class, ImpressionsCountEntity.class, AttributesEntity.class, UniqueKeyEntity.class, ImpressionsObserverCacheEntity.class, - MyLargeSegmentEntity.class + MyLargeSegmentEntity.class, RuleBasedSegmentEntity.class }, - version = 6 + version = 7 ) public abstract class SplitRoomDatabase extends RoomDatabase { @@ -55,6 +54,8 @@ public abstract class SplitRoomDatabase extends RoomDatabase { public abstract ImpressionsObserverCacheDao impressionsObserverCacheDao(); + public abstract RuleBasedSegmentDao ruleBasedSegmentDao(); + private volatile SplitQueryDao mSplitQueryDao; private static volatile Map mInstances = new ConcurrentHashMap<>(); diff --git a/src/main/java/io/split/android/client/storage/db/StorageFactory.java b/src/main/java/io/split/android/client/storage/db/StorageFactory.java index 2960f73f7..6dc6e4c4d 100644 --- a/src/main/java/io/split/android/client/storage/db/StorageFactory.java +++ b/src/main/java/io/split/android/client/storage/db/StorageFactory.java @@ -4,7 +4,9 @@ import androidx.annotation.RestrictTo; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicLong; import io.split.android.client.service.ServiceConstants; import io.split.android.client.service.impressions.observer.PersistentImpressionsObserverCacheStorage; @@ -30,6 +32,10 @@ import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.storage.mysegments.MySegmentsStorageContainerImpl; import io.split.android.client.storage.mysegments.SqLitePersistentMySegmentsStorage; +import io.split.android.client.storage.rbs.PersistentRuleBasedSegmentStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducerImpl; +import io.split.android.client.storage.rbs.SqLitePersistentRuleBasedSegmentStorageProvider; import io.split.android.client.storage.splits.PersistentSplitsStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.storage.splits.SplitsStorageImpl; @@ -47,10 +53,6 @@ public static SplitsStorage getSplitsStorage(SplitRoomDatabase splitRoomDatabase return new SplitsStorageImpl(persistentSplitsStorage); } - public static SplitsStorage getSplitsStorageForWorker(SplitRoomDatabase splitRoomDatabase, String apiKey, boolean encryptionEnabled) { - return getSplitsStorage(splitRoomDatabase, SplitCipherFactory.create(apiKey, encryptionEnabled)); - } - public static MySegmentsStorageContainer getMySegmentsStorage(SplitRoomDatabase splitRoomDatabase, SplitCipher splitCipher) { return getMySegmentsStorageContainer(splitRoomDatabase, splitCipher); } @@ -154,4 +156,14 @@ public static PersistentImpressionsObserverCacheStorage getImpressionsObserverCa public static GeneralInfoStorage getGeneralInfoStorage(SplitRoomDatabase splitRoomDatabase) { return new GeneralInfoStorageImpl(splitRoomDatabase.generalInfoDao()); } + + public static PersistentRuleBasedSegmentStorage getPersistentRuleBasedSegmentStorage(SplitRoomDatabase splitRoomDatabase, SplitCipher splitCipher, GeneralInfoStorage generalInfoStorage) { + return new SqLitePersistentRuleBasedSegmentStorageProvider(splitCipher, splitRoomDatabase, generalInfoStorage).get(); + } + + public static RuleBasedSegmentStorageProducer getRuleBasedSegmentStorageForWorker(SplitRoomDatabase splitRoomDatabase, SplitCipher splitCipher) { + PersistentRuleBasedSegmentStorage persistentRuleBasedSegmentStorage = + new SqLitePersistentRuleBasedSegmentStorageProvider(splitCipher, splitRoomDatabase, getGeneralInfoStorage(splitRoomDatabase)).get(); + return new RuleBasedSegmentStorageProducerImpl(persistentRuleBasedSegmentStorage, new ConcurrentHashMap<>(), new AtomicLong(-1)); + } } diff --git a/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java b/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java new file mode 100644 index 000000000..658a8a3b4 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentDao.java @@ -0,0 +1,30 @@ +package io.split.android.client.storage.db.rbs; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import java.util.List; + +@Dao +public interface RuleBasedSegmentDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(List entities); + + @Insert(onConflict = OnConflictStrategy.REPLACE) + void insert(RuleBasedSegmentEntity entity); + + @Query("UPDATE rule_based_segments SET name = :name, body = :body WHERE name = :formerName") + void update(String formerName, String name, String body); + + @Query("DELETE FROM rule_based_segments WHERE name IN (:names)") + void delete(List names); + + @Query("SELECT name, body, updated_at FROM rule_based_segments") + List getAll(); + + @Query("DELETE FROM rule_based_segments") + void deleteAll(); +} diff --git a/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java b/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java new file mode 100644 index 000000000..60f4e05cf --- /dev/null +++ b/src/main/java/io/split/android/client/storage/db/rbs/RuleBasedSegmentEntity.java @@ -0,0 +1,61 @@ +package io.split.android.client.storage.db.rbs; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; + +@Entity(tableName = "rule_based_segments") +public class RuleBasedSegmentEntity { + + /** @noinspection NotNullFieldNotInitialized*/ + @PrimaryKey + @ColumnInfo(name = "name") + @NonNull + private String name; + + @ColumnInfo(name = "body") + private String body; + + @ColumnInfo(name = "updated_at") + private long updatedAt; + + /** @noinspection unused*/ + // room constructor + public RuleBasedSegmentEntity() { + + } + + @Ignore + public RuleBasedSegmentEntity(String name, String body, long updatedAt) { + this.name = name; + this.body = body; + this.updatedAt = updatedAt; + } + + public String getName() { + return name; + } + + public String getBody() { + return body; + } + + public long getUpdatedAt() { + return updatedAt; + } + + + public void setName(String name) { + this.name = name; + } + + public void setBody(String body) { + this.body = body; + } + + public void setUpdatedAt(long updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java index 7f8d763a9..87a6a55ec 100644 --- a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java +++ b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorage.java @@ -9,9 +9,13 @@ public interface GeneralInfoStorage { void setSplitsUpdateTimestamp(long timestamp); - long getChangeNumber(); + long getFlagsChangeNumber(); - void setChangeNumber(long changeNumber); + void setFlagsChangeNumber(long changeNumber); + + long getRbsChangeNumber(); + + void setRbsChangeNumber(long changeNumber); @NonNull String getSplitsFilterQueryString(); @@ -30,4 +34,8 @@ public interface GeneralInfoStorage { long getRolloutCacheLastClearTimestamp(); void setRolloutCacheLastClearTimestamp(long timestamp); + + void setLastProxyUpdateTimestamp(long timestamp); + + long getLastProxyUpdateTimestamp(); } diff --git a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java index b6c5c8423..c351d9a48 100644 --- a/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java +++ b/src/main/java/io/split/android/client/storage/general/GeneralInfoStorageImpl.java @@ -8,9 +8,11 @@ import io.split.android.client.storage.db.GeneralInfoDao; import io.split.android.client.storage.db.GeneralInfoEntity; -public class GeneralInfoStorageImpl implements GeneralInfoStorage{ +public class GeneralInfoStorageImpl implements GeneralInfoStorage { private static final String ROLLOUT_CACHE_LAST_CLEAR_TIMESTAMP = "rolloutCacheLastClearTimestamp"; + private static final String RBS_CHANGE_NUMBER = "rbsChangeNumber"; + private static final String LAST_PROXY_CHECK_TIMESTAMP = "lastProxyCheckTimestamp"; private final GeneralInfoDao mGeneralInfoDao; @@ -30,16 +32,27 @@ public void setSplitsUpdateTimestamp(long timestamp) { } @Override - public long getChangeNumber() { + public long getFlagsChangeNumber() { GeneralInfoEntity entity = mGeneralInfoDao.getByName(GeneralInfoEntity.CHANGE_NUMBER_INFO); return entity != null ? entity.getLongValue() : -1L; } @Override - public void setChangeNumber(long changeNumber) { + public void setFlagsChangeNumber(long changeNumber) { mGeneralInfoDao.update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, changeNumber)); } + @Override + public long getRbsChangeNumber() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(RBS_CHANGE_NUMBER); + return entity != null ? entity.getLongValue() : -1L; + } + + @Override + public void setRbsChangeNumber(long changeNumber) { + mGeneralInfoDao.update(new GeneralInfoEntity(RBS_CHANGE_NUMBER, changeNumber)); + } + @Override @NonNull public String getSplitsFilterQueryString() { @@ -85,4 +98,15 @@ public long getRolloutCacheLastClearTimestamp() { public void setRolloutCacheLastClearTimestamp(long timestamp) { mGeneralInfoDao.update(new GeneralInfoEntity(ROLLOUT_CACHE_LAST_CLEAR_TIMESTAMP, timestamp)); } + + @Override + public void setLastProxyUpdateTimestamp(long timestamp) { + mGeneralInfoDao.update(new GeneralInfoEntity(LAST_PROXY_CHECK_TIMESTAMP, timestamp)); + } + + @Override + public long getLastProxyUpdateTimestamp() { + GeneralInfoEntity entity = mGeneralInfoDao.getByName(LAST_PROXY_CHECK_TIMESTAMP); + return entity != null ? entity.getLongValue() : 0L; + } } diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java index 2675fc5f1..b86e12288 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorage.java @@ -6,7 +6,6 @@ import io.split.android.client.storage.RolloutDefinitionsCache; public interface MySegmentsStorage extends RolloutDefinitionsCache { - void loadLocal(); Set getAll(); diff --git a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java index 0a8f51f45..009d3d6f6 100644 --- a/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java +++ b/src/main/java/io/split/android/client/storage/mysegments/MySegmentsStorageContainerImpl.java @@ -42,6 +42,11 @@ public long getUniqueAmount() { return segments.size(); } + @Override + public void loadLocal() { + // no-op + } + @Override public void clear() { synchronized (lock) { diff --git a/src/main/java/io/split/android/client/storage/rbs/Clearer.java b/src/main/java/io/split/android/client/storage/rbs/Clearer.java new file mode 100644 index 000000000..1e0ab4a9d --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/Clearer.java @@ -0,0 +1,29 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.logger.Logger; + +class Clearer implements Runnable { + + private final RuleBasedSegmentDao mDao; + private final GeneralInfoStorage mGeneralInfoStorage; + + public Clearer(RuleBasedSegmentDao dao, GeneralInfoStorage generalInfoStorage) { + mDao = checkNotNull(dao); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + @Override + public void run() { + try { + mDao.deleteAll(); + mGeneralInfoStorage.setRbsChangeNumber(-1); + } catch (Exception e) { + Logger.e("Error clearing RBS: " + e.getLocalizedMessage()); + throw e; + } + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java b/src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java new file mode 100644 index 000000000..9a68ebb2b --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProvider.java @@ -0,0 +1,25 @@ +package io.split.android.client.storage.rbs; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.split.android.client.utils.logger.Logger; + +public class LazyRuleBasedSegmentStorageProvider implements RuleBasedSegmentStorageProvider { + + private final AtomicReference mRuleBasedSegmentStorageRef = new AtomicReference<>(); + + public void set(@NonNull RuleBasedSegmentStorage ruleBasedSegmentStorage) { + if (!mRuleBasedSegmentStorageRef.compareAndSet(null, ruleBasedSegmentStorage)) { + Logger.w("RuleBasedSegmentStorage already set in LazyRuleBasedSegmentStorageProvider"); + } + } + + @Nullable + @Override + public RuleBasedSegmentStorage get() { + return mRuleBasedSegmentStorageRef.get(); + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java b/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java new file mode 100644 index 000000000..e2bd35634 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/PersistentRuleBasedSegmentStorage.java @@ -0,0 +1,19 @@ +package io.split.android.client.storage.rbs; + +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; + +public interface PersistentRuleBasedSegmentStorage { + + RuleBasedSegmentSnapshot getSnapshot(); + + void update(Set toAdd, Set toRemove, long changeNumber); + + void clear(); + + interface Provider { + + PersistentRuleBasedSegmentStorage get(); + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java new file mode 100644 index 000000000..69f050398 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentSnapshot.java @@ -0,0 +1,29 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import java.util.Map; + +import io.split.android.client.dtos.RuleBasedSegment; + +public class RuleBasedSegmentSnapshot { + + private final Map mSegments; + + private final long mChangeNumber; + + public RuleBasedSegmentSnapshot(@NonNull Map segments, long changeNumber) { + mSegments = checkNotNull(segments); + mChangeNumber = changeNumber; + } + + public Map getSegments() { + return mSegments; + } + + public long getChangeNumber() { + return mChangeNumber; + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java new file mode 100644 index 000000000..0d76ba0b7 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorage.java @@ -0,0 +1,5 @@ +package io.split.android.client.storage.rbs; + +public interface RuleBasedSegmentStorage extends RuleBasedSegmentStorageConsumer, RuleBasedSegmentStorageProducer { + +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java new file mode 100644 index 000000000..908b424dd --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageConsumer.java @@ -0,0 +1,16 @@ +package io.split.android.client.storage.rbs; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; + +import io.split.android.engine.experiments.ParsedRuleBasedSegment; + +public interface RuleBasedSegmentStorageConsumer { + + @Nullable + ParsedRuleBasedSegment get(String segmentName, String matchingKey); + + boolean contains(@NonNull Set segmentNames); +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java new file mode 100644 index 000000000..1ff19057e --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImpl.java @@ -0,0 +1,85 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.engine.experiments.ParsedRuleBasedSegment; +import io.split.android.engine.experiments.RuleBasedSegmentParser; + +public class RuleBasedSegmentStorageImpl implements RuleBasedSegmentStorage { + + private final ConcurrentHashMap mInMemorySegments; + @Nullable + private final RuleBasedSegmentParser mParser; + private final RuleBasedSegmentStorageProducer mProducer; + + public RuleBasedSegmentStorageImpl(@NonNull PersistentRuleBasedSegmentStorage persistentStorage, @NonNull RuleBasedSegmentParser parser) { + mInMemorySegments = new ConcurrentHashMap<>(); + mParser = checkNotNull(parser); + mProducer = new RuleBasedSegmentStorageProducerImpl(persistentStorage, mInMemorySegments, new AtomicLong(-1)); + } + + @VisibleForTesting + RuleBasedSegmentStorageImpl(RuleBasedSegmentStorageProducer producer, + @NonNull RuleBasedSegmentParser parser, + @NonNull ConcurrentHashMap inMemorySegmentsMap) { + mInMemorySegments = checkNotNull(inMemorySegmentsMap); + mParser = checkNotNull(parser); + mProducer = checkNotNull(producer); + } + + @Override + public @Nullable ParsedRuleBasedSegment get(String segmentName, String matchingKey) { + RuleBasedSegment ruleBasedSegment = mInMemorySegments.get(segmentName); + if (ruleBasedSegment == null) { + return null; + } + + return mParser.parse(ruleBasedSegment, matchingKey); + } + + @Override + public synchronized boolean update(@NonNull Set toAdd, @NonNull Set toRemove, long changeNumber) { + return mProducer.update(toAdd, toRemove, changeNumber); + } + + @Override + public long getChangeNumber() { + return mProducer.getChangeNumber(); + } + + @Override + public boolean contains(@NonNull Set segmentNames) { + if (segmentNames == null) { + return false; + } + + for (String name : segmentNames) { + if (!mInMemorySegments.containsKey(name)) { + return false; + } + } + return true; + } + + @WorkerThread + @Override + public synchronized void loadLocal() { + mProducer.loadLocal(); + } + + @WorkerThread + @Override + public void clear() { + mProducer.clear(); + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java new file mode 100644 index 000000000..03928045a --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducer.java @@ -0,0 +1,15 @@ +package io.split.android.client.storage.rbs; + +import androidx.annotation.NonNull; + +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.RolloutDefinitionsCache; + +public interface RuleBasedSegmentStorageProducer extends RolloutDefinitionsCache { + + boolean update(@NonNull Set toAdd, @NonNull Set toRemove, long changeNumber); + + long getChangeNumber(); +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java new file mode 100644 index 000000000..2a855073b --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImpl.java @@ -0,0 +1,80 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import io.split.android.client.dtos.RuleBasedSegment; + +public class RuleBasedSegmentStorageProducerImpl implements RuleBasedSegmentStorageProducer { + + private final ConcurrentHashMap mInMemorySegments; + private final PersistentRuleBasedSegmentStorage mPersistentStorage; + private final AtomicLong mChangeNumberRef; + + public RuleBasedSegmentStorageProducerImpl(@NonNull PersistentRuleBasedSegmentStorage persistentStorage, + @NonNull ConcurrentHashMap segments, + @NonNull AtomicLong changeNumberRef) { + mPersistentStorage = checkNotNull(persistentStorage); + mInMemorySegments = checkNotNull(segments); + mChangeNumberRef = checkNotNull(changeNumberRef); + } + + @Override + public boolean update(@NonNull Set toAdd, @NonNull Set toRemove, long changeNumber) { + boolean appliedUpdates = false; + + if (toAdd != null) { + if (!toAdd.isEmpty()) { + for (RuleBasedSegment segment : toAdd) { + mInMemorySegments.put(segment.getName(), segment); + } + + appliedUpdates = true; + } + } else { + toAdd = new HashSet<>(); + } + + if (toRemove != null) { + if (!toRemove.isEmpty()) { + for (RuleBasedSegment segment : toRemove) { + mInMemorySegments.remove(segment.getName()); + } + } + } else { + toRemove = new HashSet<>(); + } + + mChangeNumberRef.set(changeNumber); + mPersistentStorage.update(toAdd, toRemove, changeNumber); + + return appliedUpdates; + } + + @Override + public void loadLocal() { + RuleBasedSegmentSnapshot snapshot = mPersistentStorage.getSnapshot(); + Map segments = snapshot.getSegments(); + mChangeNumberRef.set(snapshot.getChangeNumber()); + mInMemorySegments.putAll(segments); + } + + @Override + public void clear() { + mInMemorySegments.clear(); + mChangeNumberRef.set(-1); + mPersistentStorage.clear(); + } + + @Override + public long getChangeNumber() { + return mChangeNumberRef.get(); + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java new file mode 100644 index 000000000..4c4393a24 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProvider.java @@ -0,0 +1,9 @@ +package io.split.android.client.storage.rbs; + +import androidx.annotation.Nullable; + +public interface RuleBasedSegmentStorageProvider { + + @Nullable + RuleBasedSegmentStorage get(); +} diff --git a/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java b/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java new file mode 100644 index 000000000..3e32e40cd --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/SnapshotLoader.java @@ -0,0 +1,68 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +final class SnapshotLoader implements Callable { + + private final RuleBasedSegmentDao mDao; + private final SplitCipher mCipher; + private final GeneralInfoStorage mGeneralInfoStorage; + + SnapshotLoader(RuleBasedSegmentDao dao, SplitCipher cipher, GeneralInfoStorage generalInfoStorage) { + mDao = checkNotNull(dao); + mCipher = checkNotNull(cipher); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + @Override + public RuleBasedSegmentSnapshot call() { + try { + long changeNumber = mGeneralInfoStorage.getRbsChangeNumber(); + List entities = mDao.getAll(); + Map segments = convertToDTOs(entities); + + return new RuleBasedSegmentSnapshot(segments, changeNumber); + } catch (Exception e) { + Logger.e("Error loading RBS from persistent storage", e.getLocalizedMessage()); + throw e; + } + } + + @NonNull + private Map convertToDTOs(@Nullable List entities) { + Map segments = new HashMap<>(); + if (entities != null) { + for (RuleBasedSegmentEntity entity : entities) { + String name = mCipher.decrypt(entity.getName()); + String body = mCipher.decrypt(entity.getBody()); + if (name == null || body == null) { + continue; + } + + try { + RuleBasedSegment ruleBasedSegment = Json.fromJson(body, RuleBasedSegment.class); + segments.put(name, ruleBasedSegment); + } catch (Exception e) { + Logger.e("Error parsing RBS with name " + name + ": " + e.getLocalizedMessage()); + } + } + } + return segments; + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java new file mode 100644 index 000000000..63d09f09b --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorage.java @@ -0,0 +1,43 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.general.GeneralInfoStorage; + +class SqLitePersistentRuleBasedSegmentStorage implements PersistentRuleBasedSegmentStorage { + + private final RuleBasedSegmentDao mDao; + private final SplitRoomDatabase mDatabase; + private final GeneralInfoStorage mGeneralInfoStorage; + private final SplitCipher mCipher; + + public SqLitePersistentRuleBasedSegmentStorage(SplitCipher cipher, + SplitRoomDatabase database, + GeneralInfoStorage generalInfoStorage) { + mCipher = checkNotNull(cipher); + mDatabase = checkNotNull(database); + mDao = database.ruleBasedSegmentDao(); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + } + + @Override + public RuleBasedSegmentSnapshot getSnapshot() { + return mDatabase.runInTransaction(new SnapshotLoader(mDao, mCipher, mGeneralInfoStorage)); + } + + @Override + public void update(Set toAdd, Set toRemove, long changeNumber) { + mDatabase.runInTransaction(new Updater(mCipher, mDao, mGeneralInfoStorage, toAdd, toRemove, changeNumber)); + } + + @Override + public void clear() { + mDatabase.runInTransaction(new Clearer(mDao, mGeneralInfoStorage)); + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java new file mode 100644 index 000000000..eb5a279b6 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageProvider.java @@ -0,0 +1,19 @@ +package io.split.android.client.storage.rbs; + +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SqLitePersistentRuleBasedSegmentStorageProvider implements PersistentRuleBasedSegmentStorage.Provider { + + private final SqLitePersistentRuleBasedSegmentStorage mPersistentStorage; + + public SqLitePersistentRuleBasedSegmentStorageProvider(SplitCipher cipher, SplitRoomDatabase database, GeneralInfoStorage generalInfoStorage) { + mPersistentStorage = new SqLitePersistentRuleBasedSegmentStorage(cipher, database, generalInfoStorage); + } + + @Override + public PersistentRuleBasedSegmentStorage get() { + return mPersistentStorage; + } +} diff --git a/src/main/java/io/split/android/client/storage/rbs/Updater.java b/src/main/java/io/split/android/client/storage/rbs/Updater.java new file mode 100644 index 000000000..12e5c3fb2 --- /dev/null +++ b/src/main/java/io/split/android/client/storage/rbs/Updater.java @@ -0,0 +1,84 @@ +package io.split.android.client.storage.rbs; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.utils.Json; +import io.split.android.client.utils.logger.Logger; + +final class Updater implements Runnable { + + @NonNull + private final SplitCipher mCipher; + @NonNull + private final GeneralInfoStorage mGeneralInfoStorage; + @NonNull + private final RuleBasedSegmentDao mDao; + @NonNull + private final Set mToAdd; + @NonNull + private final Set mToRemove; + private final long mChangeNumber; + + Updater(@NonNull SplitCipher cipher, + @NonNull RuleBasedSegmentDao dao, + @NonNull GeneralInfoStorage generalInfoStorage, + @NonNull Set toAdd, + @NonNull Set toRemove, + long changeNumber) { + mCipher = checkNotNull(cipher); + mDao = checkNotNull(dao); + mGeneralInfoStorage = checkNotNull(generalInfoStorage); + mToAdd = checkNotNull(toAdd); + mToRemove = checkNotNull(toRemove); + mChangeNumber = changeNumber; + } + + @Override + public void run() { + try { + List toDelete = new ArrayList<>(); + for (RuleBasedSegment segment : mToRemove) { + String encryptedName = mCipher.encrypt(segment.getName()); + if (encryptedName != null) { + toDelete.add(encryptedName); + } + } + + List toAdd = new ArrayList<>(); + for (RuleBasedSegment segment : mToAdd) { + if (segment == null) { + continue; + } + + try { + String name = mCipher.encrypt(segment.getName()); + String body = mCipher.encrypt(Json.toJson(segment)); + if (name == null || body == null) { + continue; + } + toAdd.add(new RuleBasedSegmentEntity(name, body, System.currentTimeMillis())); + } catch (Exception e) { + Logger.e("Error parsing RBS with name " + segment.getName() + ": " + e.getLocalizedMessage()); + } + } + + mDao.delete(toDelete); + mDao.insert(toAdd); + mGeneralInfoStorage.setRbsChangeNumber(mChangeNumber); + } catch (Exception e) { + Logger.e("Error updating RBS: " + e.getLocalizedMessage()); + throw e; + } + } +} diff --git a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java index 81ff4e5a5..b74910f6b 100644 --- a/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SplitsStorage.java @@ -12,7 +12,6 @@ import io.split.android.client.storage.RolloutDefinitionsCache; public interface SplitsStorage extends RolloutDefinitionsCache { - void loadLocal(); Split get(@NonNull String name); diff --git a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java index 5bd250a14..ecffa0670 100644 --- a/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java +++ b/src/main/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorage.java @@ -11,12 +11,15 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; -import io.split.android.client.SplitFactoryImpl; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.Status; import io.split.android.client.service.executor.parallel.SplitParallelTaskExecutorFactory; @@ -28,10 +31,6 @@ import io.split.android.client.utils.Json; import io.split.android.client.utils.logger.Logger; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.util.concurrent.ConcurrentHashMap; - public class SqLitePersistentSplitsStorage implements PersistentSplitsStorage { private static final int SQL_PARAM_BIND_SIZE = 20; @@ -86,12 +85,14 @@ public void run() { mDatabase.splitDao().delete(removedSplits); } if (!mTrafficTypes.isEmpty()) { + String encryptedTrafficTypes = mCipher.encrypt(Json.toJson(mTrafficTypes)); mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, - Json.toJson(mTrafficTypes))); + encryptedTrafficTypes)); } if (!mFlagSets.isEmpty()) { + String encryptedFlagSets = mCipher.encrypt(Json.toJson(mFlagSets)); mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, - Json.toJson(mFlagSets))); + encryptedFlagSets)); } mDatabase.generalInfoDao().update( new GeneralInfoEntity(GeneralInfoEntity.SPLITS_UPDATE_TIMESTAMP, splitChange.getUpdateTimestamp())); @@ -160,6 +161,7 @@ public void run() { mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.CHANGE_NUMBER_INFO, -1)); mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, "")); mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, "")); + mDatabase.getSplitQueryDao().invalidate(); mDatabase.splitDao().deleteAll(); } }); @@ -179,10 +181,8 @@ public String getFilterQueryString() { private List loadSplits() { Map allNamesAndBodies = mDatabase.getSplitQueryDao().getAllAsMap(); - long transformStartTime = System.currentTimeMillis(); - List splits = mEntityToSplitTransformer.transform(allNamesAndBodies); - return splits; + return mEntityToSplitTransformer.transform(allNamesAndBodies); } private List convertSplitListToEntities(List splits) { @@ -259,14 +259,14 @@ public void run() { private synchronized void parseTrafficTypesAndSets(@Nullable GeneralInfoEntity trafficTypesEntity, @Nullable GeneralInfoEntity flagSetsEntity) { Logger.v("Parsing traffic types and sets"); - if (trafficTypesEntity != null) { + if (trafficTypesEntity != null && !trafficTypesEntity.getStringValue().isEmpty()) { Type mapType = new TypeToken>(){}.getType(); String encryptedTrafficTypes = trafficTypesEntity.getStringValue(); String decryptedTrafficTypes = mCipher.decrypt(encryptedTrafficTypes); mTrafficTypes = Json.fromJson(decryptedTrafficTypes, mapType); } - if (flagSetsEntity != null) { + if (flagSetsEntity != null && !flagSetsEntity.getStringValue().isEmpty()) { Type flagsMapType = new TypeToken>>(){}.getType(); String encryptedFlagSets = flagSetsEntity.getStringValue(); String decryptedFlagSets = mCipher.decrypt(encryptedFlagSets); @@ -279,25 +279,31 @@ private void migrateTrafficTypesAndSetsFromStoredData() { try { for (Split split : mSplits) { Split parsedSplit = Json.fromJson(split.json, Split.class); - if (parsedSplit != null && parsedSplit.status == Status.ACTIVE) { - increaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); - addOrUpdateFlagSets(parsedSplit, mFlagSets); - } else { - decreaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); - deleteFromFlagSetsIfNecessary(parsedSplit, mFlagSets); + if (parsedSplit != null) { + if (parsedSplit.status == Status.ACTIVE) { + increaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); + addOrUpdateFlagSets(parsedSplit, mFlagSets); + } else { + decreaseTrafficTypeCount(parsedSplit.trafficTypeName, mTrafficTypes); + deleteFromFlagSetsIfNecessary(parsedSplit, mFlagSets); + } } } // persist TTs - String decryptedTrafficTypes = Json.toJson(mTrafficTypes); - String encryptedTrafficTypes = mCipher.encrypt(decryptedTrafficTypes); + if (mTrafficTypes != null && !mTrafficTypes.isEmpty()) { + String decryptedTrafficTypes = Json.toJson(mTrafficTypes); + String encryptedTrafficTypes = mCipher.encrypt(decryptedTrafficTypes); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, encryptedTrafficTypes)); + } - // persist flag sets - String decryptedFlagSets = Json.toJson(mFlagSets); - String encryptedFlagSets = mCipher.encrypt(decryptedFlagSets); + if (mFlagSets != null && !mFlagSets.isEmpty()) { + // persist flag sets + String decryptedFlagSets = Json.toJson(mFlagSets); + String encryptedFlagSets = mCipher.encrypt(decryptedFlagSets); - mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.TRAFFIC_TYPES_MAP, encryptedTrafficTypes)); - mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, encryptedFlagSets)); + mDatabase.generalInfoDao().update(new GeneralInfoEntity(GeneralInfoEntity.FLAG_SETS_MAP, encryptedFlagSets)); + } } catch (Exception e) { Logger.e("Failed to migrate traffic types and flag sets", e); } diff --git a/src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java b/src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java new file mode 100644 index 000000000..0ebf0d6a3 --- /dev/null +++ b/src/main/java/io/split/android/engine/experiments/ParsedRuleBasedSegment.java @@ -0,0 +1,49 @@ +package io.split.android.engine.experiments; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.ExcludedSegment; + +public class ParsedRuleBasedSegment { + private final String mName; + private final Set mExcludedKeys; + private final Set mExcludedSegments; + private final List mParsedConditions; + private final String mTrafficTypeName; + private final long mChangeNumber; + + public ParsedRuleBasedSegment(String name, Set excludedKeys, Set excludedSegments, List parsedConditions, String trafficTypeName, long changeNumber) { + mName = name; + mExcludedKeys = excludedKeys == null ? new HashSet<>() : excludedKeys; + mExcludedSegments = excludedSegments == null ? new HashSet<>() : excludedSegments; + mParsedConditions = parsedConditions; + mTrafficTypeName = trafficTypeName; + mChangeNumber = changeNumber; + } + + public String getName() { + return mName; + } + + public Set getExcludedKeys() { + return mExcludedKeys; + } + + public Set getExcludedSegments() { + return mExcludedSegments; + } + + public List getParsedConditions() { + return mParsedConditions; + } + + public String getTrafficTypeName() { + return mTrafficTypeName; + } + + public long getChangeNumber() { + return mChangeNumber; + } +} \ No newline at end of file diff --git a/src/main/java/io/split/android/engine/experiments/Parser.java b/src/main/java/io/split/android/engine/experiments/Parser.java new file mode 100644 index 000000000..aaa8cdf74 --- /dev/null +++ b/src/main/java/io/split/android/engine/experiments/Parser.java @@ -0,0 +1,9 @@ +package io.split.android.engine.experiments; + +import androidx.annotation.Nullable; + +interface Parser { + + @Nullable + O parse(I input, String matchingKey); +} diff --git a/src/main/java/io/split/android/engine/experiments/ParserCommons.java b/src/main/java/io/split/android/engine/experiments/ParserCommons.java new file mode 100644 index 000000000..449f2cc20 --- /dev/null +++ b/src/main/java/io/split/android/engine/experiments/ParserCommons.java @@ -0,0 +1,252 @@ +package io.split.android.engine.experiments; + +import static io.split.android.client.utils.Utils.checkArgument; +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; + +import io.split.android.client.dtos.Condition; +import io.split.android.client.dtos.Matcher; +import io.split.android.client.dtos.MatcherGroup; +import io.split.android.client.dtos.Partition; +import io.split.android.client.storage.mysegments.EmptyMySegmentsStorage; +import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageConsumer; +import io.split.android.client.utils.logger.Logger; +import io.split.android.engine.matchers.AllKeysMatcher; +import io.split.android.engine.matchers.AttributeMatcher; +import io.split.android.engine.matchers.BetweenMatcher; +import io.split.android.engine.matchers.BooleanMatcher; +import io.split.android.engine.matchers.CombiningMatcher; +import io.split.android.engine.matchers.DependencyMatcher; +import io.split.android.engine.matchers.EqualToMatcher; +import io.split.android.engine.matchers.GreaterThanOrEqualToMatcher; +import io.split.android.engine.matchers.InRuleBasedSegmentMatcher; +import io.split.android.engine.matchers.LessThanOrEqualToMatcher; +import io.split.android.engine.matchers.MySegmentsMatcher; +import io.split.android.engine.matchers.collections.ContainsAllOfSetMatcher; +import io.split.android.engine.matchers.collections.ContainsAnyOfSetMatcher; +import io.split.android.engine.matchers.collections.EqualToSetMatcher; +import io.split.android.engine.matchers.collections.PartOfSetMatcher; +import io.split.android.engine.matchers.semver.BetweenSemverMatcher; +import io.split.android.engine.matchers.semver.EqualToSemverMatcher; +import io.split.android.engine.matchers.semver.GreaterThanOrEqualToSemverMatcher; +import io.split.android.engine.matchers.semver.InListSemverMatcher; +import io.split.android.engine.matchers.semver.LessThanOrEqualToSemverMatcher; +import io.split.android.engine.matchers.strings.ContainsAnyOfMatcher; +import io.split.android.engine.matchers.strings.EndsWithAnyOfMatcher; +import io.split.android.engine.matchers.strings.RegularExpressionMatcher; +import io.split.android.engine.matchers.strings.StartsWithAnyOfMatcher; +import io.split.android.engine.matchers.strings.WhitelistMatcher; + +public class ParserCommons { + + private static final int CONDITIONS_UPPER_LIMIT = 50; + + private final MySegmentsStorageContainer mMySegmentsStorageContainer; + private final MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; + private RuleBasedSegmentStorageConsumer mRuleBasedSegmentStorage; + private final DefaultConditionsProvider mDefaultConditionsProvider; + private EmptyMySegmentsStorage mEmptyMySegmentsStorage; + + public ParserCommons(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, + @NonNull MySegmentsStorageContainer myLargeSegmentsStorageContainer) { + this(mySegmentsStorageContainer, myLargeSegmentsStorageContainer, new DefaultConditionsProvider()); + } + + @VisibleForTesting + ParserCommons(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, + @NonNull MySegmentsStorageContainer myLargeSegmentsStorageContainer, + DefaultConditionsProvider defaultConditionsProvider) { + mMySegmentsStorageContainer = checkNotNull(mySegmentsStorageContainer); + mMyLargeSegmentsStorageContainer = checkNotNull(myLargeSegmentsStorageContainer); + mDefaultConditionsProvider = checkNotNull(defaultConditionsProvider); + } + + public void setRuleBasedSegmentStorage(@NonNull RuleBasedSegmentStorageConsumer ruleBasedSegmentStorage) { + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); + } + + @Nullable + List getParsedConditions(String matchingKey, List conditions, String largeConditionSizeMessage) { + if (conditions.size() > CONDITIONS_UPPER_LIMIT) { + Logger.w(largeConditionSizeMessage); + return null; + } + + List parsedConditionList = new ArrayList<>(); + + try { + for (Condition condition : conditions) { + List partitions = condition.partitions; + CombiningMatcher matcher = toMatcher(condition.matcherGroup, matchingKey); + parsedConditionList.add(new ParsedCondition(condition.conditionType, matcher, partitions, condition.label)); + } + } catch (UnsupportedMatcherException e) { + Logger.w(e.getMessage()); + parsedConditionList = mDefaultConditionsProvider.getDefaultConditions(); + } + return parsedConditionList; + } + + private CombiningMatcher toMatcher(MatcherGroup matcherGroup, String matchingKey) throws UnsupportedMatcherException { + List matchers = matcherGroup.matchers; + checkArgument(!matchers.isEmpty()); + + List toCombine = new ArrayList<>(); + + for (Matcher matcher : matchers) { + AttributeMatcher attributeMatcher = toMatcher(matcher, matchingKey); + + toCombine.add(attributeMatcher); + } + + return new CombiningMatcher(matcherGroup.combiner, toCombine); + } + + private AttributeMatcher toMatcher(Matcher matcher, String matchingKey) throws UnsupportedMatcherException { + io.split.android.engine.matchers.Matcher delegate = null; + + // Values not present in {@link io.split.android.client.dtos.MatcherType} are deserialized as null + if (matcher.matcherType == null) { + throw new UnsupportedMatcherException("Unable to create matcher for matcher type"); + } + + switch (matcher.matcherType) { + case ALL_KEYS: + delegate = new AllKeysMatcher(); + break; + case IN_SEGMENT: + checkNotNull(matcher.userDefinedSegmentMatcherData); + delegate = new MySegmentsMatcher(matchingKey != null ? mMySegmentsStorageContainer.getStorageForKey(matchingKey) : getEmptyMySegmentsStorage(), + matcher.userDefinedSegmentMatcherData.segmentName); + break; + case IN_LARGE_SEGMENT: + checkNotNull(matcher.userDefinedLargeSegmentMatcherData); + delegate = new MySegmentsMatcher((matchingKey != null) ? mMyLargeSegmentsStorageContainer.getStorageForKey(matchingKey) : getEmptyMySegmentsStorage(), + matcher.userDefinedLargeSegmentMatcherData.largeSegmentName); + break; + case WHITELIST: + checkNotNull(matcher.whitelistMatcherData); + delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); + break; + case EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); + delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + break; + case GREATER_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); + delegate = new GreaterThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + break; + case LESS_THAN_OR_EQUAL_TO: + checkNotNull(matcher.unaryNumericMatcherData); + delegate = new LessThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); + break; + case BETWEEN: + checkNotNull(matcher.betweenMatcherData); + delegate = new BetweenMatcher(matcher.betweenMatcherData.start, matcher.betweenMatcherData.end, matcher.betweenMatcherData.dataType); + break; + case EQUAL_TO_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case PART_OF_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_ALL_OF_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_ANY_OF_SET: + checkNotNull(matcher.whitelistMatcherData); + delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); + break; + case STARTS_WITH: + checkNotNull(matcher.whitelistMatcherData); + delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case ENDS_WITH: + checkNotNull(matcher.whitelistMatcherData); + delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case CONTAINS_STRING: + checkNotNull(matcher.whitelistMatcherData); + delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); + break; + case MATCHES_STRING: + checkNotNull(matcher.stringMatcherData); + delegate = new RegularExpressionMatcher(matcher.stringMatcherData); + break; + case IN_SPLIT_TREATMENT: + checkNotNull(matcher.dependencyMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.dependencyMatcherData() MUST NOT BE null"); + delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); + break; + case EQUAL_TO_BOOLEAN: + checkNotNull(matcher.booleanMatcherData, + "MatcherType is " + matcher.matcherType + + ". matcher.booleanMatcherData() MUST NOT BE null"); + delegate = new BooleanMatcher(matcher.booleanMatcherData); + break; + case EQUAL_TO_SEMVER: + delegate = new EqualToSemverMatcher(matcher.stringMatcherData); + break; + case GREATER_THAN_OR_EQUAL_TO_SEMVER: + delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); + break; + case LESS_THAN_OR_EQUAL_TO_SEMVER: + delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); + break; + case BETWEEN_SEMVER: + delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); + break; + case IN_LIST_SEMVER: + delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); + break; + case IN_RULE_BASED_SEGMENT: + if (mRuleBasedSegmentStorage != null) { + delegate = new InRuleBasedSegmentMatcher(mRuleBasedSegmentStorage, + (matchingKey != null) ? mMySegmentsStorageContainer.getStorageForKey(matchingKey) : getEmptyMySegmentsStorage(), + (matchingKey != null) ? mMyLargeSegmentsStorageContainer.getStorageForKey(matchingKey) : getEmptyMySegmentsStorage(), + matcher.userDefinedSegmentMatcherData.segmentName); + } else { + // shouldn't happen + Logger.w("RuleBasedSegmentStorage not set in ParserCommons"); + } + break; + default: + // since values not present in {@link io.split.android.client.dtos.MatcherType} + // are deserialized as null, this would most likely not be reached. Adding it for completeness + throw new UnsupportedMatcherException("Unable to create matcher for matcher type: " + matcher.matcherType); + } + + if (delegate == null) { + throw new UnsupportedMatcherException("Unable to create matcher for matcher type: " + matcher.matcherType); + } + + String attribute = null; + if (matcher.keySelector != null && matcher.keySelector.attribute != null) { + attribute = matcher.keySelector.attribute; + } + + boolean negate = matcher.negate; + + return new AttributeMatcher(attribute, delegate, negate); + } + + @NonNull + private EmptyMySegmentsStorage getEmptyMySegmentsStorage() { + if (mEmptyMySegmentsStorage == null) { + mEmptyMySegmentsStorage = new EmptyMySegmentsStorage(); + } + + return mEmptyMySegmentsStorage; + } +} diff --git a/src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java b/src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java new file mode 100644 index 000000000..e3a223c96 --- /dev/null +++ b/src/main/java/io/split/android/engine/experiments/RuleBasedSegmentParser.java @@ -0,0 +1,47 @@ +package io.split.android.engine.experiments; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +import io.split.android.client.dtos.Condition; +import io.split.android.client.dtos.Excluded; +import io.split.android.client.dtos.RuleBasedSegment; + +public class RuleBasedSegmentParser implements Parser { + + private final ParserCommons mParserCommons; + + public RuleBasedSegmentParser(@NonNull ParserCommons parserCommons) { + mParserCommons = parserCommons; + } + + @Nullable + @Override + public ParsedRuleBasedSegment parse(@NonNull RuleBasedSegment input, String matchingKey) { + String name = input.getName(); + Excluded excluded = input.getExcluded(); + List conditions = input.getConditions(); + List parsedConditions = mParserCommons.getParsedConditions( + matchingKey, + conditions, + "Dropping rule based segment name=" + name + " due to large number of conditions (" + conditions.size() + ")"); + + if (parsedConditions == null) { + parsedConditions = new ArrayList<>(); + } + + if (excluded == null) { + excluded = Excluded.createEmpty(); + } + + return new ParsedRuleBasedSegment(name, + excluded.getKeys(), + excluded.getSegments(), + parsedConditions, + input.getTrafficTypeName(), + input.getChangeNumber()); + } +} diff --git a/src/main/java/io/split/android/engine/experiments/SplitParser.java b/src/main/java/io/split/android/engine/experiments/SplitParser.java index e87b2b459..03cbc27e4 100644 --- a/src/main/java/io/split/android/engine/experiments/SplitParser.java +++ b/src/main/java/io/split/android/engine/experiments/SplitParser.java @@ -1,73 +1,21 @@ package io.split.android.engine.experiments; -import static io.split.android.client.utils.Utils.checkArgument; import static io.split.android.client.utils.Utils.checkNotNull; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import java.util.ArrayList; import java.util.List; -import io.split.android.client.dtos.Condition; -import io.split.android.client.dtos.Matcher; -import io.split.android.client.dtos.MatcherGroup; -import io.split.android.client.dtos.Partition; import io.split.android.client.dtos.Split; import io.split.android.client.dtos.Status; -import io.split.android.client.storage.mysegments.EmptyMySegmentsStorage; -import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.utils.logger.Logger; -import io.split.android.engine.matchers.AllKeysMatcher; -import io.split.android.engine.matchers.AttributeMatcher; -import io.split.android.engine.matchers.BetweenMatcher; -import io.split.android.engine.matchers.BooleanMatcher; -import io.split.android.engine.matchers.CombiningMatcher; -import io.split.android.engine.matchers.DependencyMatcher; -import io.split.android.engine.matchers.EqualToMatcher; -import io.split.android.engine.matchers.GreaterThanOrEqualToMatcher; -import io.split.android.engine.matchers.LessThanOrEqualToMatcher; -import io.split.android.engine.matchers.MySegmentsMatcher; -import io.split.android.engine.matchers.collections.ContainsAllOfSetMatcher; -import io.split.android.engine.matchers.collections.ContainsAnyOfSetMatcher; -import io.split.android.engine.matchers.collections.EqualToSetMatcher; -import io.split.android.engine.matchers.collections.PartOfSetMatcher; -import io.split.android.engine.matchers.semver.BetweenSemverMatcher; -import io.split.android.engine.matchers.semver.EqualToSemverMatcher; -import io.split.android.engine.matchers.semver.GreaterThanOrEqualToSemverMatcher; -import io.split.android.engine.matchers.semver.InListSemverMatcher; -import io.split.android.engine.matchers.semver.LessThanOrEqualToSemverMatcher; -import io.split.android.engine.matchers.strings.ContainsAnyOfMatcher; -import io.split.android.engine.matchers.strings.EndsWithAnyOfMatcher; -import io.split.android.engine.matchers.strings.RegularExpressionMatcher; -import io.split.android.engine.matchers.strings.StartsWithAnyOfMatcher; -import io.split.android.engine.matchers.strings.WhitelistMatcher; -public class SplitParser { +public class SplitParser implements Parser { - public static final int CONDITIONS_UPPER_LIMIT = 50; + private final ParserCommons mParserCommons; - private final MySegmentsStorageContainer mMySegmentsStorageContainer; - private final MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; - private final DefaultConditionsProvider mDefaultConditionsProvider; - - public SplitParser(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, @Nullable MySegmentsStorageContainer myLargeSegmentsStorageContainer) { - this(mySegmentsStorageContainer, myLargeSegmentsStorageContainer, new DefaultConditionsProvider()); - } - - @VisibleForTesting - static SplitParser get(MySegmentsStorageContainer mySegmentsStorageContainer, MySegmentsStorageContainer myLargeSegmentsStorageContainer) { - return new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer); - } - - @VisibleForTesting - SplitParser(@NonNull MySegmentsStorageContainer mySegmentsStorageContainer, - @Nullable MySegmentsStorageContainer myLargeSegmentsStorageContainer, - @NonNull DefaultConditionsProvider defaultConditionsProvider) { - mMySegmentsStorageContainer = checkNotNull(mySegmentsStorageContainer); - mMyLargeSegmentsStorageContainer = myLargeSegmentsStorageContainer; - mDefaultConditionsProvider = checkNotNull(defaultConditionsProvider); + public SplitParser(ParserCommons parserCommons) { + mParserCommons = checkNotNull(parserCommons); } @Nullable @@ -75,6 +23,7 @@ public ParsedSplit parse(@Nullable Split split) { return parse(split, null); } + @Override @Nullable public ParsedSplit parse(@Nullable Split split, @Nullable String matchingKey) { try { @@ -94,25 +43,12 @@ private ParsedSplit parseWithoutExceptionHandling(Split split, String matchingKe return null; } - if (split.conditions.size() > CONDITIONS_UPPER_LIMIT) { - Logger.w("Dropping feature flag name=%s due to large number of conditions(%d)", - split.name, split.conditions.size()); + List parsedConditionList = mParserCommons.getParsedConditions(matchingKey, split.conditions, + "Dropping feature flag name=" + split.name + " due to large number of conditions (" + split.conditions.size() + ")"); + if (parsedConditionList == null) { return null; } - List parsedConditionList = new ArrayList<>(); - - try { - for (Condition condition : split.conditions) { - List partitions = condition.partitions; - CombiningMatcher matcher = toMatcher(condition.matcherGroup, matchingKey); - parsedConditionList.add(new ParsedCondition(condition.conditionType, matcher, partitions, condition.label)); - } - } catch (UnsupportedMatcherException e) { - Logger.w(e.getMessage()); - parsedConditionList = mDefaultConditionsProvider.getDefaultConditions(); - } - return new ParsedSplit(split.name, split.seed, split.killed, @@ -127,135 +63,4 @@ private ParsedSplit parseWithoutExceptionHandling(Split split, String matchingKe split.sets, split.impressionsDisabled); } - - private CombiningMatcher toMatcher(MatcherGroup matcherGroup, String matchingKey) throws UnsupportedMatcherException { - List matchers = matcherGroup.matchers; - checkArgument(!matchers.isEmpty()); - - List toCombine = new ArrayList<>(); - - for (Matcher matcher : matchers) { - AttributeMatcher attributeMatcher = toMatcher(matcher, matchingKey); - - toCombine.add(attributeMatcher); - } - - return new CombiningMatcher(matcherGroup.combiner, toCombine); - } - - private AttributeMatcher toMatcher(Matcher matcher, String matchingKey) throws UnsupportedMatcherException { - io.split.android.engine.matchers.Matcher delegate; - - // Values not present in {@link io.split.android.client.dtos.MatcherType} are deserialized as null - if (matcher.matcherType == null) { - throw new UnsupportedMatcherException("Unable to create matcher for matcher type"); - } - - switch (matcher.matcherType) { - case ALL_KEYS: - delegate = new AllKeysMatcher(); - break; - case IN_SEGMENT: - checkNotNull(matcher.userDefinedSegmentMatcherData); - delegate = new MySegmentsMatcher((matchingKey != null) ? mMySegmentsStorageContainer.getStorageForKey(matchingKey) : new EmptyMySegmentsStorage(), matcher.userDefinedSegmentMatcherData.segmentName); - break; - case IN_LARGE_SEGMENT: - checkNotNull(matcher.userDefinedLargeSegmentMatcherData); - delegate = new MySegmentsMatcher((matchingKey != null) ? mMyLargeSegmentsStorageContainer.getStorageForKey(matchingKey) : new EmptyMySegmentsStorage(), matcher.userDefinedLargeSegmentMatcherData.largeSegmentName); - break; - case WHITELIST: - checkNotNull(matcher.whitelistMatcherData); - delegate = new WhitelistMatcher(matcher.whitelistMatcherData.whitelist); - break; - case EQUAL_TO: - checkNotNull(matcher.unaryNumericMatcherData); - delegate = new EqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); - break; - case GREATER_THAN_OR_EQUAL_TO: - checkNotNull(matcher.unaryNumericMatcherData); - delegate = new GreaterThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); - break; - case LESS_THAN_OR_EQUAL_TO: - checkNotNull(matcher.unaryNumericMatcherData); - delegate = new LessThanOrEqualToMatcher(matcher.unaryNumericMatcherData.value, matcher.unaryNumericMatcherData.dataType); - break; - case BETWEEN: - checkNotNull(matcher.betweenMatcherData); - delegate = new BetweenMatcher(matcher.betweenMatcherData.start, matcher.betweenMatcherData.end, matcher.betweenMatcherData.dataType); - break; - case EQUAL_TO_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new EqualToSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case PART_OF_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new PartOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_ALL_OF_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new ContainsAllOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_ANY_OF_SET: - checkNotNull(matcher.whitelistMatcherData); - delegate = new ContainsAnyOfSetMatcher(matcher.whitelistMatcherData.whitelist); - break; - case STARTS_WITH: - checkNotNull(matcher.whitelistMatcherData); - delegate = new StartsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case ENDS_WITH: - checkNotNull(matcher.whitelistMatcherData); - delegate = new EndsWithAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case CONTAINS_STRING: - checkNotNull(matcher.whitelistMatcherData); - delegate = new ContainsAnyOfMatcher(matcher.whitelistMatcherData.whitelist); - break; - case MATCHES_STRING: - checkNotNull(matcher.stringMatcherData); - delegate = new RegularExpressionMatcher(matcher.stringMatcherData); - break; - case IN_SPLIT_TREATMENT: - checkNotNull(matcher.dependencyMatcherData, - "MatcherType is " + matcher.matcherType - + ". matcher.dependencyMatcherData() MUST NOT BE null"); - delegate = new DependencyMatcher(matcher.dependencyMatcherData.split, matcher.dependencyMatcherData.treatments); - break; - case EQUAL_TO_BOOLEAN: - checkNotNull(matcher.booleanMatcherData, - "MatcherType is " + matcher.matcherType - + ". matcher.booleanMatcherData() MUST NOT BE null"); - delegate = new BooleanMatcher(matcher.booleanMatcherData); - break; - case EQUAL_TO_SEMVER: - delegate = new EqualToSemverMatcher(matcher.stringMatcherData); - break; - case GREATER_THAN_OR_EQUAL_TO_SEMVER: - delegate = new GreaterThanOrEqualToSemverMatcher(matcher.stringMatcherData); - break; - case LESS_THAN_OR_EQUAL_TO_SEMVER: - delegate = new LessThanOrEqualToSemverMatcher(matcher.stringMatcherData); - break; - case BETWEEN_SEMVER: - delegate = new BetweenSemverMatcher(matcher.betweenStringMatcherData.start, matcher.betweenStringMatcherData.end); - break; - case IN_LIST_SEMVER: - delegate = new InListSemverMatcher(matcher.whitelistMatcherData.whitelist); - break; - default: - // since values not present in {@link io.split.android.client.dtos.MatcherType} - // are deserialized as null, this would most likely not be reached. Adding it for completeness - throw new UnsupportedMatcherException("Unable to create matcher for matcher type: " + matcher.matcherType); - } - - String attribute = null; - if (matcher.keySelector != null && matcher.keySelector.attribute != null) { - attribute = matcher.keySelector.attribute; - } - - boolean negate = matcher.negate; - - - return new AttributeMatcher(attribute, delegate, negate); - } } diff --git a/src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java b/src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java new file mode 100644 index 000000000..54d4b1f57 --- /dev/null +++ b/src/main/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcher.java @@ -0,0 +1,89 @@ +package io.split.android.engine.matchers; + +import static io.split.android.client.utils.Utils.checkNotNull; + +import androidx.annotation.NonNull; + +import java.util.Map; + +import io.split.android.client.Evaluator; +import io.split.android.client.dtos.ExcludedSegment; +import io.split.android.client.storage.mysegments.MySegmentsStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageConsumer; +import io.split.android.engine.experiments.ParsedCondition; +import io.split.android.engine.experiments.ParsedRuleBasedSegment; + +public class InRuleBasedSegmentMatcher implements Matcher { + + private final RuleBasedSegmentStorageConsumer mRuleBasedSegmentStorage; + private final MySegmentsStorage mMySegmentsStorage; + private final MySegmentsStorage mMyLargeSegmentsStorage; + private final String mSegmentName; + + public InRuleBasedSegmentMatcher(@NonNull RuleBasedSegmentStorageConsumer ruleBasedSegmentStorage, + @NonNull MySegmentsStorage mySegmentsStorage, + @NonNull MySegmentsStorage myLargeSegmentsStorage, + @NonNull String segmentName) { + mRuleBasedSegmentStorage = checkNotNull(ruleBasedSegmentStorage); + mMySegmentsStorage = checkNotNull(mySegmentsStorage); + mMyLargeSegmentsStorage = checkNotNull(myLargeSegmentsStorage); + mSegmentName = checkNotNull(segmentName); + } + + @Override + public boolean match(Object matchValue, String bucketingKey, Map attributes, Evaluator evaluator) { + if (!(matchValue instanceof String)) { + return false; + } + + final String matchingKey = (String) matchValue; + final ParsedRuleBasedSegment parsedRuleBasedSegment = mRuleBasedSegmentStorage.get(mSegmentName, matchingKey); + + if (parsedRuleBasedSegment == null) { + return false; + } + + if (isKeyExcluded(parsedRuleBasedSegment, matchingKey)) { + return false; + } + + if (inExcludedSegment(parsedRuleBasedSegment, matchingKey, bucketingKey, attributes, evaluator)) { + return false; + } + + return matchesConditions(bucketingKey, attributes, evaluator, parsedRuleBasedSegment, matchingKey); + } + + private static boolean isKeyExcluded(ParsedRuleBasedSegment parsedRuleBasedSegment, String matchingKey) { + return parsedRuleBasedSegment.getExcludedKeys().contains(matchingKey); + } + + private boolean inExcludedSegment(ParsedRuleBasedSegment parsedRuleBasedSegment, Object matchingKey, String bucketingKey, Map attributes, Evaluator evaluator) { + for (ExcludedSegment segment : parsedRuleBasedSegment.getExcludedSegments()) { + if (segment.isStandard() && mMySegmentsStorage.getAll().contains(segment.getName())) { + return true; + } + + if (segment.isRuleBased()) { + InRuleBasedSegmentMatcher inRuleBasedSegmentMatcher = new InRuleBasedSegmentMatcher(mRuleBasedSegmentStorage, mMySegmentsStorage, mMyLargeSegmentsStorage, segment.getName()); + if (inRuleBasedSegmentMatcher.match(matchingKey, bucketingKey, attributes, evaluator)) { + return true; + } + } + + if (segment.isLarge() && mMyLargeSegmentsStorage.getAll().contains(segment.getName())) { + return true; + } + } + return false; + } + + private static boolean matchesConditions(String bucketingKey, Map attributes, Evaluator evaluator, ParsedRuleBasedSegment parsedRuleBasedSegment, String matchingKey) { + for (ParsedCondition condition : parsedRuleBasedSegment.getParsedConditions()) { + if (condition.matcher().match(matchingKey, bucketingKey, attributes, evaluator)) { + return true; + } + } + return false; + } +} diff --git a/src/sharedTest/java/helper/TestingHelper.java b/src/sharedTest/java/helper/TestingHelper.java index 2d70138d2..af190706d 100644 --- a/src/sharedTest/java/helper/TestingHelper.java +++ b/src/sharedTest/java/helper/TestingHelper.java @@ -1,5 +1,8 @@ package helper; +import static java.lang.Thread.sleep; + +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; @@ -12,8 +15,6 @@ import io.split.android.client.events.SplitEventTask; import io.split.android.client.utils.logger.Logger; -import static java.lang.Thread.sleep; - public class TestingHelper { public static final String COUNTERS_REFRESH_RATE_SECS_NAME = "COUNTERS_REFRESH_RATE_SECS"; @@ -120,4 +121,14 @@ public static KeyImpression newImpression(String feature, String key) { impression.time = 100; return impression; } + + public static Object getFieldValue(Object object, String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(object); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Error accessing field: " + fieldName, e); + } + } } diff --git a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java b/src/test/java/io/split/android/client/SplitClientImplBaseTest.java index 1b6fe4a0d..e1f7f6811 100644 --- a/src/test/java/io/split/android/client/SplitClientImplBaseTest.java +++ b/src/test/java/io/split/android/client/SplitClientImplBaseTest.java @@ -12,9 +12,11 @@ import io.split.android.client.shared.SplitClientContainer; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.validators.SplitValidator; import io.split.android.client.validators.TreatmentManager; +import io.split.android.engine.experiments.ParserCommons; import io.split.android.engine.experiments.SplitParser; import io.split.android.fake.SplitTaskExecutorStub; @@ -31,6 +33,8 @@ public abstract class SplitClientImplBaseTest { @Mock protected MySegmentsStorageContainer myLargeSegmentsStorageContainer; @Mock + protected RuleBasedSegmentStorage ruleBasedSegmentStorage; + @Mock protected MySegmentsStorage mySegmentsStorage; @Mock protected ImpressionListener impressionListener; @@ -57,7 +61,7 @@ public void setUp() { container, clientContainer, new Key("test_key"), - new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer), + new SplitParser(new ParserCommons(mySegmentsStorageContainer, myLargeSegmentsStorageContainer)), impressionListener, splitClientConfig, new SplitEventsManager(splitClientConfig, new SplitTaskExecutorStub()), diff --git a/src/test/java/io/split/android/client/SplitManagerImplTest.java b/src/test/java/io/split/android/client/SplitManagerImplTest.java index 3340701ee..180bbf3ff 100644 --- a/src/test/java/io/split/android/client/SplitManagerImplTest.java +++ b/src/test/java/io/split/android/client/SplitManagerImplTest.java @@ -27,10 +27,12 @@ import io.split.android.client.dtos.Split; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.validators.SplitValidator; import io.split.android.client.validators.SplitValidatorImpl; import io.split.android.engine.ConditionsTestUtil; +import io.split.android.engine.experiments.ParserCommons; import io.split.android.engine.experiments.SplitFetcher; import io.split.android.engine.experiments.SplitParser; import io.split.android.engine.matchers.AllKeysMatcher; @@ -48,6 +50,8 @@ public class SplitManagerImplTest { @Mock MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; @Mock + RuleBasedSegmentStorage mRuleBasedSegmentStorage; + @Mock SplitManager mSplitManager; @Before @@ -55,7 +59,7 @@ public void setup() { MockitoAnnotations.openMocks(this); SplitValidator validator = new SplitValidatorImpl(); when(mMySegmentsStorageContainer.getStorageForKey("")).thenReturn(mMySegmentsStorage); - SplitParser parser = new SplitParser(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer); + SplitParser parser = new SplitParser(new ParserCommons(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer)); mSplitManager = new SplitManagerImpl(mSplitsStorage, validator, parser); } diff --git a/src/test/java/io/split/android/client/TreatmentManagerTest.java b/src/test/java/io/split/android/client/TreatmentManagerTest.java index 2791953dc..6fc11d770 100644 --- a/src/test/java/io/split/android/client/TreatmentManagerTest.java +++ b/src/test/java/io/split/android/client/TreatmentManagerTest.java @@ -32,6 +32,7 @@ import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorageProducer; import io.split.android.client.utils.Utils; @@ -44,6 +45,7 @@ import io.split.android.client.validators.TreatmentManagerImpl; import io.split.android.client.validators.ValidationMessageLogger; import io.split.android.client.validators.ValidationMessageLoggerImpl; +import io.split.android.engine.experiments.ParserCommons; import io.split.android.engine.experiments.SplitParser; import io.split.android.fake.SplitEventsManagerStub; import io.split.android.grammar.Treatments; @@ -73,11 +75,12 @@ public void loadSplitsFromFile() { MySegmentsStorageContainer mySegmentsStorageContainer = mock(MySegmentsStorageContainer.class); MySegmentsStorageContainer myLargeSegmentsStorageContainer = mock(MySegmentsStorageContainer.class); MySegmentsStorage mySegmentsStorage = mock(MySegmentsStorage.class); + RuleBasedSegmentStorage ruleBasedSegmentStorage = mock(RuleBasedSegmentStorage.class); SplitsStorage splitsStorage = mock(SplitsStorage.class); Set mySegments = new HashSet(Arrays.asList("s1", "s2", "test_copy")); List splits = fileHelper.loadAndParseSplitChangeFile("split_changes_1.json"); - SplitParser splitParser = new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer); + SplitParser splitParser = new SplitParser(new ParserCommons(mySegmentsStorageContainer, myLargeSegmentsStorageContainer)); Map splitsMap = splitsMap(splits); when(splitsStorage.getAll()).thenReturn(splitsMap); diff --git a/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java b/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java index b61287794..60978714b 100644 --- a/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java +++ b/src/test/java/io/split/android/client/events/EventsManagerCoordinatorTest.java @@ -1,7 +1,6 @@ package io.split.android.client.events; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import org.junit.Assert; @@ -35,6 +34,17 @@ public void SPLITS_UPDATEDEventIsPassedDownToChildren() { verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.SPLITS_UPDATED); } + @Test + public void RULE_BASED_SEGMENTEventIsPassedDownToChildren() { + mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); + + mEventsManager.notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + + delay(); + + verify(mMockChildEventsManager).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + } + @Test public void SPLITS_FETCHEDEventIsPassedDownToChildren() { mEventsManager.registerEventsManager(new Key("key", "bucketing"), mMockChildEventsManager); diff --git a/src/test/java/io/split/android/client/events/EventsManagerTest.java b/src/test/java/io/split/android/client/events/EventsManagerTest.java index 1f896825d..c76a3d533 100644 --- a/src/test/java/io/split/android/client/events/EventsManagerTest.java +++ b/src/test/java/io/split/android/client/events/EventsManagerTest.java @@ -166,10 +166,15 @@ public void sdkUpdateWithLargeSegmentsAndConfigEnabledEmitsEvent() throws Interr } @Test - public void sdkUpdateWithLargeSegmentsAndConfigEnabledAndWaitForLargeSegmentsFalseEmitsEvent() throws InterruptedException { + public void sdkUpdateWithLargeSegments() throws InterruptedException { sdkUpdateTest(SplitInternalEvent.MY_LARGE_SEGMENTS_UPDATED, false); } + @Test + public void sdkUpdateWithRuleBasedSegments() throws InterruptedException { + sdkUpdateTest(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED, false); + } + @Test public void sdkReadyWithSplitsAndUpdatedLargeSegments() { diff --git a/src/test/java/io/split/android/client/network/HttpClientTest.java b/src/test/java/io/split/android/client/network/HttpClientTest.java index b036ec5b8..c78b52696 100644 --- a/src/test/java/io/split/android/client/network/HttpClientTest.java +++ b/src/test/java/io/split/android/client/network/HttpClientTest.java @@ -36,6 +36,7 @@ import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.dtos.TestImpressions; import io.split.android.client.utils.Json; import io.split.android.helpers.FileHelper; @@ -149,7 +150,7 @@ public void severalRequest() throws Exception { Assert.assertTrue(mySegments.contains("groupb")); // Assert split changes - SplitChange splitChange = Json.fromJson(splitChangeResp.getData(), SplitChange.class); + SplitChange splitChange = Json.fromJson(splitChangeResp.getData(), TargetingRulesChange.class).getFeatureFlagsChange(); Assert.assertEquals(200, splitChangeResp.getHttpStatus()); assertTrue(splitChangeResp.isSuccess()); Assert.assertEquals(-1, splitChange.since); diff --git a/src/test/java/io/split/android/client/service/HttpFetcherTest.java b/src/test/java/io/split/android/client/service/HttpFetcherTest.java index 36c85ffe0..7c3e5775d 100644 --- a/src/test/java/io/split/android/client/service/HttpFetcherTest.java +++ b/src/test/java/io/split/android/client/service/HttpFetcherTest.java @@ -27,6 +27,7 @@ import io.split.android.client.dtos.AllSegmentsChange; import io.split.android.client.dtos.SegmentsChange; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.network.HttpClient; import io.split.android.client.network.HttpException; import io.split.android.client.network.HttpMethod; @@ -40,7 +41,7 @@ import io.split.android.client.service.http.HttpFetcherImpl; import io.split.android.client.service.http.HttpResponseParser; import io.split.android.client.service.mysegments.AllSegmentsResponseParser; -import io.split.android.client.service.splits.SplitChangeResponseParser; +import io.split.android.client.service.rules.TargetingRulesResponseParser; public class HttpFetcherTest { @@ -52,7 +53,7 @@ public class HttpFetcherTest { private URI mUrl; private URI mSplitChangesUrl; private URI mMySegmentsUrl; - private final HttpResponseParser mSplitChangeResponseParser = new SplitChangeResponseParser(); + private final HttpResponseParser mSplitChangeResponseParser = new TargetingRulesResponseParser(); private final HttpResponseParser mMySegmentsResponseParser = new AllSegmentsResponseParser(); @Before @@ -67,7 +68,7 @@ public void setup() throws URISyntaxException { public void testNoReachableUrl() throws URISyntaxException { - HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mUrl, mSplitChangeResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mUrl, mSplitChangeResponseParser); boolean isReachable = true; try { Map params = new HashMap<>(); @@ -90,12 +91,12 @@ public void testSuccessfulSplitChangeFetch() throws URISyntaxException, HttpExce when(request.execute()).thenReturn(response); when(mClientMock.request(uri, HttpMethod.GET, null, null)).thenReturn(request); - HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); SplitChange change = null; try { Map params = new HashMap<>(); params.put("since", -1); - change = fetcher.execute(params, null); + change = fetcher.execute(params, null).getFeatureFlagsChange(); // TODO } catch (HttpFetcherException e) { exceptionWasThrown = true; } @@ -116,22 +117,24 @@ public void tesNonEmptyHeaderSplitChangesFetch() throws URISyntaxException, Http URI uri = new URIBuilder(mSplitChangesUrl).addParameter("since", "" + -1).build(); HttpRequest request = mock(HttpRequest.class); - HttpResponse response = new HttpResponseImpl(200, dummySplitChangeResponse()); + when(request.execute()).thenReturn(response); when(mClientMock.request(uri, HttpMethod.GET, null, headers)).thenReturn(request); - HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); try { Map params = new HashMap<>(); params.put("since", -1); - SplitChange change = fetcher.execute(params, headers); + SplitChange change = fetcher.execute(params, headers).getFeatureFlagsChange(); // TODO } catch (HttpFetcherException e) { + e.printStackTrace(); exceptionWasThrown = true; } ArgumentCaptor> headersCaptor = ArgumentCaptor.forClass(Map.class); + Assert.assertFalse(exceptionWasThrown); verify(mClientMock).request(eq(uri), eq(HttpMethod.GET), eq(null), headersCaptor.capture()); Assert.assertEquals("value1", headersCaptor.getValue().get("header1")); } @@ -145,13 +148,13 @@ public void testFailedResponse() throws URISyntaxException, HttpException { when(request.execute()).thenReturn(response); when(mClientMock.request(uri, HttpMethod.GET, null, null)).thenReturn(request); - HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); SplitChange change = null; boolean failed = false; try { Map params = new HashMap<>(); params.put("since", -1); - change = fetcher.execute(params, null); + change = fetcher.execute(params, null).getFeatureFlagsChange(); // TODO } catch (HttpFetcherException e) { failed = true; } @@ -169,13 +172,13 @@ public void testWrongResponse() throws URISyntaxException, HttpException { when(request.execute()).thenReturn(response); when(mClientMock.request(uri, HttpMethod.GET, null, null)).thenReturn(request); - HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); SplitChange change = null; boolean failed = false; try { Map params = new HashMap<>(); params.put("since", -1); - change = fetcher.execute(params, null); + change = fetcher.execute(params, null).getFeatureFlagsChange(); // TODO } catch (HttpFetcherException e) { failed = true; } @@ -261,7 +264,7 @@ public void testHandleParserExceptionFetch() throws URISyntaxException, HttpExce @Test public void paramOrderIsCorrect() throws HttpFetcherException, HttpException { - HttpFetcher fetcher = getSplitChangeHttpFetcher(); + HttpFetcher fetcher = getSplitChangeHttpFetcher(); Map params = new LinkedHashMap<>(); params.put("s", "1.1"); @@ -276,7 +279,7 @@ public void paramOrderIsCorrect() throws HttpFetcherException, HttpException { @Test public void paramOrderWithoutTillIsCorrect() throws HttpException, HttpFetcherException { - HttpFetcher fetcher = getSplitChangeHttpFetcher(); + HttpFetcher fetcher = getSplitChangeHttpFetcher(); Map params = new LinkedHashMap<>(); params.put("s", "1.1"); @@ -290,7 +293,7 @@ public void paramOrderWithoutTillIsCorrect() throws HttpException, HttpFetcherEx @Test public void paramOrderWithoutSpecIsCorrect() throws HttpException, HttpFetcherException { - HttpFetcher fetcher = getSplitChangeHttpFetcher(); + HttpFetcher fetcher = getSplitChangeHttpFetcher(); Map params = new LinkedHashMap<>(); params.put("since", "-1"); @@ -304,7 +307,7 @@ public void paramOrderWithoutSpecIsCorrect() throws HttpException, HttpFetcherEx @Test public void paramOrderWithoutSetsIsCorrect() throws HttpException { - HttpFetcher fetcher = getSplitChangeHttpFetcher(); + HttpFetcher fetcher = getSplitChangeHttpFetcher(); Map params = new LinkedHashMap<>(); params.put("till", "100"); @@ -322,7 +325,7 @@ public void paramOrderWithoutSetsIsCorrect() throws HttpException { @Test public void httpExceptionWithStatusCodeAddsStatusCodeToHttpFetcherException() throws HttpException { - HttpFetcher fetcher = getSplitChangeHttpFetcher(); + HttpFetcher fetcher = getSplitChangeHttpFetcher(); HttpRequest request = mock(HttpRequest.class); when(request.execute()).thenThrow(new HttpException("Not found", 404)); @@ -341,12 +344,12 @@ public void httpExceptionWithStatusCodeAddsStatusCodeToHttpFetcherException() th } @NonNull - private HttpFetcher getSplitChangeHttpFetcher() throws HttpException { + private HttpFetcher getSplitChangeHttpFetcher() throws HttpException { HttpRequest mockRequest = mock(HttpRequest.class); when(mockRequest.execute()).thenReturn(new HttpResponseImpl(200, dummySplitChangeResponse())); when(mClientMock.request(any(), any(), any(), any())).thenReturn(mockRequest); - HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); + HttpFetcher fetcher = new HttpFetcherImpl<>(mClientMock, mSplitChangesUrl, mSplitChangeResponseParser); return fetcher; } @@ -361,9 +364,26 @@ public boolean matches(URI argument) { } private String dummySplitChangeResponse() { - return "{\"splits\":[{\"name\":\"sample_feature\", \"status\":\"ACTIVE\"}],\n" + - " \"since\":-1,\n" + - " \"till\":100}"; +// return "{\"splits\":[{\"name\":\"sample_feature\", \"status\":\"ACTIVE\"}],\n" + +// " \"since\":-1,\n" + +// " \"till\":100}"; + return "{\n" + + " \"ff\": {\n" + + " \"splits\": [\n" + + " {\n" + + " \"name\": \"sample_feature\",\n" + + " \"status\": \"ACTIVE\"\n" + + " }\n" + + " ],\n" + + " \"since\": -1,\n" + + " \"till\": 100\n" + + " },\n" + + " \"rbs\": {\n" + + " \"d\": [],\n" + + " \"s\": -1,\n" + + " \"t\": 50\n" + + " }\n" + + "}"; } private String dummyMySegmentsResponse() { diff --git a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java index 9bbe08901..9a4eb8580 100644 --- a/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java +++ b/src/test/java/io/split/android/client/service/SplitSyncTaskTest.java @@ -1,10 +1,13 @@ package io.split.android.client.service; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.longThat; +import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -27,6 +30,7 @@ import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.splits.SplitsSyncTask; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; @@ -39,6 +43,7 @@ public class SplitSyncTaskTest { SplitsStorage mSplitsStorage; SplitChange mSplitChange = null; SplitsSyncHelper mSplitsSyncHelper; + RuleBasedSegmentStorageProducer mRuleBasedSegmentStorage; SplitsSyncTask mTask; String mQueryString = "qs=1"; @@ -54,8 +59,9 @@ public void setup() { mSplitsStorage = mock(SplitsStorage.class); mSplitsSyncHelper = mock(SplitsSyncHelper.class); mEventsManager = mock(SplitEventsManager.class); + mRuleBasedSegmentStorage = mock(RuleBasedSegmentStorageProducer.class); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL)); + when(mSplitsSyncHelper.sync(notNull(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLIT_KILL)); loadSplitChanges(); } @@ -66,15 +72,16 @@ public void correctExecution() throws HttpFetcherException { // Querystring is the same, so no clear sould be called // And updateTimestamp is 0 // Retry is off, so splitSyncHelper.sync should be called - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(-1L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(-1, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == -1L && argument.getRbsSince() == -1L), eq(false), eq(false), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); } @Test @@ -86,15 +93,16 @@ public void cleanSplitsWhenQueryStringHasChanged() throws HttpFetcherException { String otherQs = "q=other"; Map params = new HashMap<>(); params.put("since", 100L); - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, otherQs, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(200L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(-1, true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == -1 && argument.getRbsSince() == 200), eq(true), eq(true), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); verify(mSplitsStorage, times(1)).updateSplitsFilterQueryString(otherQs); } @@ -103,7 +111,7 @@ public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherExcept // Splits have to be cleared when query string on db is != than current one on current sdk client instance // Setting up cache not expired - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(1111L); @@ -111,7 +119,7 @@ public void noClearSplitsWhenQueryStringHasNotChanged() throws HttpFetcherExcept mTask.execute(); - verify(mSplitsSyncHelper, times(1)).sync(100L, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + verify(mSplitsSyncHelper, times(1)).sync(argThat(argument -> argument.getFlagsSince() == 100L), eq(false), eq(false), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); verify(mSplitsStorage, never()).updateSplitsFilterQueryString(anyString()); } @@ -121,12 +129,12 @@ public void splitUpdatedNotified() throws HttpFetcherException { // Querystring is the same, so no clear sould be called // And updateTimestamp is 0 // Retry is off, so splitSyncHelper.sync should be called - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); mTask.execute(); @@ -139,12 +147,12 @@ public void splitFetchdNotified() throws HttpFetcherException { // Querystring is the same, so no clear sould be called // And updateTimestamp is 0 // Retry is off, so splitSyncHelper.sync should be called - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); mTask.execute(); @@ -153,12 +161,12 @@ public void splitFetchdNotified() throws HttpFetcherException { @Test public void syncIsTrackedInTelemetry() { - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(100L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn(mQueryString); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.SPLITS_SYNC)); mTask.execute(); @@ -167,7 +175,7 @@ public void syncIsTrackedInTelemetry() { @Test public void recordSuccessInTelemetry() { - mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, + mTask = SplitsSyncTask.build(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mQueryString, mEventsManager, mTelemetryRuntimeProducer); when(mSplitsStorage.getTill()).thenReturn(-1L); when(mSplitsStorage.getUpdateTimestamp()).thenReturn(0L); diff --git a/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java b/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java index 134db2981..677030c0c 100644 --- a/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java +++ b/src/test/java/io/split/android/client/service/SplitUpdateTaskTest.java @@ -1,8 +1,18 @@ package io.split.android.client.service; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mockito; import io.split.android.client.dtos.SplitChange; @@ -12,20 +22,14 @@ import io.split.android.client.service.http.HttpFetcherException; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.splits.SplitsUpdateTask; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.helpers.FileHelper; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - public class SplitUpdateTaskTest { SplitsStorage mSplitsStorage; + RuleBasedSegmentStorage mRuleBasedSegmentStorage; SplitChange mSplitChange = null; SplitsSyncHelper mSplitsSyncHelper; SplitEventsManager mEventsManager; @@ -33,25 +37,33 @@ public class SplitUpdateTaskTest { SplitsUpdateTask mTask; long mChangeNumber = 234567833L; + long mRbsChangeNumber = 234567830L; @Before public void setup() { mSplitsStorage = Mockito.mock(SplitsStorage.class); + mRuleBasedSegmentStorage = Mockito.mock(RuleBasedSegmentStorage.class); mSplitsSyncHelper = Mockito.mock(SplitsSyncHelper.class); mEventsManager = Mockito.mock(SplitEventsManager.class); - mTask = new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorage, mChangeNumber, mEventsManager); - when(mSplitsSyncHelper.sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); - when(mSplitsSyncHelper.sync(anyLong(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + mTask = new SplitsUpdateTask(mSplitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorage, mChangeNumber, mRbsChangeNumber, mEventsManager); + when(mSplitsSyncHelper.sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); + when(mSplitsSyncHelper.sync(any(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES))).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.GENERIC_TASK)); loadSplitChanges(); } @Test public void correctExecution() throws HttpFetcherException { when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(10L); mTask.execute(); - verify(mSplitsSyncHelper).sync(mChangeNumber, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + verify(mSplitsSyncHelper).sync(argThat(new ArgumentMatcher() { + @Override + public boolean matches(SplitsSyncHelper.SinceChangeNumbers argument) { + return argument.getFlagsSince() == 234567833L && argument.getRbsSince() == 234567830L; + } + }), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); } @Test @@ -60,7 +72,16 @@ public void storedChangeNumBigger() throws HttpFetcherException { mTask.execute(); - verify(mSplitsSyncHelper, never()).sync(anyLong(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); + } + + @Test + public void storedRbsChangeNumBigger() throws HttpFetcherException { + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(mRbsChangeNumber + 100L); + + mTask.execute(); + + verify(mSplitsSyncHelper, never()).sync(any(), anyBoolean(), anyBoolean(), eq(ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES)); } @After diff --git a/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java b/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java index 3c791fd70..db145d724 100644 --- a/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java +++ b/src/test/java/io/split/android/client/service/SplitsChangesCheckerTest.java @@ -1,7 +1,6 @@ package io.split.android.client.service; import org.junit.Assert; -import org.junit.Rule; import org.junit.Test; import io.split.android.client.service.synchronizer.SplitsChangeChecker; @@ -11,21 +10,21 @@ public class SplitsChangesCheckerTest { @Test public void testSplitsChangesArrived() { - boolean result = mSplitsChangesChecker.splitsHaveChanged( 100, 101); + boolean result = mSplitsChangesChecker.changeNumberIsNewer( 100, 101); Assert.assertTrue(result); } @Test public void testSplitsNoChangesMinorChangeNumber() { - boolean result = mSplitsChangesChecker.splitsHaveChanged( 101, 100); + boolean result = mSplitsChangesChecker.changeNumberIsNewer( 101, 100); Assert.assertFalse(result); } @Test public void testSplitsNoChangesEqualChangeNumber() { - boolean result = mSplitsChangesChecker.splitsHaveChanged( 100, 100); + boolean result = mSplitsChangesChecker.changeNumberIsNewer( 100, 100); Assert.assertFalse(result); } diff --git a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java index 2ff74c234..b6e0793ec 100644 --- a/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java +++ b/src/test/java/io/split/android/client/service/SplitsSyncHelperTest.java @@ -2,10 +2,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -16,26 +19,37 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.dtos.RuleBasedSegmentChange; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.network.SplitHttpHeadersBuilder; import io.split.android.client.service.executor.SplitTaskExecutionInfo; import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.http.HttpFetcher; import io.split.android.client.service.http.HttpFetcherException; +import io.split.android.client.service.http.HttpStatus; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.sseclient.BackoffCounter; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageImplTest; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.ProcessedSplitChange; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.OperationType; @@ -45,32 +59,38 @@ public class SplitsSyncHelperTest { @Mock - HttpFetcher mSplitsFetcher; + HttpFetcher mSplitsFetcher; @Mock SplitsStorage mSplitsStorage; - SplitChange mSplitChange = null; + TargetingRulesChange mTargetingRulesChange = TargetingRulesChange.create(SplitChange.create(-1, 1506703262916L, Collections.emptyList()), RuleBasedSegmentChange.create(-1, 262325L, Collections.singletonList(RuleBasedSegmentStorageImplTest.createRuleBasedSegment("rbs")))); @Spy SplitChangeProcessor mSplitChangeProcessor; + @Spy + RuleBasedSegmentChangeProcessor mRuleBasedSegmentChangeProcessor; @Mock private TelemetryRuntimeProducer mTelemetryRuntimeProducer; @Mock private BackoffCounter mBackoffCounter; + @Mock + private RuleBasedSegmentStorageProducer mRuleBasedSegmentStorageProducer; + @Mock + private GeneralInfoStorage mGeneralInfoStorage; private SplitsSyncHelper mSplitsSyncHelper; - private final Map mDefaultParams = new HashMap<>(); - private final Map mSecondFetchParams = new HashMap<>(); + private Map mDefaultParams = new HashMap<>(); + private Map mSecondFetchParams = new HashMap<>(); private AutoCloseable mAutoCloseable; @Before public void setup() { mAutoCloseable = MockitoAnnotations.openMocks(this); mDefaultParams.clear(); - mDefaultParams.put("s", "1.1"); - mDefaultParams.put("since", -1L); - mSecondFetchParams.clear(); - mSecondFetchParams.put("since", 1506703262916L); - mSplitsSyncHelper = new SplitsSyncHelper(mSplitsFetcher, mSplitsStorage, mSplitChangeProcessor, mTelemetryRuntimeProducer, mBackoffCounter, "1.1"); + mDefaultParams = getSinceParams(-1, -1); + mSecondFetchParams = getSinceParams(1506703262916L, 262325L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(262325L); + // Use a short proxy check interval for all tests + mSplitsSyncHelper = new SplitsSyncHelper(mSplitsFetcher, mSplitsStorage, mSplitChangeProcessor, mRuleBasedSegmentChangeProcessor, mRuleBasedSegmentStorageProducer, mGeneralInfoStorage, mTelemetryRuntimeProducer, mBackoffCounter, "1.3", false, 1L); loadSplitChanges(); } @@ -87,18 +107,22 @@ public void tearDown() { public void correctSyncExecution() throws HttpFetcherException { // On correct execution without having clear param // should execute fetcher, update storage and avoid clearing splits cache - when(mSplitsFetcher.execute(mDefaultParams, null)).thenReturn(mSplitChange); - SplitChange secondSplitChange = mSplitChange; - secondSplitChange.since = mSplitChange.till; - when(mSplitsFetcher.execute(mSecondFetchParams, null)).thenReturn(secondSplitChange); + SplitChange secondSplitChange = mTargetingRulesChange.getFeatureFlagsChange(); + secondSplitChange.since = mTargetingRulesChange.getFeatureFlagsChange().till; + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(secondSplitChange, RuleBasedSegmentChange.create(262325L, 262325L, Collections.emptyList()))); when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L).thenReturn(262325L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, times(1)).update(any()); - verify(mSplitChangeProcessor, times(1)).process(mSplitChange); + verify(mSplitChangeProcessor, times(1)).process(mTargetingRulesChange.getFeatureFlagsChange()); + verify(mRuleBasedSegmentChangeProcessor).process((List) any(), anyLong()); + verify(mRuleBasedSegmentStorageProducer, times(1)).update(any(), any(), eq(262325L)); verify(mSplitsStorage, never()).clear(); + verify(mRuleBasedSegmentStorageProducer, never()).clear(); assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); } @@ -109,17 +133,17 @@ public void correctSyncExecutionNoCache() throws HttpFetcherException { Map headers = new HashMap<>(); headers.put(SplitHttpHeadersBuilder.CACHE_CONTROL_HEADER, SplitHttpHeadersBuilder.CACHE_CONTROL_NO_CACHE); - when(mSplitsFetcher.execute(mDefaultParams, headers)).thenReturn(mSplitChange); - SplitChange secondSplitChange = mSplitChange; - secondSplitChange.since = mSplitChange.till; - when(mSplitsFetcher.execute(mSecondFetchParams, null)).thenReturn(secondSplitChange); + SplitChange secondSplitChange = mTargetingRulesChange.getFeatureFlagsChange(); + secondSplitChange.since = mTargetingRulesChange.getFeatureFlagsChange().till; + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(secondSplitChange, RuleBasedSegmentChange.create(262325L, 262325L, Collections.emptyList()))); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, headers); verify(mSplitsStorage, times(1)).update(any()); - verify(mSplitChangeProcessor, times(1)).process(mSplitChange); + verify(mSplitChangeProcessor, times(1)).process(mTargetingRulesChange.getFeatureFlagsChange()); verify(mSplitsStorage, never()).clear(); assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); } @@ -130,45 +154,45 @@ public void fetcherSyncException() throws HttpFetcherException { .thenThrow(HttpFetcherException.class); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, never()).update(any()); verify(mSplitsStorage, never()).clear(); - verify(mSplitChangeProcessor, never()).process(mSplitChange); + verify(mSplitChangeProcessor, never()).process(mTargetingRulesChange.getFeatureFlagsChange()); assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); } @Test public void storageException() throws HttpFetcherException { - when(mSplitsFetcher.execute(mDefaultParams, null)).thenReturn(mSplitChange); + when(mSplitsFetcher.execute(mDefaultParams, null)).thenReturn(mTargetingRulesChange); doThrow(NullPointerException.class).when(mSplitsStorage).update(any(ProcessedSplitChange.class)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, times(1)).update(any()); verify(mSplitsStorage, times(1)).clear(); - verify(mSplitChangeProcessor, times(1)).process(mSplitChange); + verify(mSplitChangeProcessor, times(1)).process(mTargetingRulesChange.getFeatureFlagsChange()); assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); } @Test public void shouldClearStorageAfterFetch() throws HttpFetcherException { - when(mSplitsFetcher.execute(mDefaultParams, null)).thenReturn(mSplitChange); - SplitChange secondSplitChange = mSplitChange; - secondSplitChange.since = mSplitChange.till; - when(mSplitsFetcher.execute(mSecondFetchParams, null)).thenReturn(secondSplitChange); + SplitChange secondSplitChange = mTargetingRulesChange.getFeatureFlagsChange(); + secondSplitChange.since = mTargetingRulesChange.getFeatureFlagsChange().till; + when(mSplitsFetcher.execute(any(), any())) + .thenReturn(TargetingRulesChange.create(secondSplitChange, RuleBasedSegmentChange.create(262325L, 262325L, Collections.emptyList()))); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher, times(1)).execute(mDefaultParams, null); verify(mSplitsStorage, times(1)).update(any()); verify(mSplitsStorage, times(1)).clear(); - verify(mSplitChangeProcessor, times(1)).process(mSplitChange); + verify(mSplitChangeProcessor, times(1)).process(mTargetingRulesChange.getFeatureFlagsChange()); assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); } @@ -179,19 +203,19 @@ public void errorIsRecordedInTelemetry() throws HttpFetcherException { .thenThrow(new HttpFetcherException("error", "error", 500)); when(mSplitsStorage.getTill()).thenReturn(-1L); - mSplitsSyncHelper.sync(-1, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mTelemetryRuntimeProducer).recordSyncError(OperationType.SPLITS, 500); } @Test public void performSplitsFetchUntilSinceEqualsTill() throws HttpFetcherException { - SplitChange firstSplitChange = getSplitChange(-1, 2); - SplitChange secondSplitChange = getSplitChange(2, 3); - SplitChange thirdSplitChange = getSplitChange(3, 3); - Map firstParams = getSinceParams(-1L); - Map secondParams = getSinceParams(2L); - Map thirdParams = getSinceParams(3L); + TargetingRulesChange firstSplitChange = getSplitChange(-1, 2); + TargetingRulesChange secondSplitChange = getSplitChange(2, 3); + TargetingRulesChange thirdSplitChange = getSplitChange(3, 3); + Map firstParams = getSinceParams(-1L, -1L); + Map secondParams = getSinceParams(2L, 262325L); + Map thirdParams = getSinceParams(3L, 262325L); when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 3L); @@ -199,7 +223,7 @@ public void performSplitsFetchUntilSinceEqualsTill() throws HttpFetcherException when(mSplitsFetcher.execute(eq(secondParams), any())).thenReturn(secondSplitChange); when(mSplitsFetcher.execute(eq(thirdParams), any())).thenReturn(thirdSplitChange); - mSplitsSyncHelper.sync(3, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(3, -1), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, times(3)).getTill(); verify(mSplitsFetcher).execute(eq(firstParams), any()); @@ -209,28 +233,26 @@ public void performSplitsFetchUntilSinceEqualsTill() throws HttpFetcherException @Test public void performSplitFetchUntilStoredChangeNumberIsGreaterThanRequested() throws HttpFetcherException { - SplitChange firstSplitChange = getSplitChange(-1, 2); - SplitChange secondSplitChange = getSplitChange(2, 4); - Map firstParams = getSinceParams(-1L); - Map secondParams = getSinceParams(2L); + TargetingRulesChange firstSplitChange = getSplitChange(-1, 2); + TargetingRulesChange secondSplitChange = getSplitChange(2, 4); + TargetingRulesChange thirdSplitChange = getSplitChange(4, 4); when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 4L); - when(mSplitsFetcher.execute(eq(firstParams), any())).thenReturn(firstSplitChange); - when(mSplitsFetcher.execute(eq(secondParams), any())).thenReturn(secondSplitChange); + when(mSplitsFetcher.execute(any(), any())).thenReturn(firstSplitChange).thenReturn(secondSplitChange).thenReturn(thirdSplitChange); - mSplitsSyncHelper.sync(3, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(3, -1), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, times(3)).getTill(); - verify(mSplitsFetcher).execute(eq(firstParams), any()); - verify(mSplitsFetcher).execute(eq(secondParams), any()); + verify(mSplitsFetcher, times(3)).execute(any(), any()); } @Test - public void syncWithClearBeforeUpdateOnlyClearsStorageOnce() { + public void syncWithClearBeforeUpdateOnlyClearsStorageOnce() throws HttpFetcherException { + when(mSplitsFetcher.execute(mDefaultParams, null)).thenReturn(mTargetingRulesChange); when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 4L); - mSplitsSyncHelper.sync(3, true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(3, -1), true, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage).clear(); } @@ -239,7 +261,7 @@ public void syncWithClearBeforeUpdateOnlyClearsStorageOnce() { public void syncWithoutClearBeforeUpdateDoesNotClearStorage() { when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 4L); - mSplitsSyncHelper.sync(3, false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(3, -1), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsStorage, never()).clear(); } @@ -247,6 +269,7 @@ public void syncWithoutClearBeforeUpdateDoesNotClearStorage() { @Test public void cdnIsBypassedWhenNeeded() throws HttpFetcherException { when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L); + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L); when(mSplitsFetcher.execute(anyMap(), any())).thenReturn( getSplitChange(-1, 2), getSplitChange(2, 3), @@ -263,14 +286,50 @@ public void cdnIsBypassedWhenNeeded() throws HttpFetcherException { getSplitChange(4, 4) ); - mSplitsSyncHelper.sync(4, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(4, -1), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); Map headers = new HashMap<>(); headers.put("Cache-Control", "no-cache"); - Map firstParams = getSinceParams(-1L); - Map secondParams = getSinceParams(2L); - Map thirdParams = getSinceParams(3L); - Map bypassedParams = getSinceParams(3L); + Map firstParams = getSinceParams(-1L, -1L); + Map secondParams = getSinceParams(2L, -1L); + Map thirdParams = getSinceParams(3L, -1L); + Map bypassedParams = getSinceParams(3L, -1L); + bypassedParams.put("till", 3L); + + verify(mSplitsFetcher).execute(firstParams, headers); + verify(mSplitsFetcher).execute(secondParams, headers); + verify(mSplitsFetcher, times(10)).execute(thirdParams, headers); + verify(mSplitsFetcher).execute(bypassedParams, headers); + } + + @Test + public void cdnIsBypassedWhenNeededWithRuleBasedSegments() throws HttpFetcherException { + when(mRuleBasedSegmentStorageProducer.getChangeNumber()).thenReturn(-1L, 2L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L); + when(mSplitsStorage.getTill()).thenReturn(-1L); + when(mSplitsFetcher.execute(anyMap(), any())).thenReturn( + getRuleBasedSegmentChange(-1, 2), + getRuleBasedSegmentChange(2, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(3, 3), + getRuleBasedSegmentChange(4, 4) + ); + + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, 4), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + Map headers = new HashMap<>(); + headers.put("Cache-Control", "no-cache"); + Map firstParams = getSinceParams(-1L, -1L); + Map secondParams = getSinceParams(-1L, 2L); + Map thirdParams = getSinceParams(-1L, 3L); + Map bypassedParams = getSinceParams(-1L, 3L); bypassedParams.put("till", 3L); verify(mSplitsFetcher).execute(firstParams, headers); @@ -291,7 +350,7 @@ public void backoffIsAppliedWhenRetryingSplits() throws HttpFetcherException { getSplitChange(4, 4) ); - mSplitsSyncHelper.sync(4, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(4, -1), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mBackoffCounter).resetCounter(); verify(mBackoffCounter, times(2)).getNextRetryTime(); @@ -299,11 +358,12 @@ public void backoffIsAppliedWhenRetryingSplits() throws HttpFetcherException { @Test public void replaceTillWhenFilterHasChanged() throws HttpFetcherException { - mSplitsSyncHelper.sync(14829471, true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(14829471, -1), true, true, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); Map params = new HashMap<>(); - params.put("s", "1.1"); + params.put("s", "1.3"); params.put("since", -1L); + params.put("rbSince", -1L); verify(mSplitsFetcher).execute(eq(params), eq(null)); verifyNoMoreInteractions(mSplitsFetcher); } @@ -314,7 +374,7 @@ public void returnTaskInfoToDoNotRetryWhenHttpFetcherExceptionStatusCodeIs414() .thenThrow(new HttpFetcherException("error", "error", 414)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertEquals(true, result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @@ -325,7 +385,7 @@ public void doNotRetryFlagIsNullWhenFetcherExceptionStatusCodeIsNot414() throws .thenThrow(new HttpFetcherException("error", "error", 500)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @@ -336,7 +396,7 @@ public void returnTaskInfoToDoNotRetryWhenHttpFetcherExceptionStatusCodeIs9009() .thenThrow(new HttpFetcherException("error", "error", 9009)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertEquals(true, result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @@ -347,14 +407,14 @@ public void doNotRetryFlagIsNullWhenFetcherExceptionStatusCodeIsNot9009() throws .thenThrow(new HttpFetcherException("error", "error", 500)); when(mSplitsStorage.getTill()).thenReturn(-1L); - SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(-1, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + SplitTaskExecutionInfo result = mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); } @Test public void defaultQueryParamOrderIsCorrect() throws HttpFetcherException { - mSplitsSyncHelper.sync(100, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(100, -1), ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); verify(mSplitsFetcher).execute(argThat(new ArgumentMatcher>() { @Override @@ -366,27 +426,137 @@ public boolean matches(Map argument) { }), any()); } + @Test + public void proxyErrorTriggersFallbackAndOmitsRbSince() throws Exception { + // Before first sync (no fallback) + when(mGeneralInfoStorage.getLastProxyUpdateTimestamp()).thenReturn(0L); + + // Simulate proxy outdated error (400 with latest spec) + when(mSplitsFetcher.execute(any(), any())) + .thenThrow(new HttpFetcherException("Proxy outdated", "Proxy outdated", HttpStatus.INTERNAL_PROXY_OUTDATED.getCode())) // Use real status code + .thenReturn(TargetingRulesChange.create(SplitChange.create(-1, 2, Collections.emptyList()), RuleBasedSegmentChange.create(-1, 2, Collections.emptyList()))) + .thenReturn(TargetingRulesChange.create(SplitChange.create(2, 2, Collections.emptyList()), RuleBasedSegmentChange.create(2, 2, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L, -1L); + + // First sync triggers the proxy error and sets fallback mode + try { + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + } catch (Exception ignored) {} + + // Now simulate fallback state persisted + when(mGeneralInfoStorage.getLastProxyUpdateTimestamp()).thenReturn(System.currentTimeMillis()); + + // Second sync, now in fallback mode, should use fallback spec and omit rbSince + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + // Capture and verify the params + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(Map.class); + verify(mSplitsFetcher, atLeastOnce()).execute(paramsCaptor.capture(), any()); + boolean foundFallback = false; + for (Map params : paramsCaptor.getAllValues()) { + System.out.println("Captured params: " + params); + + if ("1.2".equals(params.get("s")) && params.get("since") != null && !params.containsKey("rbSince")) { + foundFallback = true; + break; + } + } + assertTrue("Expected a fallback call with s=1.2 and no rbSince", foundFallback); + } + + @Test + public void fallbackPersistsUntilIntervalElapses() throws Exception { + // Simulate proxy outdated error + long timestamp = System.currentTimeMillis(); + when(mGeneralInfoStorage.getLastProxyUpdateTimestamp()).thenReturn(timestamp); + when(mSplitsFetcher.execute(any(), any())) + .thenThrow(new HttpFetcherException("Proxy outdated", "Proxy outdated", HttpStatus.INTERNAL_PROXY_OUTDATED.getCode())) + // First fallback fetch returns till=2, second fallback fetch returns till=2 (still not caught up), + // third fallback fetch returns till=3 (caught up, loop can exit) + .thenReturn(TargetingRulesChange.create(SplitChange.create(-1, 2, Collections.emptyList()), RuleBasedSegmentChange.create(-1, 2, Collections.emptyList()))) + .thenReturn(TargetingRulesChange.create(SplitChange.create(3, 3, Collections.emptyList()), RuleBasedSegmentChange.create(3, 3, Collections.emptyList()))); + // Simulate advancing change numbers for storage + when(mSplitsStorage.getTill()).thenReturn(-1L, 2L, 3L); + // Trigger fallback + try { mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } catch (Exception ignored) {} + // Simulate time NOT elapsed + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + verify(mSplitsFetcher, times(1)).execute(argThat(params -> + "1.2".equals(params.get("s")) && + !params.containsKey("rbSince") + ), any()); + } + + @Test + public void generic400InFallbackDoesNotResetToNone() throws Exception { + // Simulate proxy outdated error + when(mSplitsFetcher.execute(any(), any())) + .thenThrow(new HttpFetcherException("Proxy outdated", "Proxy outdated", HttpStatus.INTERNAL_PROXY_OUTDATED.getCode())) + .thenThrow(new HttpFetcherException("Bad Request", "Bad Request", 400)); + when(mSplitsStorage.getTill()).thenReturn(-1L); + // Trigger fallback + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + // Next call gets a generic 400, should remain in fallback + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + verify(mSplitsFetcher, times(2)).execute(argThat(params -> + "1.2".equals(params.get("s")) && + !params.containsKey("rbSince") + ), any()); + } + + @Test + public void successfulRecoveryReturnsToNormalSpec() throws Exception { + // Simulate proxy outdated error, then recovery + when(mSplitsFetcher.execute(any(), any())) + .thenThrow(new HttpFetcherException("Proxy outdated", "Proxy outdated", HttpStatus.INTERNAL_PROXY_OUTDATED.getCode())) + .thenReturn(TargetingRulesChange.create(SplitChange.create(-1, 2, Collections.emptyList()), RuleBasedSegmentChange.create(-1, 2, Collections.emptyList()))) + .thenReturn(TargetingRulesChange.create(SplitChange.create(2, 2, Collections.emptyList()), RuleBasedSegmentChange.create(2, 2, Collections.emptyList()))); + when(mSplitsStorage.getTill()).thenReturn(-1L); + // Trigger fallback + try { mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); } catch (Exception ignored) {} + // Simulate interval elapsed + when(mGeneralInfoStorage.getLastProxyUpdateTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)); + mSplitsSyncHelper.sync(getSinceChangeNumbers(-1, -1L), false, false, ServiceConstants.ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + // Next call should be with latest spec + verify(mSplitsFetcher, times(1)).execute(argThat(params -> + "1.3".equals(params.get("s")) && + params.containsKey("rbSince") + ), any()); + } + + private static SplitsSyncHelper.SinceChangeNumbers getSinceChangeNumbers(int flagsSince, long rbsSince) { + return new SplitsSyncHelper.SinceChangeNumbers(flagsSince, rbsSince); + } + private void loadSplitChanges() { - if (mSplitChange == null) { + if (mTargetingRulesChange == null) { FileHelper fileHelper = new FileHelper(); - mSplitChange = fileHelper.loadSplitChangeFromFile("split_changes_1.json"); + mTargetingRulesChange = TargetingRulesChange.create(fileHelper.loadSplitChangeFromFile("split_changes_1.json")); } } - private Map getSinceParams(long since) { + private Map getSinceParams(long since, long rbSince) { Map params = new LinkedHashMap<>(); - params.put("s", "1.1"); + params.put("s", "1.3"); params.put("since", since); + params.put("rbSince", rbSince); return params; } - private SplitChange getSplitChange(int since, int till) { + private TargetingRulesChange getSplitChange(int since, int till) { SplitChange splitChange = new SplitChange(); splitChange.since = since; splitChange.till = till; splitChange.splits = new ArrayList<>(); - return splitChange; + return TargetingRulesChange.create(splitChange); + } + + private TargetingRulesChange getRuleBasedSegmentChange(int since, int till) { + RuleBasedSegmentChange ruleBasedSegmentChange = RuleBasedSegmentChange.create(since, till, new ArrayList<>()); + + return TargetingRulesChange.create(SplitChange.create(10, 10, new ArrayList<>()), ruleBasedSegmentChange); } } diff --git a/src/test/java/io/split/android/client/service/SynchronizerTest.java b/src/test/java/io/split/android/client/service/SynchronizerTest.java index 98a18c766..6a5e4aa15 100644 --- a/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -42,7 +42,7 @@ import io.split.android.client.api.Key; import io.split.android.client.dtos.Event; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.events.SplitEventsManager; import io.split.android.client.impressions.DecoratedImpression; import io.split.android.client.impressions.Impression; @@ -163,7 +163,7 @@ public void setup(SplitClientConfig splitClientConfig, ImpressionManagerConfig.M mTaskExecutor = taskExecutor; mSingleThreadedTaskExecutor = spy(new SplitTaskExecutorStub()); - HttpFetcher splitsFetcher = Mockito.mock(HttpFetcher.class); + HttpFetcher splitsFetcher = Mockito.mock(HttpFetcher.class); HttpFetcher mySegmentsFetcher = Mockito.mock(HttpFetcher.class); HttpRecorder> eventsRecorder = Mockito.mock(HttpRecorder.class); HttpRecorder> impressionsRecorder = Mockito.mock(HttpRecorder.class); @@ -704,11 +704,22 @@ public void beingNotifiedOfMySegmentsSyncTriggersMySegmentsLoad() { public void synchronizeSplitsWithSince() { setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); SplitsUpdateTask task = mock(SplitsUpdateTask.class); - when(mTaskFactory.createSplitsUpdateTask(1000)).thenReturn(task); + when(mTaskFactory.createSplitsUpdateTask(1000L, -1L)).thenReturn(task); mSynchronizer.synchronizeSplits(1000); - verify(mFeatureFlagsSynchronizer).synchronize(1000); + verify(mFeatureFlagsSynchronizer).synchronize(1000L, null); + } + + @Test + public void synchronizeRuleBasedSegmentsWithSince() { + setup(SplitClientConfig.builder().synchronizeInBackground(false).build()); + SplitsUpdateTask task = mock(SplitsUpdateTask.class); + when(mTaskFactory.createSplitsUpdateTask(-1L, 1000L)).thenReturn(task); + + mSynchronizer.synchronizeRuleBasedSegments(1000); + + verify(mFeatureFlagsSynchronizer).synchronize(null, 1000L); } @Test diff --git a/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java b/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java new file mode 100644 index 000000000..5cbdf45ef --- /dev/null +++ b/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentChangeProcessorTest.java @@ -0,0 +1,41 @@ +package io.split.android.client.service.rules; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static io.split.android.client.storage.rbs.RuleBasedSegmentStorageImplTest.createRuleBasedSegment; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.dtos.Status; + +public class RuleBasedSegmentChangeProcessorTest { + + private RuleBasedSegmentChangeProcessor mProcessor; + + @Before + public void setUp() { + mProcessor = new RuleBasedSegmentChangeProcessor(); + } + + @Test + public void testProcess() { + List segments = new ArrayList<>(); + segments.add(createRuleBasedSegment("segment1", Status.ACTIVE)); + segments.add(createRuleBasedSegment("segment2", Status.ARCHIVED)); + segments.add(createRuleBasedSegment("segment3", Status.ACTIVE)); + + ProcessedRuleBasedSegmentChange result = mProcessor.process(segments, 123L); + + assertEquals(2, result.getActive().size()); + assertEquals(1, result.getArchived().size()); + assertEquals(123L, result.getChangeNumber()); + assertTrue(result.getActive().stream().map(RuleBasedSegment::getName).anyMatch("segment1"::equals)); + assertTrue(result.getActive().stream().map(RuleBasedSegment::getName).anyMatch("segment3"::equals)); + assertTrue(result.getArchived().stream().map(RuleBasedSegment::getName).anyMatch("segment2"::equals)); + } +} diff --git a/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java b/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java new file mode 100644 index 000000000..de9113ed2 --- /dev/null +++ b/src/test/java/io/split/android/client/service/rules/RuleBasedSegmentInPlaceUpdateTaskTest.java @@ -0,0 +1,97 @@ +package io.split.android.client.service.rules; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static io.split.android.client.storage.rbs.RuleBasedSegmentStorageImplTest.createRuleBasedSegment; + +import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.events.ISplitEventsManager; +import io.split.android.client.events.SplitInternalEvent; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; + +public class RuleBasedSegmentInPlaceUpdateTaskTest { + + private RuleBasedSegmentInPlaceUpdateTask mTask; + private RuleBasedSegmentStorage mRuleBasedSegmentStorage; + private RuleBasedSegmentChangeProcessor mChangeProcessor; + private ISplitEventsManager mEventsManager; + + @Before + public void setUp() { + mChangeProcessor = mock(RuleBasedSegmentChangeProcessor.class); + mRuleBasedSegmentStorage = mock(RuleBasedSegmentStorage.class); + mEventsManager = mock(ISplitEventsManager.class); + } + + @Test + public void splitEventsManagerIsNotifiedWithUpdateEvent() { + RuleBasedSegment ruleBasedSegment = createRuleBasedSegment("segment1"); + long changeNumber = 123L; + + when(mChangeProcessor.process(ruleBasedSegment, changeNumber)).thenReturn(new ProcessedRuleBasedSegmentChange(Set.of(ruleBasedSegment), Collections.emptySet(), 123L, System.currentTimeMillis())); + when(mRuleBasedSegmentStorage.update(Set.of(ruleBasedSegment), Set.of(), changeNumber)).thenReturn(true); + + mTask = getTask(ruleBasedSegment, changeNumber); + + mTask.execute(); + + verify(mEventsManager).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + } + + @Test + public void splitEventsManagerIsNotNotifiedWhenUpdateResultIsFalse() { + RuleBasedSegment ruleBasedSegment = createRuleBasedSegment("segment1"); + long changeNumber = 123L; + + when(mChangeProcessor.process(ruleBasedSegment, changeNumber)).thenReturn(new ProcessedRuleBasedSegmentChange(Set.of(ruleBasedSegment), Collections.emptySet(), 123L, System.currentTimeMillis())); + when(mRuleBasedSegmentStorage.update(Set.of(ruleBasedSegment), Set.of(), changeNumber)).thenReturn(false); + + mTask = getTask(ruleBasedSegment, changeNumber); + + mTask.execute(); + + verify(mEventsManager, times(0)).notifyInternalEvent(SplitInternalEvent.RULE_BASED_SEGMENTS_UPDATED); + } + + @Test + public void changeProcessorIsCalledWithRuleBasedSegmentAndChangeNumber() { + RuleBasedSegment ruleBasedSegment = createRuleBasedSegment("segment1"); + long changeNumber = 123L; + + mTask = getTask(ruleBasedSegment, changeNumber); + + mTask.execute(); + + verify(mChangeProcessor).process(ruleBasedSegment, changeNumber); + } + + @Test + public void updateIsCalledOnStorage() { + RuleBasedSegment ruleBasedSegment = createRuleBasedSegment("segment1"); + long changeNumber = 123L; + + when(mChangeProcessor.process(ruleBasedSegment, changeNumber)).thenReturn(new ProcessedRuleBasedSegmentChange(Set.of(ruleBasedSegment), Collections.emptySet(), 123L, System.currentTimeMillis())); + + mTask = getTask(ruleBasedSegment, changeNumber); + + mTask.execute(); + + verify(mRuleBasedSegmentStorage).update(Set.of(ruleBasedSegment), Set.of(), changeNumber); + } + + @NonNull + private RuleBasedSegmentInPlaceUpdateTask getTask(RuleBasedSegment ruleBasedSegment, long changeNumber) { + return new RuleBasedSegmentInPlaceUpdateTask(mRuleBasedSegmentStorage, + mChangeProcessor, mEventsManager, ruleBasedSegment, changeNumber); + } +} diff --git a/src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java b/src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java new file mode 100644 index 000000000..2ce2ded65 --- /dev/null +++ b/src/test/java/io/split/android/client/service/rules/TargetingRulesResponseParserTest.java @@ -0,0 +1,77 @@ +package io.split.android.client.service.rules; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Set; + +import io.split.android.client.dtos.Excluded; +import io.split.android.client.dtos.ExcludedSegment; +import io.split.android.client.dtos.TargetingRulesChange; +import io.split.android.client.service.http.HttpResponseParserException; +import io.split.android.helpers.FileHelper; + +public class TargetingRulesResponseParserTest { + + private TargetingRulesResponseParser parser; + private FileHelper fileHelper; + + @Before + public void setUp() { + parser = new TargetingRulesResponseParser(); + fileHelper = new FileHelper(); + } + + @Test + public void parsesNewTargetingRulesChangeJson() throws Exception { + String json = fileHelper.loadFileContent("split_changes_small.json"); + TargetingRulesChange result = parser.parse(json); + assertNotNull(result); + assertNotNull(result.getFeatureFlagsChange()); + assertEquals("FACUNDO_TEST", result.getFeatureFlagsChange().splits.get(0).name); + assertEquals(1506703262916L, result.getFeatureFlagsChange().till); + assertEquals(-1, result.getFeatureFlagsChange().since); + assertEquals(1506703262920L, result.getRuleBasedSegmentsChange().getSince()); + assertEquals(1506703263000L, result.getRuleBasedSegmentsChange().getTill()); + assertEquals("mauro_rule_based_segment", result.getRuleBasedSegmentsChange().getSegments().get(0).getName()); + Excluded excluded = result.getRuleBasedSegmentsChange().getSegments().get(0).getExcluded(); + assertEquals(1, excluded.getKeys().size()); + Set excludedSegments = excluded.getSegments(); + assertEquals(4, excludedSegments.size()); + // check that it contains 4 excluded segments: standard, large, rule-based and unsupported + assertTrue(excludedSegments.contains(ExcludedSegment.standard("segment_test"))); + assertTrue(excludedSegments.contains(ExcludedSegment.large("segment_test2"))); + assertTrue(excludedSegments.contains(ExcludedSegment.ruleBased("segment_test3"))); + } + + @Test + public void parsesLegacySplitChangeJson() throws Exception { + String json = fileHelper.loadFileContent("split_changes_legacy.json"); + TargetingRulesChange result = parser.parse(json); + assertNotNull(result); + assertNotNull(result.getFeatureFlagsChange()); + assertEquals("FACUNDO_TEST", result.getFeatureFlagsChange().splits.get(0).name); + assertEquals(1506703262916L, result.getFeatureFlagsChange().till); + assertEquals(-1, result.getFeatureFlagsChange().since); + assertEquals(-1, result.getRuleBasedSegmentsChange().getSince()); + assertEquals(-1, result.getRuleBasedSegmentsChange().getTill()); + assertTrue(result.getRuleBasedSegmentsChange().getSegments().isEmpty()); + } + + @Test + public void parseNullReturnsNull() throws HttpResponseParserException { + TargetingRulesChange result = parser.parse(null); + assertNull(result); + } + + @Test + public void parseEmptyReturnsNull() throws HttpResponseParserException { + TargetingRulesChange result = parser.parse(""); + assertNull(result); + } +} diff --git a/src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java b/src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java new file mode 100644 index 000000000..7d8792a84 --- /dev/null +++ b/src/test/java/io/split/android/client/service/splits/OutdatedSplitProxyHandlerTest.java @@ -0,0 +1,90 @@ +package io.split.android.client.service.splits; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class OutdatedSplitProxyHandlerTest { + private static final String LATEST_SPEC = "1.3"; + private static final String PREVIOUS_SPEC = "1.2"; + private GeneralInfoStorage mockStorage; + private OutdatedSplitProxyHandler handler; + + @Before + public void setUp() { + mockStorage = mock(GeneralInfoStorage.class); + when(mockStorage.getLastProxyUpdateTimestamp()).thenReturn(0L); + handler = new OutdatedSplitProxyHandler(LATEST_SPEC, PREVIOUS_SPEC, false, mockStorage, 1000L); // 1s interval for tests + } + + @Test + public void initialStateIsNoneAndUsesLatestSpec() { + assertFalse(handler.isFallbackMode()); + assertFalse(handler.isRecoveryMode()); + assertEquals(LATEST_SPEC, handler.getCurrentSpec()); + } + + @Test + public void proxyErrorTriggersFallbackModeAndUsesPreviousSpec() { + handler.trackProxyError(); + assertTrue(handler.isFallbackMode()); + assertEquals(PREVIOUS_SPEC, handler.getCurrentSpec()); + } + + @Test + public void fallbackModePersistsUntilIntervalElapses() { + handler.trackProxyError(); + assertTrue(handler.isFallbackMode()); + // simulate a call to performProxyCheck within interval + handler.performProxyCheck(); + assertTrue(handler.isFallbackMode()); + assertEquals(PREVIOUS_SPEC, handler.getCurrentSpec()); + } + + @Test + public void intervalElapsedEntersRecoveryModeAndUsesLatestSpec() { + handler.trackProxyError(); + // simulate time passing (10 seconds ago) + long now = System.currentTimeMillis(); + when(mockStorage.getLastProxyUpdateTimestamp()).thenReturn(now - 10000L); + // Re-create handler to force atomic long to re-read from storage + handler = new OutdatedSplitProxyHandler(LATEST_SPEC, PREVIOUS_SPEC, false, mockStorage, 1000L); + handler.performProxyCheck(); + assertTrue(handler.isRecoveryMode()); + assertEquals(LATEST_SPEC, handler.getCurrentSpec()); + } + + @Test + public void recoveryModeResetsToNoneAfterResetProxyCheckTimestamp() { + handler.trackProxyError(); + long now = System.currentTimeMillis(); + when(mockStorage.getLastProxyUpdateTimestamp()).thenReturn(now - 10000L); + handler = new OutdatedSplitProxyHandler(LATEST_SPEC, PREVIOUS_SPEC, false, mockStorage, 1000L); + handler.performProxyCheck(); + assertTrue(handler.isRecoveryMode()); + handler.resetProxyCheckTimestamp(); + // Simulate storage now returns 0L after reset + when(mockStorage.getLastProxyUpdateTimestamp()).thenReturn(0L); + handler = new OutdatedSplitProxyHandler(LATEST_SPEC, PREVIOUS_SPEC, false, mockStorage, 1000L); + handler.performProxyCheck(); + assertFalse(handler.isFallbackMode()); + assertFalse(handler.isRecoveryMode()); + assertEquals(LATEST_SPEC, handler.getCurrentSpec()); + } + + @Test + public void settingUpForBackgroundSyncIsAlwaysInNoneMode() { + handler = new OutdatedSplitProxyHandler(LATEST_SPEC, PREVIOUS_SPEC, true, mockStorage, 1000L); + handler.performProxyCheck(); + assertFalse(handler.isFallbackMode()); + assertFalse(handler.isRecoveryMode()); + assertEquals(LATEST_SPEC, handler.getCurrentSpec()); + } +} diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java b/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java index 5e6adb906..c0b734f85 100644 --- a/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/NotificationParserTest.java @@ -20,6 +20,7 @@ import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationType; import io.split.android.client.service.sseclient.notifications.OccupancyNotification; +import io.split.android.client.service.sseclient.notifications.RuleBasedSegmentChangeNotification; import io.split.android.client.service.sseclient.notifications.SplitKillNotification; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.sseclient.notifications.StreamingError; @@ -39,6 +40,7 @@ public class NotificationParserTest { private final static String CONTROL = "{\"id\":\"x2dE2TEiJL:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:OTc5Nzc4NDYz\",\"timestamp\":1584647533288,\"encoding\":\"json\",\"channel\":\"control_pri\",\"data\":\"{\\\"type\\\":\\\"CONTROL\\\",\\\"controlType\\\":\\\"STREAMING_RESUMED\\\"}\"}"; private static final String MY_LARGE_SEGMENTS_UPDATE = "{\"id\": \"diSrQttrC9:0:0\",\"clientId\": \"pri:MjcyNDE2NDUxMA==\",\"timestamp\": 1702507131100,\"encoding\": \"json\",\"channel\": \"NzM2MDI5Mzc0_MTc1MTYwODQxMQ==_memberships\",\"data\": \"{\\\"type\\\":\\\"MEMBERSHIPS_LS_UPDATE\\\",\\\"cn\\\":1702507130121,\\\"n\\\":[\\\"android_test\\\"],\\\"c\\\":2,\\\"u\\\":2,\\\"d\\\":\\\"eJwEwLsRwzAMA9BdWKsg+IFBraJTkRXS5rK7388+tg+KdC8+jq4eBBQLFcUnO8FAAC36gndOSEyFqJFP32Vf2+f+3wAAAP//hUQQ9A==\\\",\\\"i\\\":100,\\\"h\\\":1,\\\"s\\\":325}\"}"; + private static final String RULE_BASED_SEGMENT_UPDATE = "{\"id\":\"1111\",\"clientId\":\"pri:ODc1NjQyNzY1\",\"timestamp\":1588254699236,\"encoding\":\"json\",\"channel\":\"xxxx_xxxx_flags\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":111,\\\"c\\\":0,\\\"d\\\":\\\"eyJuYW1lIjoicmJzX3Rlc3QiLCJzdGF0dXMiOiJBQ1RJVkUiLCJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiZXhjbHVkZWQiOnsia2V5cyI6W10sInNlZ21lbnRzIjpbXX0sImNvbmRpdGlvbnMiOlt7Im1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX19XX0=\\\"}\"}"; @Before public void setup() { @@ -158,4 +160,14 @@ public void parseMyLargeSegmentsNotificationData() { assertEquals((Integer) 325, notification.getAlgorithmSeed()); assertEquals(HashingAlgorithm.MURMUR3_32, notification.getHashingAlgorithm()); } + + @Test + public void parseRuleBasedSegmentsNotificationData() { + IncomingNotification incomingNotification = mParser.parseIncoming(RULE_BASED_SEGMENT_UPDATE); + RuleBasedSegmentChangeNotification notification = mParser.parseRuleBasedSegmentUpdate(incomingNotification.getJsonData()); + + assertEquals(1684265694505L, notification.getChangeNumber()); + assertEquals(CompressionType.NONE, notification.getCompressionType()); + assertEquals(111, (long) notification.getPreviousChangeNumber()); + } } diff --git a/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java b/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java index 863d7805e..526417ec4 100644 --- a/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/NotificationProcessorTest.java @@ -21,10 +21,12 @@ import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.splits.SplitKillTask; import io.split.android.client.service.sseclient.notifications.IncomingNotification; +import io.split.android.client.service.sseclient.notifications.InstantUpdateChangeNotification; import io.split.android.client.service.sseclient.notifications.MembershipNotification; import io.split.android.client.service.sseclient.notifications.NotificationParser; import io.split.android.client.service.sseclient.notifications.NotificationProcessor; import io.split.android.client.service.sseclient.notifications.NotificationType; +import io.split.android.client.service.sseclient.notifications.RuleBasedSegmentChangeNotification; import io.split.android.client.service.sseclient.notifications.SplitKillNotification; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.sseclient.notifications.memberships.MembershipsNotificationProcessor; @@ -38,7 +40,7 @@ public class NotificationProcessorTest { @Mock private NotificationParser mNotificationParser; @Mock - private BlockingQueue mSplitsChangeQueue; + private BlockingQueue mSplitsChangeQueue; @Mock private IncomingNotification mIncomingNotification; private NotificationProcessor mNotificationProcessor; @@ -104,4 +106,23 @@ public void notificationProcessorDelegatesMySegmentsNotificationsV2ToRegisteredP verify(mySegmentsNotificationProcessor).process(mySegmentChangeNotification); } + + @Test + public void ruleBasedSegmentNotification() { + + RuleBasedSegmentChangeNotification notification = mock(RuleBasedSegmentChangeNotification.class); + when(mIncomingNotification.getType()).thenReturn(NotificationType.RULE_BASED_SEGMENT_UPDATE); + when(notification.getChangeNumber()).thenReturn(200L); + when(notification.getType()).thenReturn(NotificationType.RULE_BASED_SEGMENT_UPDATE); + when(mNotificationParser.parseIncoming(anyString())).thenReturn(mIncomingNotification); + when(mNotificationParser.parseRuleBasedSegmentUpdate(anyString())).thenReturn(notification); + + mNotificationProcessor.process(mIncomingNotification); + + ArgumentCaptor messageCaptor = + ArgumentCaptor.forClass(RuleBasedSegmentChangeNotification.class); + verify(mSplitsChangeQueue, times(1)).offer(messageCaptor.capture()); + Assert.assertEquals(NotificationType.RULE_BASED_SEGMENT_UPDATE, messageCaptor.getValue().getType()); + Assert.assertEquals(200L, messageCaptor.getValue().getChangeNumber()); + } } diff --git a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java b/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java index 0d8cbe339..b946113db 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SplitUpdateWorkerTest.java @@ -28,17 +28,22 @@ import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.rules.RuleBasedSegmentInPlaceUpdateTask; import io.split.android.client.service.splits.SplitInPlaceUpdateTask; +import io.split.android.client.service.sseclient.notifications.InstantUpdateChangeNotification; +import io.split.android.client.service.sseclient.notifications.NotificationType; +import io.split.android.client.service.sseclient.notifications.RuleBasedSegmentChangeNotification; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.sseclient.reactor.SplitUpdatesWorker; import io.split.android.client.service.synchronizer.Synchronizer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.utils.CompressionUtil; import io.split.android.fake.SplitTaskExecutorStub; public class SplitUpdateWorkerTest { - BlockingQueue mNotificationsQueue; + BlockingQueue mNotificationsQueue; SplitUpdatesWorker mWorker; @@ -54,16 +59,20 @@ public class SplitUpdateWorkerTest { private SplitTaskFactory mSplitTaskFactory; @Mock private SplitUpdatesWorker.Base64Decoder mBase64Decoder; + @Mock + private RuleBasedSegmentStorage mRuleBasedSegmentStorage; private static final String TEST_SPLIT = "{\"trafficTypeName\":\"account\",\"name\":\"android_test_2\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1955610140,\"seed\":-633015570,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1648733409158,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"IN_SPLIT_TREATMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":{\"split\":\"android_test_3\",\"treatments\":[\"on\"]},\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"in split android_test_3 treatment [on]\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"account\",\"attribute\":null},\"matcherType\":\"ALL_KEYS\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"booleanMatcherData\":null,\"dependencyMatcherData\":null,\"stringMatcherData\":null}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"default rule\"}]}"; + private static final String TEST_RULE_BASED_SEGMENT = "{\"changeNumber\":5,\"name\":\"mauro_rule_based_segment\",\"status\":\"ACTIVE\",\"trafficTypeName\":\"user\",\"excluded\":{\"keys\":[\"mauro@split.io\",\"gaston@split.io\"],\"segments\":[{\"name\":\"segment_test\",\"type\":\"standard\"}]},\"conditions\":[{\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"WHITELIST\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"mdp\",\"tandil\",\"bsas\"]}},{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"email\"},\"matcherType\":\"ENDS_WITH\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"@split.io\"]}}]}},{\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"regular_segment\"}}]}}]}"; @Before public void setup() { MockitoAnnotations.initMocks(this); - mNotificationsQueue = new ArrayBlockingQueue<>(50); + mNotificationsQueue = new ArrayBlockingQueue<>(10); mWorker = new SplitUpdatesWorker(mSynchronizer, mNotificationsQueue, mSplitsStorage, + mRuleBasedSegmentStorage, mCompressionUtilProvider, mSplitTaskExecutor, mSplitTaskFactory, @@ -174,6 +183,7 @@ public void newNotificationSubmitsTaskInExecutor() throws InterruptedException { when(notification.getCompressionType()).thenReturn(CompressionType.NONE); when(mockCompressor.decompress(any())).thenReturn(bytes); + when(mRuleBasedSegmentStorage.contains(any())).thenReturn(true); when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); when(mBase64Decoder.decode(anyString())).thenReturn(bytes); @@ -185,6 +195,31 @@ public void newNotificationSubmitsTaskInExecutor() throws InterruptedException { .synchronizeSplits(anyLong()); } + @Test + public void newRuleBasedSegmentNotificationSubmitsTaskOnExecutor() throws InterruptedException { + byte[] bytes = TEST_RULE_BASED_SEGMENT.getBytes(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + RuleBasedSegmentInPlaceUpdateTask updateTask = mock(RuleBasedSegmentInPlaceUpdateTask.class); + RuleBasedSegmentChangeNotification notification = getRuleBasedSegmentNotification(); + + when(mRuleBasedSegmentStorage.contains(any())).thenReturn(true); + when(mSplitTaskFactory.createRuleBasedSegmentUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(1000L); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn(bytes); + + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + when(mBase64Decoder.decode(anyString())).thenReturn(bytes); + + mNotificationsQueue.offer(notification); + Thread.sleep(100); + + verify(mSplitTaskExecutor).submit(eq(updateTask), argThat(Objects::nonNull)); + verify(mSynchronizer, never()).synchronizeSplits(anyLong()); + verify(mSynchronizer, never()).synchronizeRuleBasedSegments(anyLong()); + } + @Test public void synchronizeSplitsIsCalledOnSynchronizerWhenTaskFails() throws InterruptedException { initWorkerWithStubExecutor(); @@ -208,6 +243,29 @@ public void synchronizeSplitsIsCalledOnSynchronizerWhenTaskFails() throws Interr verify(mSynchronizer).synchronizeSplits(changeNumber + 1); } + @Test + public void synchronizeRuleBasedSegmentsIsCalledOnSynchronizerWhenTaskFails() throws InterruptedException { + initWorkerWithStubExecutor(); + + long changeNumber = 1000L; + RuleBasedSegmentInPlaceUpdateTask updateTask = mock(RuleBasedSegmentInPlaceUpdateTask.class); + RuleBasedSegmentChangeNotification notification = getRuleBasedSegmentNotification(); + CompressionUtil mockCompressor = mock(CompressionUtil.class); + + when(updateTask.execute()).thenAnswer(invocation -> SplitTaskExecutionInfo.error(SplitTaskType.RULE_BASED_SEGMENT_SYNC)); + when(mSplitTaskFactory.createRuleBasedSegmentUpdateTask(any(), anyLong())).thenReturn(updateTask); + when(mRuleBasedSegmentStorage.getChangeNumber()).thenReturn(changeNumber); + when(notification.getChangeNumber()).thenReturn(changeNumber + 1); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(mockCompressor.decompress(any())).thenReturn(TEST_RULE_BASED_SEGMENT.getBytes()); + when(mCompressionUtilProvider.get(any())).thenReturn(mockCompressor); + + mNotificationsQueue.offer(notification); + Thread.sleep(500); + + verify(mSynchronizer).synchronizeRuleBasedSegments(changeNumber + 1); + } + @Test public void synchronizeSplitsIsCalledOnSynchronizerWhenParsingFails() throws InterruptedException { initWorkerWithStubExecutor(); @@ -314,6 +372,7 @@ private void initWorkerWithStubExecutor() { mWorker = new SplitUpdatesWorker(mSynchronizer, mNotificationsQueue, mSplitsStorage, + mRuleBasedSegmentStorage, mCompressionUtilProvider, new SplitTaskExecutorStub(), mSplitTaskFactory, @@ -323,6 +382,7 @@ private void initWorkerWithStubExecutor() { private synchronized static SplitsChangeNotification getLegacyNotification() { SplitsChangeNotification mock = mock(SplitsChangeNotification.class); + when(mock.getType()).thenReturn(NotificationType.SPLIT_UPDATE); when(mock.getChangeNumber()).thenReturn(1000L); return mock; } @@ -330,9 +390,20 @@ private synchronized static SplitsChangeNotification getLegacyNotification() { private synchronized static SplitsChangeNotification getNewNotification(Long changeNumber) { SplitsChangeNotification mock = mock(SplitsChangeNotification.class); when(mock.getCompressionType()).thenReturn(CompressionType.ZLIB); + when(mock.getType()).thenReturn(NotificationType.SPLIT_UPDATE); when(mock.getData()).thenReturn(TEST_SPLIT); when(mock.getPreviousChangeNumber()).thenReturn(1000L); when(mock.getChangeNumber()).thenReturn(changeNumber == null ? 0 : changeNumber); return mock; } + + private RuleBasedSegmentChangeNotification getRuleBasedSegmentNotification() { + RuleBasedSegmentChangeNotification notification = mock(RuleBasedSegmentChangeNotification.class); + when(notification.getChangeNumber()).thenReturn(2000L); + when(notification.getCompressionType()).thenReturn(CompressionType.NONE); + when(notification.getData()).thenReturn(TEST_RULE_BASED_SEGMENT); + when(notification.getType()).thenReturn(NotificationType.RULE_BASED_SEGMENT_UPDATE); + when(notification.getPreviousChangeNumber()).thenReturn(1000L); + return notification; + } } diff --git a/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java b/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java index f6bec9eda..74ea21706 100644 --- a/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java +++ b/src/test/java/io/split/android/client/service/sseclient/SseHandlerTest.java @@ -28,6 +28,7 @@ import io.split.android.client.service.sseclient.notifications.NotificationProcessor; import io.split.android.client.service.sseclient.notifications.NotificationType; import io.split.android.client.service.sseclient.notifications.OccupancyNotification; +import io.split.android.client.service.sseclient.notifications.RuleBasedSegmentChangeNotification; import io.split.android.client.service.sseclient.notifications.SplitKillNotification; import io.split.android.client.service.sseclient.notifications.SplitsChangeNotification; import io.split.android.client.service.sseclient.notifications.StreamingError; @@ -275,6 +276,21 @@ public void sseNonRecoverableConnectionErrorIsRecordedInTelemetry() { Assert.assertTrue(argumentCaptor.getValue().getTimestamp() > 0); } + @Test + public void incomingRuleBasedSegmentChange() { + IncomingNotification incomingNotification = + new IncomingNotification(NotificationType.RULE_BASED_SEGMENT_UPDATE, "", "", 100); + RuleBasedSegmentChangeNotification notification = new RuleBasedSegmentChangeNotification(-1); + + when(mNotificationParser.parseIncoming(anyString())).thenReturn(incomingNotification); + when(mNotificationParser.parseRuleBasedSegmentUpdate(anyString())).thenReturn(notification); + when(mManagerKeeper.isStreamingActive()).thenReturn(true); + + mSseHandler.handleIncomingMessage(buildMessage("{}")); + + verify(mNotificationProcessor).process(incomingNotification); + } + private void setupNotification() { when(mNotificationParser.isError(any())).thenReturn(true); int code = 40000; diff --git a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java b/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java index ae21ea58c..a996b9aba 100644 --- a/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java +++ b/src/test/java/io/split/android/client/service/synchronizer/FeatureFlagsSynchronizerImplTest.java @@ -33,6 +33,7 @@ import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskFactory; import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.service.rules.LoadRuleBasedSegmentsTask; import io.split.android.client.service.splits.FilterSplitsInCacheTask; import io.split.android.client.service.splits.LoadSplitsTask; import io.split.android.client.service.splits.SplitsSyncTask; @@ -77,20 +78,36 @@ public void setUp() { @Test public void synchronizeSplitsWithSince() { SplitsUpdateTask task = mock(SplitsUpdateTask.class); - when(mTaskFactory.createSplitsUpdateTask(1000)).thenReturn(task); + when(mTaskFactory.createSplitsUpdateTask(1000L, -1L)).thenReturn(task); - mFeatureFlagsSynchronizer.synchronize(1000); + mFeatureFlagsSynchronizer.synchronize(1000L, -1L); verify(mRetryTimerSplitsUpdate).setTask(eq(task), argThat(Objects::nonNull)); verify(mRetryTimerSplitsUpdate).start(); } + @Test + public void synchronizeRuleBasedSegmentsWithSince() { + SplitsUpdateTask task = mock(SplitsUpdateTask.class); + when(mTaskFactory.createSplitsUpdateTask((Long) null, 1000L)).thenReturn(task); + + mFeatureFlagsSynchronizer.synchronize(null, 1000L); + + verify(mRetryTimerSplitsUpdate).setTask(eq(task), argThat(Objects::nonNull)); + verify(mTaskFactory).createSplitsUpdateTask((Long) null, 1000L); + verify(mRetryTimerSplitsUpdate).start(); + } + @Test public void loadAndSynchronizeSplits() { LoadSplitsTask mockLoadTask = mock(LoadSplitsTask.class); when(mockLoadTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_SPLITS)); when(mTaskFactory.createLoadSplitsTask()).thenReturn(mockLoadTask); + LoadRuleBasedSegmentsTask mockLoadRuleBasedSegmentsTask = mock(LoadRuleBasedSegmentsTask.class); + when(mockLoadRuleBasedSegmentsTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.LOAD_LOCAL_RULE_BASED_SEGMENTS)); + when(mTaskFactory.createLoadRuleBasedSegmentsTask()).thenReturn(mockLoadRuleBasedSegmentsTask); + FilterSplitsInCacheTask mockFilterTask = mock(FilterSplitsInCacheTask.class); when(mockFilterTask.execute()).thenReturn(SplitTaskExecutionInfo.success(SplitTaskType.FILTER_SPLITS_CACHE)); when(mTaskFactory.createFilterSplitsInCacheTask()).thenReturn(mockFilterTask); @@ -110,7 +127,7 @@ public void loadAndSynchronizeSplits() { ArgumentCaptor> argument = ArgumentCaptor.forClass(List.class); verify(mTaskExecutor).executeSerially(argument.capture()); - assertEquals(3, argument.getValue().size()); + assertEquals(4, argument.getValue().size()); } @Test diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java b/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java index 1b950b365..b5f2ade6a 100644 --- a/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java +++ b/src/test/java/io/split/android/client/service/workmanager/splits/SplitsSyncWorkerTaskBuilderTest.java @@ -7,18 +7,23 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; + import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; import java.net.URISyntaxException; -import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.service.executor.SplitTask; import io.split.android.client.service.http.HttpFetcher; +import io.split.android.client.service.rules.RuleBasedSegmentChangeProcessor; import io.split.android.client.service.splits.SplitChangeProcessor; import io.split.android.client.service.splits.SplitsSyncHelper; import io.split.android.client.service.splits.SplitsSyncTask; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; @@ -27,10 +32,13 @@ public class SplitsSyncWorkerTaskBuilderTest { private StorageProvider mStorageProvider; private FetcherProvider mFetcherProvider; private SplitChangeProcessor mSplitChangeProcessor; + private RuleBasedSegmentChangeProcessor mRuleBasedSegmentChangeProcessor; private SyncHelperProvider mSplitsSyncHelperProvider; private SplitsStorage mSplitsStorage; - private HttpFetcher mSplitsFetcher; + private HttpFetcher mSplitsFetcher; private TelemetryStorage mTelemetryStorage; + private RuleBasedSegmentStorageProducer mRuleBasedSegmentStorageProducer; + private GeneralInfoStorage mGeneralinfoStorage; @Before public void setUp() throws URISyntaxException { @@ -41,22 +49,21 @@ public void setUp() throws URISyntaxException { mSplitsFetcher = mock(HttpFetcher.class); mTelemetryStorage = mock(TelemetryStorage.class); mSplitsSyncHelperProvider = mock(SyncHelperProvider.class); + mRuleBasedSegmentStorageProducer = mock(RuleBasedSegmentStorageProducer.class); + mRuleBasedSegmentChangeProcessor = mock(RuleBasedSegmentChangeProcessor.class); + mGeneralinfoStorage = mock(GeneralInfoStorage.class); when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn("filterQueryString"); when(mStorageProvider.provideSplitsStorage()).thenReturn(mSplitsStorage); + when(mStorageProvider.provideRuleBasedSegmentStorage()).thenReturn(mRuleBasedSegmentStorageProducer); when(mStorageProvider.provideTelemetryStorage()).thenReturn(mTelemetryStorage); when(mFetcherProvider.provideFetcher("filterQueryString")).thenReturn(mSplitsFetcher); - when(mSplitsSyncHelperProvider.provideSplitsSyncHelper(any(), any(), any(), any(), any())).thenReturn(mock(SplitsSyncHelper.class)); + when(mSplitsSyncHelperProvider.provideSplitsSyncHelper(any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(mock(SplitsSyncHelper.class)); } @Test public void getTaskUsesStorageProviderForSplitsStorage() { - SplitsSyncWorkerTaskBuilder builder = new SplitsSyncWorkerTaskBuilder( - mStorageProvider, - mFetcherProvider, - mSplitChangeProcessor, - mSplitsSyncHelperProvider, - null); + SplitsSyncWorkerTaskBuilder builder = getSplitsSyncWorkerTaskBuilder(null); builder.getTask(); @@ -65,12 +72,7 @@ public void getTaskUsesStorageProviderForSplitsStorage() { @Test public void getTaskUsesFetcherProviderForFetcher() throws URISyntaxException { - SplitsSyncWorkerTaskBuilder builder = new SplitsSyncWorkerTaskBuilder( - mStorageProvider, - mFetcherProvider, - mSplitChangeProcessor, - mSplitsSyncHelperProvider, - null); + SplitsSyncWorkerTaskBuilder builder = getSplitsSyncWorkerTaskBuilder(null); builder.getTask(); @@ -83,6 +85,7 @@ public void getTaskUsesStorageProviderForTelemetryStorage() { mStorageProvider, mFetcherProvider, mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, mSplitsSyncHelperProvider, null); @@ -94,12 +97,15 @@ public void getTaskUsesStorageProviderForTelemetryStorage() { @Test public void getTaskUsesSplitsSyncHelperProviderForSplitsSyncHelper() throws URISyntaxException { when(mSplitsStorage.getSplitsFilterQueryString()).thenReturn("string"); + when(mStorageProvider.provideRuleBasedSegmentStorage()).thenReturn(mRuleBasedSegmentStorageProducer); when(mFetcherProvider.provideFetcher("string")).thenReturn(mSplitsFetcher); + when(mStorageProvider.provideGeneralInfoStorage()).thenReturn(mGeneralinfoStorage); SplitsSyncWorkerTaskBuilder builder = new SplitsSyncWorkerTaskBuilder( mStorageProvider, mFetcherProvider, mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, mSplitsSyncHelperProvider, "1.5"); @@ -110,6 +116,9 @@ public void getTaskUsesSplitsSyncHelperProviderForSplitsSyncHelper() throws URIS verify(mSplitsSyncHelperProvider).provideSplitsSyncHelper(mSplitsFetcher, mSplitsStorage, mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, + mRuleBasedSegmentStorageProducer, + mGeneralinfoStorage, mTelemetryStorage, "1.5"); } @@ -118,12 +127,7 @@ public void getTaskUsesSplitsSyncHelperProviderForSplitsSyncHelper() throws URIS public void getTaskReturnsNullWhenURISyntaxExceptionIsThrown() throws URISyntaxException { when(mFetcherProvider.provideFetcher("filterQueryString")).thenThrow(new URISyntaxException("test", "test")); - SplitsSyncWorkerTaskBuilder builder = new SplitsSyncWorkerTaskBuilder( - mStorageProvider, - mFetcherProvider, - mSplitChangeProcessor, - mSplitsSyncHelperProvider, - null); + SplitsSyncWorkerTaskBuilder builder = getSplitsSyncWorkerTaskBuilder(null); SplitTask task = builder.getTask(); @@ -134,17 +138,25 @@ public void getTaskReturnsNullWhenURISyntaxExceptionIsThrown() throws URISyntaxE public void getTaskUsesSplitSyncTaskStaticMethod() { try (MockedStatic mockedStatic = mockStatic(SplitsSyncTask.class)) { SplitsSyncHelper splitsSyncHelper = mock(SplitsSyncHelper.class); - when(mSplitsSyncHelperProvider.provideSplitsSyncHelper(any(), any(), any(), any(), any())).thenReturn(splitsSyncHelper); - SplitsSyncWorkerTaskBuilder builder = new SplitsSyncWorkerTaskBuilder( - mStorageProvider, - mFetcherProvider, - mSplitChangeProcessor, - mSplitsSyncHelperProvider, - "2.5"); + when(mSplitsSyncHelperProvider.provideSplitsSyncHelper(any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(splitsSyncHelper); + when(mStorageProvider.provideRuleBasedSegmentStorage()).thenReturn(mRuleBasedSegmentStorageProducer); + + SplitsSyncWorkerTaskBuilder builder = getSplitsSyncWorkerTaskBuilder("2.5"); builder.getTask(); - mockedStatic.verify(() -> SplitsSyncTask.buildForBackground(splitsSyncHelper, mSplitsStorage, "filterQueryString", mTelemetryStorage)); + mockedStatic.verify(() -> SplitsSyncTask.buildForBackground(splitsSyncHelper, mSplitsStorage, mRuleBasedSegmentStorageProducer, "filterQueryString", mTelemetryStorage)); } } + + @NonNull + private SplitsSyncWorkerTaskBuilder getSplitsSyncWorkerTaskBuilder(String flagsSpec) { + return new SplitsSyncWorkerTaskBuilder( + mStorageProvider, + mFetcherProvider, + mSplitChangeProcessor, + mRuleBasedSegmentChangeProcessor, + mSplitsSyncHelperProvider, + flagsSpec); + } } diff --git a/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java b/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java index 29190bc7b..acf181ac9 100644 --- a/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java +++ b/src/test/java/io/split/android/client/service/workmanager/splits/StorageProviderTest.java @@ -5,12 +5,17 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; + import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.cipher.SplitCipherFactory; import io.split.android.client.storage.db.SplitRoomDatabase; import io.split.android.client.storage.db.StorageFactory; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorageProducer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; @@ -22,29 +27,47 @@ public class StorageProviderTest { @Before public void setUp() { mDatabase = mock(SplitRoomDatabase.class); - mStorageProvider = new StorageProvider(mDatabase, "sdk-key", true, true); + mStorageProvider = getStorageProvider("sdk-key", true, true); + } + + @NonNull + private StorageProvider getStorageProvider(String apiKey, boolean encryptionEnabled, boolean shouldRecordTelemetry) { + return new StorageProvider(mDatabase, apiKey, encryptionEnabled, shouldRecordTelemetry); } @Test public void provideSplitsStorageUsesStorageFactory() { try (MockedStatic mockedStatic = mockStatic(StorageFactory.class)) { - when(StorageFactory.getSplitsStorageForWorker(mDatabase, "sdk-key", true)).thenReturn(mock(SplitsStorage.class)); - - mStorageProvider.provideSplitsStorage(); - - mockedStatic.verify(() -> StorageFactory.getSplitsStorageForWorker(mDatabase, "sdk-key", true)); + try (MockedStatic mockedStaticCipher = mockStatic(SplitCipherFactory.class)) { + SplitCipher cipher = mock(SplitCipher.class); + SplitsStorage splitStorage = mock(SplitsStorage.class); + when(StorageFactory.getSplitsStorage(mDatabase, cipher)).thenReturn(splitStorage); + when(SplitCipherFactory.create("sdk-key", true)).thenReturn(cipher); + mStorageProvider = getStorageProvider("sdk-key", true, true); + + mStorageProvider.provideSplitsStorage(); + + mockedStaticCipher.verify(() -> SplitCipherFactory.create("sdk-key", true)); + mockedStatic.verify(() -> StorageFactory.getSplitsStorage(mDatabase, cipher)); + } } } @Test public void provideSplitsStorageWithDisabledEncryptionUsesStorageFactory() { - mStorageProvider = new StorageProvider(mDatabase, "sdk-key", false, true); try (MockedStatic mockedStatic = mockStatic(StorageFactory.class)) { - when(StorageFactory.getSplitsStorageForWorker(mDatabase, "sdk-key", false)).thenReturn(mock(SplitsStorage.class)); - - mStorageProvider.provideSplitsStorage(); - - mockedStatic.verify(() -> StorageFactory.getSplitsStorageForWorker(mDatabase, "sdk-key", false)); + try (MockedStatic mockedStaticCipher = mockStatic(SplitCipherFactory.class)) { + SplitCipher cipher = mock(SplitCipher.class); + SplitsStorage splitStorage = mock(SplitsStorage.class); + when(StorageFactory.getSplitsStorage(mDatabase, cipher)).thenReturn(splitStorage); + when(SplitCipherFactory.create("sdk-key", false)).thenReturn(cipher); + mStorageProvider = getStorageProvider("sdk-key", false, true); + + mStorageProvider.provideSplitsStorage(); + + mockedStaticCipher.verify(() -> SplitCipherFactory.create("sdk-key", false)); + mockedStatic.verify(() -> StorageFactory.getSplitsStorage(mDatabase, cipher)); + } } } @@ -52,11 +75,16 @@ public void provideSplitsStorageWithDisabledEncryptionUsesStorageFactory() { public void provideSplitsStorageCallsLoadLocalOnSplitsStorage() { SplitsStorage splitsStorage = mock(SplitsStorage.class); try (MockedStatic mockedStatic = mockStatic(StorageFactory.class)) { - when(StorageFactory.getSplitsStorageForWorker(mDatabase, "sdk-key", true)).thenReturn(splitsStorage); + try (MockedStatic mockedStaticCipher = mockStatic(SplitCipherFactory.class)) { + SplitCipher cipher = mock(SplitCipher.class); + when(SplitCipherFactory.create("sdk-key", true)).thenReturn(cipher); + when(StorageFactory.getSplitsStorage(mDatabase, cipher)).thenReturn(splitsStorage); + mStorageProvider = getStorageProvider("sdk-key", true, true); - mStorageProvider.provideSplitsStorage(); + mStorageProvider.provideSplitsStorage(); - verify(splitsStorage).loadLocal(); + verify(splitsStorage).loadLocal(); + } } } @@ -83,4 +111,55 @@ public void provideTelemetryStorageWithDisabledTelemetryUsesStorageFactory() { mockedStatic.verify(() -> StorageFactory.getTelemetryStorage(false)); } } + + @Test + public void provideRuleBasedSegmentStorageProducerUsesStorageFactory() { + try (MockedStatic mockedStatic = mockStatic(StorageFactory.class)) { + try (MockedStatic mockedStaticCipher = mockStatic(SplitCipherFactory.class)) { + SplitCipher cipher = mock(SplitCipher.class); + when(SplitCipherFactory.create("sdk-key", true)).thenReturn(cipher); + when(StorageFactory.getRuleBasedSegmentStorageForWorker(mDatabase, cipher)).thenReturn(mock(RuleBasedSegmentStorageProducer.class)); + mStorageProvider = getStorageProvider("sdk-key", true, true); + + mStorageProvider.provideRuleBasedSegmentStorage(); + + mockedStaticCipher.verify(() -> SplitCipherFactory.create("sdk-key", true)); + mockedStatic.verify(() -> StorageFactory.getRuleBasedSegmentStorageForWorker(mDatabase, cipher)); + } + } + } + + @Test + public void provideRuleBasedSegmentStoargeProducerCallsLoadLocalOnRuleBasedSegmentStorageProducer() { + RuleBasedSegmentStorageProducer ruleBasedSegmentStorageProducer = mock(RuleBasedSegmentStorageProducer.class); + try (MockedStatic mockedStatic = mockStatic(StorageFactory.class)) { + try (MockedStatic mockedStaticCipher = mockStatic(SplitCipherFactory.class)) { + SplitCipher cipher = mock(SplitCipher.class); + when(SplitCipherFactory.create("sdk-key", true)).thenReturn(cipher); + when(StorageFactory.getRuleBasedSegmentStorageForWorker(mDatabase, cipher)).thenReturn(ruleBasedSegmentStorageProducer); + mStorageProvider = getStorageProvider("sdk-key", true, true); + + mStorageProvider.provideRuleBasedSegmentStorage(); + + verify(ruleBasedSegmentStorageProducer).loadLocal(); + } + } + } + + @Test + public void provideRuleBasedSegmentStorageProducerWithDisabledEncryptionUsesStorageFactory() { + try (MockedStatic mockedStatic = mockStatic(StorageFactory.class)) { + try (MockedStatic mockedStaticCipher = mockStatic(SplitCipherFactory.class)) { + SplitCipher cipher = mock(SplitCipher.class); + when(SplitCipherFactory.create("sdk-key", false)).thenReturn(cipher); + when(StorageFactory.getRuleBasedSegmentStorageForWorker(mDatabase, cipher)).thenReturn(mock(RuleBasedSegmentStorageProducer.class)); + mStorageProvider = getStorageProvider("sdk-key", false, true); + + mStorageProvider.provideRuleBasedSegmentStorage(); + + mockedStaticCipher.verify(() -> SplitCipherFactory.create("sdk-key", false)); + mockedStatic.verify(() -> StorageFactory.getRuleBasedSegmentStorageForWorker(mDatabase, cipher)); + } + } + } } diff --git a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt index af0b3609a..0c5b4ae45 100644 --- a/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt +++ b/src/test/java/io/split/android/client/storage/cipher/ApplyCipherTaskTest.kt @@ -16,11 +16,14 @@ import io.split.android.client.storage.db.MySegmentDao import io.split.android.client.storage.db.MySegmentEntity import io.split.android.client.storage.db.SplitDao import io.split.android.client.storage.db.SplitEntity +import io.split.android.client.storage.db.SplitQueryDao import io.split.android.client.storage.db.SplitRoomDatabase import io.split.android.client.storage.db.attributes.AttributesDao import io.split.android.client.storage.db.attributes.AttributesEntity import io.split.android.client.storage.db.impressions.unique.UniqueKeyEntity import io.split.android.client.storage.db.impressions.unique.UniqueKeysDao +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -48,6 +51,9 @@ class ApplyCipherTaskTest { @Mock private lateinit var splitDao: SplitDao + @Mock + private lateinit var splitQueryDao: SplitQueryDao + @Mock private lateinit var mySegmentDao: MySegmentDao @@ -72,12 +78,16 @@ class ApplyCipherTaskTest { @Mock private lateinit var generalInfoDao: GeneralInfoDao + @Mock + private lateinit var ruleBasedSegmentDao: RuleBasedSegmentDao + private lateinit var applyCipherTask: ApplyCipherTask @Before fun setup() { MockitoAnnotations.openMocks(this) `when`(splitDatabase.splitDao()).thenReturn(splitDao) + `when`(splitDatabase.splitQueryDao).thenReturn(splitQueryDao) `when`(splitDatabase.mySegmentDao()).thenReturn(mySegmentDao) `when`(splitDatabase.myLargeSegmentDao()).thenReturn(myLargeSegmentDao) `when`(splitDatabase.impressionDao()).thenReturn(impressionDao) @@ -86,6 +96,7 @@ class ApplyCipherTaskTest { `when`(splitDatabase.uniqueKeysDao()).thenReturn(uniqueKeysDao) `when`(splitDatabase.attributesDao()).thenReturn(attributesDao) `when`(splitDatabase.generalInfoDao()).thenReturn(generalInfoDao) + `when`(splitDatabase.ruleBasedSegmentDao()).thenReturn(ruleBasedSegmentDao) `when`(fromCipher.decrypt(anyString())).thenAnswer { invocation -> "decrypted_${invocation.arguments[0]}" } `when`(toCipher.encrypt(anyString())).thenAnswer { invocation -> "encrypted_${invocation.arguments[0]}" } @@ -125,6 +136,8 @@ class ApplyCipherTaskTest { verify(toCipher).encrypt("decrypted_name2") verify(toCipher).encrypt("decrypted_body2") verify(splitDao).update("name2", "encrypted_decrypted_name2", "encrypted_decrypted_body2") + + verify(splitQueryDao).invalidate() } @Test @@ -366,4 +379,36 @@ class ApplyCipherTaskTest { verify(toCipher).encrypt("decrypted_{\"attr3\":\"val3\",\"attr4\":\"val4\"}") verify(attributesDao).update("key2", "encrypted_decrypted_key2", "encrypted_decrypted_{\"attr3\":\"val3\",\"attr4\":\"val4\"}") } + + @Test + fun `rule based segments are migrated`() { + `when`(ruleBasedSegmentDao.all).thenReturn( + listOf( + RuleBasedSegmentEntity().apply { name = "name1"; body = "body1"; updatedAt = 999991 }, + RuleBasedSegmentEntity().apply { name = "name2"; body = "body2"; updatedAt = 999992 }, + RuleBasedSegmentEntity().apply { name = "name3"; body = "body3"; updatedAt = 999993 }, + ) + ) + + applyCipherTask.execute() + + verify(ruleBasedSegmentDao).all + verify(fromCipher).decrypt("name1") + verify(fromCipher).decrypt("body1") + verify(toCipher).encrypt("decrypted_name1") + verify(toCipher).encrypt("decrypted_body1") + verify(ruleBasedSegmentDao).update("name1", "encrypted_decrypted_name1", "encrypted_decrypted_body1") + + verify(fromCipher).decrypt("name2") + verify(fromCipher).decrypt("body2") + verify(toCipher).encrypt("decrypted_name2") + verify(toCipher).encrypt("decrypted_body2") + verify(ruleBasedSegmentDao).update("name2", "encrypted_decrypted_name2", "encrypted_decrypted_body2") + + verify(fromCipher).decrypt("name3") + verify(fromCipher).decrypt("body3") + verify(toCipher).encrypt("decrypted_name3") + verify(toCipher).encrypt("decrypted_body3") + verify(ruleBasedSegmentDao).update("name3", "encrypted_decrypted_name3", "encrypted_decrypted_body3") + } } diff --git a/src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java b/src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java new file mode 100644 index 000000000..d066e8b2c --- /dev/null +++ b/src/test/java/io/split/android/client/storage/common/SplitStorageContainerTest.java @@ -0,0 +1,69 @@ +package io.split.android.client.storage.common; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +import org.junit.Test; +import org.mockito.MockedStatic; + +import io.split.android.client.service.impressions.observer.PersistentImpressionsObserverCacheStorage; +import io.split.android.client.storage.attributes.AttributesStorageContainer; +import io.split.android.client.storage.attributes.PersistentAttributesStorage; +import io.split.android.client.storage.events.EventsStorage; +import io.split.android.client.storage.events.PersistentEventsStorage; +import io.split.android.client.storage.general.GeneralInfoStorage; +import io.split.android.client.storage.impressions.ImpressionsStorage; +import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage; +import io.split.android.client.storage.impressions.PersistentImpressionsStorage; +import io.split.android.client.storage.impressions.PersistentImpressionsUniqueStorage; +import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.PersistentRuleBasedSegmentStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.client.storage.splits.PersistentSplitsStorage; +import io.split.android.client.storage.splits.SplitsStorage; +import io.split.android.client.telemetry.storage.TelemetryStorage; +import io.split.android.engine.experiments.ParserCommons; + +public class SplitStorageContainerTest { + + @Test + public void ruleBasedSegmentStorageIsSetInParserCommons() { + try (MockedStatic mockStatic = mockStatic(RuleBasedSegmentStorageInitializer.class)) { + RuleBasedSegmentStorageInitializer.Result result = mock(RuleBasedSegmentStorageInitializer.Result.class); + MySegmentsStorageContainer segmentStorage = mock(MySegmentsStorageContainer.class); + MySegmentsStorageContainer largeSegmentStorage = mock(MySegmentsStorageContainer.class); + PersistentRuleBasedSegmentStorage persistentRbsStorage = mock(PersistentRuleBasedSegmentStorage.class); + mockStatic.when(() -> RuleBasedSegmentStorageInitializer.initialize(segmentStorage, largeSegmentStorage, persistentRbsStorage)).thenReturn(result); + SplitStorageContainer container = new SplitStorageContainer( + mock(SplitsStorage.class), + segmentStorage, + largeSegmentStorage, + mock(PersistentSplitsStorage.class), + mock(EventsStorage.class), + mock(PersistentEventsStorage.class), + mock(ImpressionsStorage.class), + mock(PersistentImpressionsStorage.class), + mock(PersistentImpressionsCountStorage.class), + mock(PersistentImpressionsUniqueStorage.class), + mock(AttributesStorageContainer.class), + mock(PersistentAttributesStorage.class), + mock(TelemetryStorage.class), + mock(PersistentImpressionsObserverCacheStorage.class), + mock(GeneralInfoStorage.class), + persistentRbsStorage); + + + mockStatic.verify(() -> RuleBasedSegmentStorageInitializer.initialize(segmentStorage, largeSegmentStorage, persistentRbsStorage)); + } + } + + @Test + public void RuleBasedSegmentStorageInitializerSetsStorageInParserCommons() { + ParserCommons parserCommons = mock(ParserCommons.class); + RuleBasedSegmentStorage ruleBasedSegmentStorage = mock(RuleBasedSegmentStorage.class); + RuleBasedSegmentStorageInitializer.initialize(parserCommons, ruleBasedSegmentStorage); + + verify(parserCommons).setRuleBasedSegmentStorage(ruleBasedSegmentStorage); + } +} diff --git a/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java b/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java index aca7b1185..9a207f2b4 100644 --- a/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java +++ b/src/test/java/io/split/android/client/storage/general/GeneralInfoStorageImplTest.java @@ -40,16 +40,16 @@ public void getSplitsUpdateTimestampGetsValueFromDao() { } @Test - public void setChangeNumberSetsValueOnDao() { - mGeneralInfoStorage.setChangeNumber(123L); + public void setFlagsChangeNumberSetsValueOnDao() { + mGeneralInfoStorage.setFlagsChangeNumber(123L); verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("splitChangesChangeNumber") && entity.getLongValue() == 123L)); } @Test - public void getChangeNumberGetsValueFromDao() { + public void getFlagsChangeNumberGetsValueFromDao() { when(mGeneralInfoDao.getByName("splitChangesChangeNumber")).thenReturn(new GeneralInfoEntity("splitChangesChangeNumber", 123L)); - long changeNumber = mGeneralInfoStorage.getChangeNumber(); + long changeNumber = mGeneralInfoStorage.getFlagsChangeNumber(); assertEquals(123L, changeNumber); verify(mGeneralInfoDao).getByName("splitChangesChangeNumber"); @@ -120,9 +120,9 @@ public void setRolloutCacheLastClearTimestampSetsValueOnDao() { } @Test - public void getChangeNumberReturnsMinusOneIfEntityIsNull() { + public void getFlagsChangeNumberReturnsMinusOneIfEntityIsNull() { when(mGeneralInfoDao.getByName("splitChangesChangeNumber")).thenReturn(null); - long changeNumber = mGeneralInfoStorage.getChangeNumber(); + long changeNumber = mGeneralInfoStorage.getFlagsChangeNumber(); assertEquals(-1L, changeNumber); } @@ -166,4 +166,28 @@ public void getRolloutCacheLastClearTimestampReturnsZeroIfEntityIsNull() { assertEquals(0L, timestamp); } + + @Test + public void getRbsChangeNumberReturnsMinusOneIfEntityIsNull() { + when(mGeneralInfoDao.getByName("rbsChangeNumber")).thenReturn(null); + long changeNumber = mGeneralInfoStorage.getRbsChangeNumber(); + + assertEquals(-1L, changeNumber); + } + + @Test + public void getRbsChangeNumberReturnsValueFromDao() { + when(mGeneralInfoDao.getByName("rbsChangeNumber")).thenReturn(new GeneralInfoEntity("rbsChangeNumber", 123L)); + long changeNumber = mGeneralInfoStorage.getRbsChangeNumber(); + + assertEquals(123L, changeNumber); + verify(mGeneralInfoDao).getByName("rbsChangeNumber"); + } + + @Test + public void setRbsChangeNumberSetsValueOnDao() { + mGeneralInfoStorage.setRbsChangeNumber(123L); + + verify(mGeneralInfoDao).update(argThat(entity -> entity.getName().equals("rbsChangeNumber") && entity.getLongValue() == 123L)); + } } diff --git a/src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java b/src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java new file mode 100644 index 000000000..01b84b6a7 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/LazyRuleBasedSegmentStorageProviderTest.java @@ -0,0 +1,29 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +public class LazyRuleBasedSegmentStorageProviderTest { + + @Test + public void refCanOnlyBeSetOnce() { + LazyRuleBasedSegmentStorageProvider provider = new LazyRuleBasedSegmentStorageProvider(); + RuleBasedSegmentStorage firstInstance = mock(RuleBasedSegmentStorage.class); + RuleBasedSegmentStorage secondInstance = mock(RuleBasedSegmentStorage.class); + provider.set(firstInstance); + provider.set(secondInstance); + + assertSame(firstInstance, provider.get()); + assertNotSame(secondInstance, provider.get()); + } + + @Test + public void getReturnsNullWhenSetHasNotBeenCalled() { + LazyRuleBasedSegmentStorageProvider provider = new LazyRuleBasedSegmentStorageProvider(); + assertNull(provider.get()); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java b/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java new file mode 100644 index 000000000..6b8cdaa84 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageImplTest.java @@ -0,0 +1,245 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import io.split.android.client.dtos.Excluded; +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.dtos.Status; +import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.engine.experiments.ParsedRuleBasedSegment; +import io.split.android.engine.experiments.ParserCommons; +import io.split.android.engine.experiments.RuleBasedSegmentParser; + +public class RuleBasedSegmentStorageImplTest { + + private RuleBasedSegmentStorageImpl storage; + private PersistentRuleBasedSegmentStorage mPersistentStorage; + private RuleBasedSegmentParser mParser; + + @Before + public void setUp() { + mPersistentStorage = mock(PersistentRuleBasedSegmentStorage.class); + mParser = new RuleBasedSegmentParser(new ParserCommons(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class))); + storage = new RuleBasedSegmentStorageImpl(mPersistentStorage, mParser); + } + + @Test + public void get() { + RuleBasedSegment segment = createRuleBasedSegment("segment1"); + storage.update(Set.of(segment), null, 1); + assertNotNull(storage.get("segment1", "matchingKey")); + } + + @Test + public void sequentialUpdate() { + RuleBasedSegment segment1 = createRuleBasedSegment("segment1"); + RuleBasedSegment segment2 = createRuleBasedSegment("segment2"); + + Set toAdd = new HashSet<>(); + toAdd.add(segment1); + toAdd.add(segment2); + + storage.update(toAdd, null, 2); + assertNotNull(storage.get("segment1", "matchingKey")); + assertNotNull(storage.get("segment2", "matchingKey")); + assertEquals(2, storage.getChangeNumber()); + + Set toRemove = new HashSet<>(); + toRemove.add(segment1); + storage.update(null, toRemove, 3); + assertNull(storage.get("segment1", "matchingKey")); + assertNotNull(storage.get("segment2", "matchingKey")); + assertEquals(3, storage.getChangeNumber()); + } + + @Test + public void defaultChangeNumberIsMinusOne() { + assertEquals(-1, storage.getChangeNumber()); + } + + @Test + public void updateChangeNumber() { + storage.update(null, null, 5); + assertEquals(5, storage.getChangeNumber()); + } + + @Test + public void contains() { + RuleBasedSegment segment = createRuleBasedSegment("segment1"); + Set segmentNames = Set.of(segment); + storage.update(segmentNames, null, 1); + + Set segmentNames2 = new HashSet<>(); + segmentNames2.add("segment1"); + segmentNames2.add("segment2"); + + assertTrue(storage.contains(Set.of("segment1"))); + assertFalse(storage.contains(segmentNames2)); + } + + @Test + public void clearRemovesAllSegments() { + RuleBasedSegment segment = createRuleBasedSegment("segment1"); + storage.update(Set.of(segment), null, 1); + storage.clear(); + assertNull(storage.get("segment1", "matchingKey")); + } + + @Test + public void clearResetsChangeNumber() { + RuleBasedSegment segment = createRuleBasedSegment("segment1"); + storage.update(Set.of(segment), null, 10); + long preClearChangeNumber = storage.getChangeNumber(); + + storage.clear(); + + assertEquals(10, preClearChangeNumber); + assertEquals(-1, storage.getChangeNumber()); + } + + @Test + public void updateWithNullAddAndRemoveIsSafe() { + long initialChangeNumber = storage.getChangeNumber(); + storage.update(null, null, 10); + assertEquals(10, storage.getChangeNumber()); + assertNotEquals(initialChangeNumber, storage.getChangeNumber()); + } + + @Test + public void segmentRemoval() { + RuleBasedSegment segment1 = createRuleBasedSegment("segment1"); + RuleBasedSegment segment2 = createRuleBasedSegment("segment2"); + + Set toAdd = new HashSet<>(); + toAdd.add(segment1); + toAdd.add(segment2); + storage.update(toAdd, null, 1); + + assertNotNull(storage.get("segment1", "matchingKey")); + assertNotNull(storage.get("segment2", "matchingKey")); + + Set toRemove = new HashSet<>(); + toRemove.add(createRuleBasedSegment("segment1")); + storage.update(null, toRemove, 2); + + assertNull(storage.get("segment1", "matchingKey")); + assertNotNull(storage.get("segment2", "matchingKey")); + } + + @Test + public void segmentRemovalOfSameSegment() { + RuleBasedSegment segment1 = createRuleBasedSegment("segment1"); + Set segments = Collections.singleton(segment1); + + storage.update(segments, segments, 1); + assertNull(storage.get("segment1", "matchingKey")); + assertEquals(1, storage.getChangeNumber()); + } + + @Test + public void updateReturnsTrueWhenThereWereAddedSegments() { + RuleBasedSegment segment1 = createRuleBasedSegment("segment1"); + RuleBasedSegment segment2 = createRuleBasedSegment("segment2"); + Set toAdd = new HashSet<>(); + toAdd.add(segment1); + toAdd.add(segment2); + + assertTrue(storage.update(toAdd, null, 1)); + } + + @Test + public void loadLocalGetsSnapshotFromPersistentStorage() { + when(mPersistentStorage.getSnapshot()).thenReturn(new RuleBasedSegmentSnapshot(Map.of(), 1)); + storage.loadLocal(); + + verify(mPersistentStorage).getSnapshot(); + } + + @Test + public void loadLocalPopulatesValues() { + RuleBasedSegmentSnapshot snapshot = new RuleBasedSegmentSnapshot(Map.of("segment1", createRuleBasedSegment("segment1")), + 1); + when(mPersistentStorage.getSnapshot()).thenReturn(snapshot); + + long initialCn = storage.getChangeNumber(); + ParsedRuleBasedSegment initialSegment1 = storage.get("segment1", "matchingKey"); + + storage.loadLocal(); + + long finalCn = storage.getChangeNumber(); + ParsedRuleBasedSegment finalSegment1 = storage.get("segment1", "matchingKey"); + + assertEquals(-1, initialCn); + assertEquals(1, finalCn); + assertNull(initialSegment1); + assertNotNull(finalSegment1); + } + + @Test + public void clearCallsClearOnPersistentStorage() { + storage.clear(); + + verify(mPersistentStorage).clear(); + } + + @Test + public void callDelegatesToProducer() { + RuleBasedSegmentStorageProducer producer = mock(RuleBasedSegmentStorageProducer.class); + storage = new RuleBasedSegmentStorageImpl(producer, mParser, new ConcurrentHashMap<>()); + + storage.update(null, null, 1); + + verify(producer).update(null, null, 1); + } + + @Test + public void clearDelegatesToProducer() { + RuleBasedSegmentStorageProducer producer = mock(RuleBasedSegmentStorageProducer.class); + storage = new RuleBasedSegmentStorageImpl(producer, mParser, new ConcurrentHashMap<>()); + + storage.clear(); + + verify(producer).clear(); + } + + @Test + public void loadLocalDelegatesToProducer() { + RuleBasedSegmentStorageProducer producer = mock(RuleBasedSegmentStorageProducer.class); + storage = new RuleBasedSegmentStorageImpl(producer, mParser, new ConcurrentHashMap<>()); + + storage.loadLocal(); + + verify(producer).loadLocal(); + } + + public static RuleBasedSegment createRuleBasedSegment(String name, Status status) { + return new RuleBasedSegment(name, + "user", + 1, + status, + new ArrayList<>(), + new Excluded()); + } + + public static RuleBasedSegment createRuleBasedSegment(String name) { + return createRuleBasedSegment(name, Status.ACTIVE); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java b/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java new file mode 100644 index 000000000..dc8c27a95 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/RuleBasedSegmentStorageProducerImplTest.java @@ -0,0 +1,68 @@ +package io.split.android.client.storage.rbs; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static io.split.android.client.storage.rbs.RuleBasedSegmentStorageImplTest.createRuleBasedSegment; + +import org.junit.Before; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import io.split.android.client.dtos.RuleBasedSegment; + +public class RuleBasedSegmentStorageProducerImplTest { + + private PersistentRuleBasedSegmentStorage mPersistentStorage; + private RuleBasedSegmentStorageProducerImpl storage; + + @Before + public void setUp() { + mPersistentStorage = mock(PersistentRuleBasedSegmentStorage.class); + storage = new RuleBasedSegmentStorageProducerImpl(mPersistentStorage, new ConcurrentHashMap<>(), new AtomicLong(-1)); + } + + @Test + public void sequentialUpdate() { + RuleBasedSegment segment1 = createRuleBasedSegment("segment1"); + RuleBasedSegment segment2 = createRuleBasedSegment("segment2"); + + Set toAdd = new HashSet<>(); + toAdd.add(segment1); + toAdd.add(segment2); + + storage.update(toAdd, null, 2); + + verify(mPersistentStorage).update(argThat(argument -> argument.size() == 2), argThat(Set::isEmpty), eq(2L)); + } + + @Test + public void updateChangeNumber() { + storage.update(null, null, 5); + + verify(mPersistentStorage).update(argThat(Set::isEmpty), argThat(Set::isEmpty), eq(5L)); + } + + @Test + public void clear() { + storage.clear(); + + verify(mPersistentStorage).clear(); + } + + @Test + public void loadLocal() { + when(mPersistentStorage.getSnapshot()).thenReturn(new RuleBasedSegmentSnapshot(Map.of(), 1)); + + storage.loadLocal(); + + verify(mPersistentStorage).getSnapshot(); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java b/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java new file mode 100644 index 000000000..f59a15ebb --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/SnapshotLoaderTest.java @@ -0,0 +1,118 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SnapshotLoaderTest { + + private RuleBasedSegmentDao mDao; + private SplitCipher mCipher; + private GeneralInfoStorage mGeneralInfoStorage; + private SnapshotLoader mSnapshotLoader; + + @Before + public void setUp() { + mDao = mock(RuleBasedSegmentDao.class); + mCipher = mock(SplitCipher.class); + mGeneralInfoStorage = mock(GeneralInfoStorage.class); + mSnapshotLoader = new SnapshotLoader(mDao, mCipher, mGeneralInfoStorage); + } + + @Test + public void callReturnsCorrectSnapshotWithDecryptedSegments() throws Exception { + long expectedChangeNumber = 123L; + when(mGeneralInfoStorage.getRbsChangeNumber()).thenReturn(expectedChangeNumber); + + RuleBasedSegmentEntity entity1 = new RuleBasedSegmentEntity("segment1", "encryptedBody1", System.currentTimeMillis()); + RuleBasedSegmentEntity entity2 = new RuleBasedSegmentEntity("segment2", "encryptedBody2", System.currentTimeMillis()); + List entities = Arrays.asList(entity1, entity2); + when(mDao.getAll()).thenReturn(entities); + + when(mCipher.decrypt("segment1")).thenReturn("segment1"); + when(mCipher.decrypt("segment2")).thenReturn("segment2"); + when(mCipher.decrypt("encryptedBody1")).thenAnswer(invocation -> "{ \"name\": \"segment1\", \"trafficTypeName\": \"user\", \"changeNumber\": 1 }"); + when(mCipher.decrypt("encryptedBody2")).thenAnswer(invocation -> "{ \"name\": \"segment2\", \"trafficTypeName\": \"user\", \"changeNumber\": 2 }"); + + RuleBasedSegmentSnapshot result = mSnapshotLoader.call(); + + assertNotNull(result); + assertEquals(expectedChangeNumber, result.getChangeNumber()); + + Map segments = result.getSegments(); + assertEquals(2, segments.size()); + RuleBasedSegment rbs1 = segments.get("segment1"); + assertNotNull(rbs1); + assertEquals("segment1", rbs1.getName()); + assertEquals("user", rbs1.getTrafficTypeName()); + assertEquals(1, rbs1.getChangeNumber()); + + RuleBasedSegment rbs2 = segments.get("segment2"); + assertNotNull(rbs2); + assertEquals("segment2", rbs2.getName()); + assertEquals("user", rbs2.getTrafficTypeName()); + assertEquals(2, rbs2.getChangeNumber()); + } + + @Test + public void callGetsChangeNumberFromGeneralInfoStorage() { + mSnapshotLoader.call(); + + verify(mGeneralInfoStorage).getRbsChangeNumber(); + } + + @Test + public void callGetsAllSegmentsFromDao() { + mSnapshotLoader.call(); + + verify(mDao).getAll(); + } + + @Test + public void callDecryptsNameAndBodyFromEntity() { + when(mDao.getAll()).thenReturn(Arrays.asList( + new RuleBasedSegmentEntity("segment1", "encryptedBody1", System.currentTimeMillis()), + new RuleBasedSegmentEntity("segment2", "encryptedBody2", System.currentTimeMillis()))); + + mSnapshotLoader.call(); + + verify(mCipher).decrypt("segment1"); + verify(mCipher).decrypt("segment2"); + verify(mCipher).decrypt("encryptedBody1"); + verify(mCipher).decrypt("encryptedBody2"); + } + + @Test + public void constructorThrowsNullPointerExceptionWhenDaoIsNull() { + assertThrows(NullPointerException.class, + () -> new SnapshotLoader(null, mCipher, mGeneralInfoStorage)); + } + + @Test + public void constructorThrowsNullPointerExceptionWhenCipherIsNull() { + assertThrows(NullPointerException.class, + () -> new SnapshotLoader(mDao, null, mGeneralInfoStorage)); + } + + @Test + public void constructorThrowsNullPointerExceptionWhenGeneralInfoStorageIsNull() { + assertThrows(NullPointerException.class, + () -> new SnapshotLoader(mDao, mCipher, null)); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java b/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java new file mode 100644 index 000000000..13c646e0c --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/SqLitePersistentRuleBasedSegmentStorageTest.java @@ -0,0 +1,107 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static helper.TestingHelper.getFieldValue; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.HashSet; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SqLitePersistentRuleBasedSegmentStorageTest { + + private SplitCipher mCipher; + private SplitRoomDatabase mDatabase; + private RuleBasedSegmentDao mDao; + private GeneralInfoStorage mGeneralInfoStorage; + + private SqLitePersistentRuleBasedSegmentStorage storage; + + @Before + public void setUp() { + mCipher = mock(SplitCipher.class); + mDatabase = mock(SplitRoomDatabase.class); + mDao = mock(RuleBasedSegmentDao.class); + mGeneralInfoStorage = mock(GeneralInfoStorage.class); + when(mDatabase.ruleBasedSegmentDao()).thenReturn(mDao); + + storage = new SqLitePersistentRuleBasedSegmentStorage(mCipher, mDatabase, mGeneralInfoStorage); + } + + @Test + public void getSnapshotBuildsAndRunsSnapshotLoaderInstanceInTransaction() { + RuleBasedSegmentSnapshot expectedSnapshot = mock(RuleBasedSegmentSnapshot.class); + when(mDatabase.runInTransaction((SnapshotLoader) any())).thenReturn(expectedSnapshot); + + RuleBasedSegmentSnapshot result = storage.getSnapshot(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SnapshotLoader.class); + verify(mDatabase).runInTransaction(captor.capture()); + SnapshotLoader snapshotLoader = captor.getValue(); + assertSame(mDao, getFieldValue(snapshotLoader, "mDao")); + assertSame(mCipher, getFieldValue(snapshotLoader, "mCipher")); + assertSame(mGeneralInfoStorage, getFieldValue(snapshotLoader, "mGeneralInfoStorage")); + assertSame(expectedSnapshot, result); + } + + @Test + public void updateBuildsAndRunsUpdaterInstanceInTransaction() { + Set toAdd = new HashSet<>(); + Set toRemove = new HashSet<>(); + long changeNumber = 123L; + + storage.update(toAdd, toRemove, changeNumber); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Updater.class); + verify(mDatabase).runInTransaction(captor.capture()); + Updater updater = captor.getValue(); + assertSame(mCipher, getFieldValue(updater, "mCipher")); + assertSame(mDao, getFieldValue(updater, "mDao")); + assertSame(mGeneralInfoStorage, getFieldValue(updater, "mGeneralInfoStorage")); + assertSame(toAdd, getFieldValue(updater, "mToAdd")); + assertSame(toRemove, getFieldValue(updater, "mToRemove")); + assertSame(changeNumber, getFieldValue(updater, "mChangeNumber")); + } + + @Test + public void clearBuildsAndRunsClearerInstanceInTransaction() { + storage.clear(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Clearer.class); + verify(mDatabase).runInTransaction(captor.capture()); + Clearer clearer = captor.getValue(); + assertSame(mDao, getFieldValue(clearer, "mDao")); + assertSame(mGeneralInfoStorage, getFieldValue(clearer, "mGeneralInfoStorage")); + } + + @Test + public void cipherCannotBeNull() { + assertThrows(NullPointerException.class, + () -> new SqLitePersistentRuleBasedSegmentStorage(null, mDatabase, mGeneralInfoStorage)); + } + + @Test + public void databaseCannotBeNull() { + assertThrows(NullPointerException.class, + () -> new SqLitePersistentRuleBasedSegmentStorage(mCipher, null, mGeneralInfoStorage)); + } + + @Test + public void generalInfoStorageCannotBeNull() { + assertThrows(NullPointerException.class, + () -> new SqLitePersistentRuleBasedSegmentStorage(mCipher, mDatabase, null)); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java b/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java new file mode 100644 index 000000000..a6cd2dee8 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/SqLiteRuleBasedSegmentsPersistentStorageProviderTest.java @@ -0,0 +1,22 @@ +package io.split.android.client.storage.rbs; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +import org.junit.Test; + +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.SplitRoomDatabase; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class SqLiteRuleBasedSegmentsPersistentStorageProviderTest { + + @Test + public void providesSqLiteImplementation() { + PersistentRuleBasedSegmentStorage.Provider provider = + new SqLitePersistentRuleBasedSegmentStorageProvider(mock(SplitCipher.class), mock(SplitRoomDatabase.class), mock(GeneralInfoStorage.class)); + PersistentRuleBasedSegmentStorage persistentRuleBasedSegmentStorage = provider.get(); + + assertTrue(persistentRuleBasedSegmentStorage instanceof SqLitePersistentRuleBasedSegmentStorage); + } +} diff --git a/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java b/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java new file mode 100644 index 000000000..edb779f98 --- /dev/null +++ b/src/test/java/io/split/android/client/storage/rbs/UpdaterTest.java @@ -0,0 +1,125 @@ +package io.split.android.client.storage.rbs; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static io.split.android.client.storage.rbs.RuleBasedSegmentStorageImplTest.createRuleBasedSegment; + +import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.storage.cipher.SplitCipher; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentDao; +import io.split.android.client.storage.db.rbs.RuleBasedSegmentEntity; +import io.split.android.client.storage.general.GeneralInfoStorage; + +public class UpdaterTest { + + private SplitCipher mCipher; + private RuleBasedSegmentDao mDao; + private GeneralInfoStorage mGeneralInfoStorage; + private Updater mUpdater; + + @Before + public void setUp() { + mCipher = mock(SplitCipher.class); + mDao = mock(RuleBasedSegmentDao.class); + mGeneralInfoStorage = mock(GeneralInfoStorage.class); + } + + @Test + public void runEncryptsRemovedSegmentNamesBeforeSendingToDao() { + Set toRemove = Set.of( + createRuleBasedSegment("segment1"), createRuleBasedSegment("segment2")); + when(mCipher.encrypt(any())).thenAnswer(invocation -> "encrypted_" + invocation.getArgument(0)); + mUpdater = createUpdater(Collections.emptySet(), toRemove, 10); + + mUpdater.run(); + + verify(mCipher).encrypt("segment1"); + verify(mCipher).encrypt("segment2"); + verify(mDao).delete(argThat(new ArgumentMatcher>() { + @Override + public boolean matches(List argument) { + return argument.size() == 2 && + argument.contains("encrypted_segment1") && + argument.contains("encrypted_segment2"); + } + })); + } + + @Test + public void runEncryptsAddedSegmentNamesBeforeSendingToDao() { + Set toAdd = Set.of( + createRuleBasedSegment("segment1"), createRuleBasedSegment("segment2")); + when(mCipher.encrypt(any())).thenAnswer(invocation -> "encrypted_" + invocation.getArgument(0)); + mUpdater = createUpdater(toAdd, Collections.emptySet(), 10); + + mUpdater.run(); + + verify(mCipher).encrypt("segment1"); + verify(mCipher).encrypt("segment2"); + verify(mDao).insert(argThat(new ArgumentMatcher>() { + @Override + public boolean matches(List argument) { + argument.sort(Comparator.comparing(RuleBasedSegmentEntity::getName)); + RuleBasedSegmentEntity ruleBasedSegmentEntity = argument.get(0); + RuleBasedSegmentEntity ruleBasedSegmentEntity1 = argument.get(1); + return argument.size() == 2 && + ruleBasedSegmentEntity.getName().equals("encrypted_segment1") && + ruleBasedSegmentEntity1.getName().equals("encrypted_segment2") && + ruleBasedSegmentEntity.getBody().startsWith("encrypted_") && + ruleBasedSegmentEntity1.getBody().startsWith("encrypted_"); + } + })); + } + + @Test + public void runUpdatesChangeNumber() { + mUpdater = createUpdater(Collections.emptySet(), Collections.emptySet(), 10); + + mUpdater.run(); + + verify(mGeneralInfoStorage).setRbsChangeNumber(10); + } + + @Test + public void runDoesNotUpdateSegmentIfEncryptedNameIsNull() { + Set toAdd = Set.of( + createRuleBasedSegment("segment1"), createRuleBasedSegment("segment2")); + Set toRemove = Set.of( + createRuleBasedSegment("segment3"), createRuleBasedSegment("segment4")); + when(mCipher.encrypt(anyString())).thenReturn(null); + when(mCipher.encrypt(argThat(argument -> argument.contains("segment1")))).thenReturn("encrypted_segment1"); + when(mCipher.encrypt("segment3")).thenReturn("encrypted_segment3"); + mUpdater = createUpdater(toAdd, toRemove, 10); + + mUpdater.run(); + + verify(mCipher).encrypt("segment1"); + verify(mCipher).encrypt("segment2"); + verify(mCipher).encrypt("segment3"); + verify(mCipher).encrypt("segment4"); + verify(mDao).delete(argThat(argument -> argument.size() == 1 && + argument.get(0).equals("encrypted_segment3"))); + verify(mDao).insert(argThat((ArgumentMatcher>) argument -> argument.size() == 1 && + argument.get(0).getName().equals("encrypted_segment1"))); + } + + @NonNull + private Updater createUpdater(Set toAdd, Set toRemove, long changeNumber) { + return new Updater(mCipher, mDao, mGeneralInfoStorage, toAdd, toRemove, changeNumber); + } +} diff --git a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java index 38c07f3c0..7145fd775 100644 --- a/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java +++ b/src/test/java/io/split/android/client/storage/splits/SqLitePersistentSplitsStorageTest.java @@ -45,6 +45,8 @@ public class SqLitePersistentSplitsStorageTest { @Mock private SplitDao mSplitDao; @Mock + private SplitQueryDao mSplitQueryDao; + @Mock private SplitCipher mCipher; private SqLitePersistentSplitsStorage mStorage; private AutoCloseable mAutoCloseable; @@ -56,6 +58,7 @@ public void setUp() { mAutoCloseable = MockitoAnnotations.openMocks(this); when(mDatabase.generalInfoDao()).thenReturn(mock(GeneralInfoDao.class)); when(mDatabase.splitDao()).thenReturn(mSplitDao); + when(mDatabase.getSplitQueryDao()).thenReturn(mSplitQueryDao); doAnswer((Answer) invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -201,6 +204,7 @@ public boolean matches(GeneralInfoEntity argument) { } })); verify(mDatabase.splitDao()).deleteAll(); + verify(mDatabase.getSplitQueryDao()).invalidate(); } @Test diff --git a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java index 36b8728a5..1c80f33c5 100644 --- a/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java +++ b/src/test/java/io/split/android/client/utils/SplitClientImplFactory.java @@ -2,6 +2,8 @@ import static org.mockito.Mockito.mock; +import androidx.annotation.NonNull; + import java.util.Collections; import io.split.android.client.EventsTracker; @@ -17,7 +19,6 @@ import io.split.android.client.impressions.DecoratedImpressionListener; import io.split.android.client.impressions.ImpressionListener; import io.split.android.client.shared.SplitClientContainer; -import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.storage.TelemetryStorage; import io.split.android.client.validators.KeyValidatorImpl; @@ -26,6 +27,7 @@ import io.split.android.client.validators.TreatmentManager; import io.split.android.client.validators.TreatmentManagerFactory; import io.split.android.client.validators.TreatmentManagerFactoryImpl; +import io.split.android.engine.experiments.ParserCommons; import io.split.android.engine.experiments.SplitParser; import io.split.android.fake.SplitTaskExecutorStub; @@ -35,7 +37,7 @@ public class SplitClientImplFactory { public static SplitClientImpl get(Key key, SplitsStorage splitsStorage) { SplitClientConfig cfg = SplitClientConfig.builder().build(); SplitEventsManager eventsManager = new SplitEventsManager(cfg, new SplitTaskExecutorStub()); - SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); + SplitParser splitParser = getSplitParser(); TelemetryStorage telemetryStorage = mock(TelemetryStorage.class); TreatmentManagerFactory treatmentManagerFactory = new TreatmentManagerFactoryImpl( new KeyValidatorImpl(), new SplitValidatorImpl(), new ImpressionListener.FederatedImpressionListener(mock(DecoratedImpressionListener.class), Collections.emptyList()), @@ -62,7 +64,7 @@ false, new AttributesMergerImpl(), telemetryStorage, splitParser, } public static SplitClientImpl get(Key key, ImpressionListener impressionListener) { - SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); + SplitParser splitParser = getSplitParser(); SplitClientConfig cfg = SplitClientConfig.builder().build(); return new SplitClientImpl( mock(SplitFactory.class), @@ -80,7 +82,7 @@ public static SplitClientImpl get(Key key, ImpressionListener impressionListener } public static SplitClientImpl get(Key key, SplitEventsManager eventsManager) { - SplitParser splitParser = new SplitParser(mock(MySegmentsStorageContainer.class), mock(MySegmentsStorageContainer.class)); + SplitParser splitParser = getSplitParser(); return new SplitClientImpl( mock(SplitFactory.class), mock(SplitClientContainer.class), @@ -95,4 +97,9 @@ public static SplitClientImpl get(Key key, SplitEventsManager eventsManager) { mock(TreatmentManager.class) ); } + + @NonNull + private static SplitParser getSplitParser() { + return new SplitParser(mock(ParserCommons.class)); + } } diff --git a/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java b/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java index 77d185093..f75e1e8d6 100644 --- a/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java +++ b/src/test/java/io/split/android/engine/experiments/EvaluatorTest.java @@ -22,9 +22,9 @@ import io.split.android.client.EvaluatorImpl; import io.split.android.client.TreatmentLabels; import io.split.android.client.dtos.Split; -import io.split.android.client.exceptions.ChangeNumberExceptionWrapper; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.grammar.Treatments; import io.split.android.helpers.FileHelper; @@ -41,12 +41,13 @@ public void loadSplitsFromFile() { MySegmentsStorage myLargeSegmentsStorage = mock(MySegmentsStorage.class); MySegmentsStorageContainer mySegmentsStorageContainer = mock(MySegmentsStorageContainer.class); MySegmentsStorageContainer myLargeSegmentsStorageContainer = mock(MySegmentsStorageContainer.class); + RuleBasedSegmentStorage ruleBasedSegmentStorage = mock(RuleBasedSegmentStorage.class); SplitsStorage splitsStorage = mock(SplitsStorage.class); Set mySegments = new HashSet<>(Arrays.asList("s1", "s2", "test_copy")); Set myLargeSegments = new HashSet<>(Arrays.asList("segment1")); List splits = fileHelper.loadAndParseSplitChangeFile("split_changes_1.json"); - SplitParser splitParser = new SplitParser(mySegmentsStorageContainer, myLargeSegmentsStorageContainer); + SplitParser splitParser = new SplitParser(new ParserCommons(mySegmentsStorageContainer, myLargeSegmentsStorageContainer)); Map splitsMap = splitsMap(splits); when(splitsStorage.getAll()).thenReturn(splitsMap); diff --git a/src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java b/src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java new file mode 100644 index 000000000..cd0391292 --- /dev/null +++ b/src/test/java/io/split/android/engine/experiments/RuleBasedSegmentParserTest.java @@ -0,0 +1,200 @@ +package io.split.android.engine.experiments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.split.android.client.dtos.Condition; +import io.split.android.client.dtos.Excluded; +import io.split.android.client.dtos.ExcludedSegment; +import io.split.android.client.dtos.RuleBasedSegment; +import io.split.android.client.dtos.Status; + +public class RuleBasedSegmentParserTest { + + private static final String SEGMENT_NAME = "test-segment"; + private static final String TRAFFIC_TYPE = "user"; + private static final long CHANGE_NUMBER = 123456789L; + private static final String MATCHING_KEY = "test-key"; + + private ParserCommons mParserCommons; + private RuleBasedSegmentParser mParser; + + @Before + public void setUp() { + mParserCommons = mock(ParserCommons.class); + mParser = new RuleBasedSegmentParser(mParserCommons); + } + + @Test + public void validParsing() { + List conditions = new ArrayList<>(); + List parsedConditions = new ArrayList<>(); + parsedConditions.add(mock(ParsedCondition.class)); + + Set excludedKeys = new HashSet<>(Arrays.asList("excluded1", "excluded2")); + Set excludedSegments = new HashSet<>(Arrays.asList(ExcludedSegment.standard("segment1"), ExcludedSegment.standard("segment2"))); + + Excluded excluded = mock(Excluded.class); + when(excluded.getKeys()).thenReturn(excludedKeys); + when(excluded.getSegments()).thenReturn(excludedSegments); + + RuleBasedSegment segment = new RuleBasedSegment( + SEGMENT_NAME, + TRAFFIC_TYPE, + CHANGE_NUMBER, + Status.ACTIVE, + conditions, + excluded + ); + + when(mParserCommons.getParsedConditions( + eq(MATCHING_KEY), + eq(conditions), + anyString() + )).thenReturn(parsedConditions); + + ParsedRuleBasedSegment result = mParser.parse(segment, MATCHING_KEY); + + assertNotNull(result); + assertEquals(SEGMENT_NAME, result.getName()); + assertEquals(TRAFFIC_TYPE, result.getTrafficTypeName()); + assertEquals(CHANGE_NUMBER, result.getChangeNumber()); + assertEquals(excludedKeys, result.getExcludedKeys()); + assertEquals(excludedSegments, result.getExcludedSegments()); + assertEquals(parsedConditions, result.getParsedConditions()); + } + + @Test + public void parseWithNullConditionsCreatesEmptyConditionList() { + List conditions = new ArrayList<>(); + + Excluded excluded = mock(Excluded.class); + when(excluded.getKeys()).thenReturn(Collections.emptySet()); + when(excluded.getSegments()).thenReturn(Collections.emptySet()); + + RuleBasedSegment segment = new RuleBasedSegment( + SEGMENT_NAME, + TRAFFIC_TYPE, + CHANGE_NUMBER, + Status.ACTIVE, + conditions, + excluded + ); + + when(mParserCommons.getParsedConditions( + eq(MATCHING_KEY), + eq(conditions), + anyString() + )).thenReturn(null); + + ParsedRuleBasedSegment result = mParser.parse(segment, MATCHING_KEY); + + assertNotNull(result); + assertEquals(SEGMENT_NAME, result.getName()); + assertEquals(Collections.emptyList(), result.getParsedConditions()); + } + + @Test + public void parseWithNullMatchingKey() { + List conditions = new ArrayList<>(); + List parsedConditions = new ArrayList<>(); + parsedConditions.add(mock(ParsedCondition.class)); + + Excluded excluded = mock(Excluded.class); + when(excluded.getKeys()).thenReturn(Collections.emptySet()); + when(excluded.getSegments()).thenReturn(Collections.emptySet()); + + RuleBasedSegment segment = new RuleBasedSegment( + SEGMENT_NAME, + TRAFFIC_TYPE, + CHANGE_NUMBER, + Status.ACTIVE, + conditions, + excluded + ); + + when(mParserCommons.getParsedConditions( + eq(null), + eq(conditions), + anyString() + )).thenReturn(parsedConditions); + + ParsedRuleBasedSegment result = mParser.parse(segment, null); + + assertNotNull(result); + assertEquals(parsedConditions, result.getParsedConditions()); + } + + @Test + public void parseWithNullExcludedReturnsEmptyExcludedLists() { + List conditions = new ArrayList<>(); + List parsedConditions = new ArrayList<>(); + parsedConditions.add(mock(ParsedCondition.class)); + + RuleBasedSegment segment = new RuleBasedSegment( + SEGMENT_NAME, + TRAFFIC_TYPE, + CHANGE_NUMBER, + Status.ACTIVE, + conditions, + null + ); + + when(mParserCommons.getParsedConditions( + eq(MATCHING_KEY), + eq(conditions), + anyString() + )).thenReturn(parsedConditions); + + ParsedRuleBasedSegment result = mParser.parse(segment, MATCHING_KEY); + + assertNotNull(result); + assertTrue(result.getExcludedKeys().isEmpty()); + assertTrue(result.getExcludedSegments().isEmpty()); + } + + @Test + public void parseEmptyConditions() { + List conditions = Collections.emptyList(); + List parsedConditions = Collections.emptyList(); + + Excluded excluded = mock(Excluded.class); + when(excluded.getKeys()).thenReturn(Collections.emptySet()); + when(excluded.getSegments()).thenReturn(Collections.emptySet()); + + RuleBasedSegment segment = new RuleBasedSegment( + SEGMENT_NAME, + TRAFFIC_TYPE, + CHANGE_NUMBER, + Status.ACTIVE, + conditions, + excluded + ); + + when(mParserCommons.getParsedConditions( + eq(MATCHING_KEY), + eq(conditions), + anyString() + )).thenReturn(parsedConditions); + + ParsedRuleBasedSegment result = mParser.parse(segment, MATCHING_KEY); + + assertNotNull(result); + assertEquals(Collections.emptyList(), result.getParsedConditions()); + } +} diff --git a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java index 6bdd68705..9a48de3ad 100644 --- a/src/test/java/io/split/android/engine/experiments/SplitParserTest.java +++ b/src/test/java/io/split/android/engine/experiments/SplitParserTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.never; @@ -34,9 +35,11 @@ import io.split.android.client.dtos.Split; import io.split.android.client.dtos.Status; import io.split.android.client.dtos.UserDefinedLargeSegmentMatcherData; +import io.split.android.client.dtos.UserDefinedSegmentMatcherData; import io.split.android.client.dtos.WhitelistMatcherData; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.engine.ConditionsTestUtil; import io.split.android.engine.matchers.AttributeMatcher; import io.split.android.engine.matchers.BetweenMatcher; @@ -65,6 +68,8 @@ public class SplitParserTest { MySegmentsStorageContainer mMySegmentsStorageContainer; @Mock MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; + @Mock + RuleBasedSegmentStorage mRuleBasedSegmentStorage; @Before public void setup() { @@ -155,11 +160,6 @@ public void equal_to_negative_number() { assertThat(actual, is(equalTo(expected))); } - @NonNull - private SplitParser createParser() { - return new SplitParser(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer); - } - @Test public void between() { @@ -497,6 +497,33 @@ public void inLargeSegmentMatcherParsingTest() { verify(mMySegmentsStorageContainer, never()).getStorageForKey("matching_key"); } + @Test + public void inRuleBasedSegmentMatcherParsingTest() { + Condition condition = new Condition(); + condition.conditionType = ConditionType.ROLLOUT; + condition.label = null; + condition.partitions = null; + Matcher matcher = new Matcher(); + matcher.matcherType = MatcherType.IN_RULE_BASED_SEGMENT; + UserDefinedSegmentMatcherData userDefinedSegmentMatcherData = new UserDefinedSegmentMatcherData(); + userDefinedSegmentMatcherData.segmentName = "segment1"; + matcher.userDefinedSegmentMatcherData = userDefinedSegmentMatcherData; + condition.matcherGroup = new MatcherGroup(); + condition.matcherGroup.matchers = Collections.singletonList(matcher); + Split split = makeSplit("test1", Collections.singletonList(condition)); + + SplitParser parser = createParser(); + + ParsedSplit parsedSplit = parser.parse(split); + assertEquals("test1", parsedSplit.feature()); + assertEquals("off", parsedSplit.defaultTreatment()); + assertEquals(1, parsedSplit.parsedConditions().size()); + ParsedCondition parsedCondition = parsedSplit.parsedConditions().get(0); + assertNull(parsedCondition.label()); + assertEquals(ConditionType.ROLLOUT, parsedCondition.conditionType()); + assertNull(parsedCondition.partitions()); + } + @Test public void impressionsDisabledParsingTest(){ SplitParser parser = createParser(); @@ -513,6 +540,13 @@ public void impressionsDisabledParsingTest(){ assertTrue(actual2.impressionsDisabled()); } + @NonNull + private SplitParser createParser() { + ParserCommons parserCommons = new ParserCommons(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer); + parserCommons.setRuleBasedSegmentStorage(mRuleBasedSegmentStorage); + return new SplitParser(parserCommons); + } + private void set_matcher_test(Condition c, io.split.android.engine.matchers.Matcher m) { SplitParser parser = createParser(); diff --git a/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java b/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java index 4a972866d..2948b7978 100644 --- a/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java +++ b/src/test/java/io/split/android/engine/experiments/UnsupportedMatcherSplitParserTest.java @@ -15,6 +15,7 @@ import io.split.android.client.dtos.Split; import io.split.android.client.storage.mysegments.MySegmentsStorage; import io.split.android.client.storage.mysegments.MySegmentsStorageContainer; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; import io.split.android.client.utils.Json; public class UnsupportedMatcherSplitParserTest { @@ -27,6 +28,8 @@ public class UnsupportedMatcherSplitParserTest { private MySegmentsStorageContainer mMyLargeSegmentsStorageContainer; @Mock private DefaultConditionsProvider mDefaultConditionsProvider; + @Mock + private RuleBasedSegmentStorage mRuleBasedSegmentStorage; private final Split mTestFlag = Json.fromJson("{\"changeNumber\":1709843458770,\"trafficTypeName\":\"user\",\"name\":\"feature_flag_for_test\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-1364119282,\"seed\":-605938843,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"algo\":2,\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":null},\"matcherType\":\"WRONG_MATCHER_TYPE\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"dependencyMatcherData\":null,\"booleanMatcherData\":null,\"stringMatcherData\":\"123123\"}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100}],\"label\":\"wrong matcher type\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"sem\"},\"matcherType\":\"MATCHES_STRING\",\"negate\":false,\"userDefinedSegmentMatcherData\":null,\"whitelistMatcherData\":null,\"unaryNumericMatcherData\":null,\"betweenMatcherData\":null,\"dependencyMatcherData\":null,\"booleanMatcherData\":null,\"stringMatcherData\":\"1.2.3\"}]},\"partitions\":[{\"treatment\":\"on\",\"size\":100},{\"treatment\":\"off\",\"size\":0}],\"label\":\"sem matches 1.2.3\"}],\"configurations\":{},\"sets\":[]}", Split.class); private AutoCloseable mAutoCloseable; private SplitParser mSplitParser; @@ -37,7 +40,8 @@ public void setUp() { when(mMySegmentsStorageContainer.getStorageForKey("")).thenReturn(mMySegmentsStorage); when(mMySegmentsStorage.getAll()).thenReturn(Collections.emptySet()); when(mDefaultConditionsProvider.getDefaultConditions()).thenReturn(Collections.emptyList()); - mSplitParser = new SplitParser(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer, mDefaultConditionsProvider); + mSplitParser = new SplitParser( + new ParserCommons(mMySegmentsStorageContainer, mMyLargeSegmentsStorageContainer, mDefaultConditionsProvider)); } @After diff --git a/src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java b/src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java new file mode 100644 index 000000000..7c5883148 --- /dev/null +++ b/src/test/java/io/split/android/engine/matchers/InRuleBasedSegmentMatcherTest.java @@ -0,0 +1,243 @@ +package io.split.android.engine.matchers; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.split.android.client.Evaluator; +import io.split.android.client.dtos.ExcludedSegment; +import io.split.android.client.storage.mysegments.MySegmentsStorage; +import io.split.android.client.storage.rbs.RuleBasedSegmentStorage; +import io.split.android.engine.experiments.ParsedCondition; +import io.split.android.engine.experiments.ParsedRuleBasedSegment; + +public class InRuleBasedSegmentMatcherTest { + + private static final String SEGMENT_NAME = "test-segment"; + private static final String MATCHING_KEY = "test-key"; + private static final String BUCKETING_KEY = "test-bucketing-key"; + + private RuleBasedSegmentStorage mRuleBasedSegmentStorage; + private MySegmentsStorage mMySegmentsStorage; + private MySegmentsStorage mMyLargeSegmentsStorage; + private InRuleBasedSegmentMatcher mMatcher; + private Evaluator mEvaluator; + + @Before + public void setUp() { + mRuleBasedSegmentStorage = mock(RuleBasedSegmentStorage.class); + mMySegmentsStorage = mock(MySegmentsStorage.class); + mMyLargeSegmentsStorage = mock(MySegmentsStorage.class); + mEvaluator = mock(Evaluator.class); + mMatcher = new InRuleBasedSegmentMatcher(mRuleBasedSegmentStorage, mMySegmentsStorage, mMyLargeSegmentsStorage, SEGMENT_NAME); + } + + @Test + public void matchReturnsFalseWhenMatchValueIsNotString() { + assertFalse(mMatcher.match(123, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + assertFalse(mMatcher.match(true, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + assertFalse(mMatcher.match(null, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchReturnsFalseWhenKeyIsExcluded() { + ParsedRuleBasedSegment segment = createSegment( + new HashSet<>(Collections.singletonList(MATCHING_KEY)), + Collections.emptySet(), + Collections.emptyList() + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + + assertFalse(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchReturnsFalseWhenInExcludedSegment() { + ExcludedSegment excludedSegment = ExcludedSegment.standard("excluded-segment"); + Set mySegments = new HashSet<>(Collections.singletonList("excluded-segment")); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + new HashSet<>(Collections.singletonList(excludedSegment)), + Collections.emptyList() + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + when(mMySegmentsStorage.getAll()).thenReturn(mySegments); + + assertFalse(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchReturnsFalseWhenInExcludedLargeSegment() { + ExcludedSegment excludedSegment = ExcludedSegment.large("excluded-segment"); + Set mySegments = new HashSet<>(Collections.singletonList("excluded-segment")); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + new HashSet<>(Collections.singletonList(excludedSegment)), + Collections.emptyList() + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + when(mMyLargeSegmentsStorage.getAll()).thenReturn(mySegments); + + assertFalse(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchReturnsFalseWhenInExcludedRuleBasedSegment() { + ExcludedSegment excludedSegment = ExcludedSegment.ruleBased("excluded-segment"); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + new HashSet<>(Collections.singletonList(excludedSegment)), + Collections.emptyList() + ); + CombiningMatcher conditionMatcher = mock(CombiningMatcher.class); + Map attributes = new HashMap<>(); + attributes.put("age", 30); + attributes.put("country", "US"); + + when(conditionMatcher.match(eq(MATCHING_KEY), eq(BUCKETING_KEY), eq(attributes), eq(mEvaluator))).thenReturn(true); + + ParsedCondition condition = mock(ParsedCondition.class); + when(condition.matcher()).thenReturn(conditionMatcher); + + List conditions = Collections.singletonList(condition); + + ParsedRuleBasedSegment storedExcludedRbs + = createSegment(Collections.emptySet(), Collections.emptySet(), conditions); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + when(mRuleBasedSegmentStorage.get(eq("excluded-segment"), eq(MATCHING_KEY))).thenReturn(storedExcludedRbs); + + assertFalse(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, attributes, mEvaluator)); + } + + @Test + public void matchReturnsTrueWhenConditionMatches() { + CombiningMatcher conditionMatcher = mock(CombiningMatcher.class); + when(conditionMatcher.match(eq(MATCHING_KEY), eq(BUCKETING_KEY), anyMap(), eq(mEvaluator))).thenReturn(true); + + ParsedCondition condition = mock(ParsedCondition.class); + when(condition.matcher()).thenReturn(conditionMatcher); + + List conditions = Collections.singletonList(condition); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + Collections.emptySet(), + conditions + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + + assertTrue(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchReturnsFalseWhenNoConditionMatches() { + CombiningMatcher conditionMatcher = mock(CombiningMatcher.class); + when(conditionMatcher.match(eq(MATCHING_KEY), eq(BUCKETING_KEY), anyMap(), eq(mEvaluator))).thenReturn(false); + + ParsedCondition condition = mock(ParsedCondition.class); + when(condition.matcher()).thenReturn(conditionMatcher); + + List conditions = Collections.singletonList(condition); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + Collections.emptySet(), + conditions + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + + assertFalse(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchReturnsTrueWhenOneOfMultipleConditionsMatches() { + CombiningMatcher conditionMatcher1 = mock(CombiningMatcher.class); + when(conditionMatcher1.match(eq(MATCHING_KEY), eq(BUCKETING_KEY), anyMap(), eq(mEvaluator))).thenReturn(false); + + CombiningMatcher conditionMatcher2 = mock(CombiningMatcher.class); + when(conditionMatcher2.match(eq(MATCHING_KEY), eq(BUCKETING_KEY), anyMap(), eq(mEvaluator))).thenReturn(true); + + ParsedCondition condition1 = mock(ParsedCondition.class); + when(condition1.matcher()).thenReturn(conditionMatcher1); + + ParsedCondition condition2 = mock(ParsedCondition.class); + when(condition2.matcher()).thenReturn(conditionMatcher2); + + List conditions = Arrays.asList(condition1, condition2); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + Collections.emptySet(), + conditions + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + + assertTrue(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator)); + } + + @Test + public void matchWithAttributes() { + CombiningMatcher conditionMatcher = mock(CombiningMatcher.class); + Map attributes = new HashMap<>(); + attributes.put("age", 30); + attributes.put("country", "US"); + + when(conditionMatcher.match(eq(MATCHING_KEY), eq(BUCKETING_KEY), eq(attributes), eq(mEvaluator))).thenReturn(true); + + ParsedCondition condition = mock(ParsedCondition.class); + when(condition.matcher()).thenReturn(conditionMatcher); + + List conditions = Collections.singletonList(condition); + + ParsedRuleBasedSegment segment = createSegment( + Collections.emptySet(), + Collections.emptySet(), + conditions + ); + + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(segment); + + assertTrue(mMatcher.match(MATCHING_KEY, BUCKETING_KEY, attributes, mEvaluator)); + } + + @Test + public void matchWhenStorageReturnsNull() { + when(mRuleBasedSegmentStorage.get(eq(SEGMENT_NAME), eq(MATCHING_KEY))).thenReturn(null); + + boolean result = mMatcher.match(MATCHING_KEY, BUCKETING_KEY, Collections.emptyMap(), mEvaluator); + assertFalse(result); + } + + private ParsedRuleBasedSegment createSegment(Set excludedKeys, Set excludedSegments, List conditions) { + ParsedRuleBasedSegment segment = mock(ParsedRuleBasedSegment.class); + when(segment.getExcludedKeys()).thenReturn(excludedKeys); + when(segment.getExcludedSegments()).thenReturn(excludedSegments); + when(segment.getParsedConditions()).thenReturn(conditions != null ? conditions : new ArrayList<>()); + return segment; + } +} diff --git a/src/test/java/io/split/android/helpers/FileHelper.java b/src/test/java/io/split/android/helpers/FileHelper.java index 15adcd6fd..353e05e0a 100644 --- a/src/test/java/io/split/android/helpers/FileHelper.java +++ b/src/test/java/io/split/android/helpers/FileHelper.java @@ -11,26 +11,27 @@ import io.split.android.client.dtos.Split; import io.split.android.client.dtos.SplitChange; +import io.split.android.client.dtos.TargetingRulesChange; import io.split.android.client.utils.Json; public class FileHelper { - public List loadAndParseSplitChangeFile (String name) { + public List loadAndParseSplitChangeFile(String name) { try { String content = loadFileContent(name); - SplitChange change = Json.fromJson(content, SplitChange.class); - return change.splits; + TargetingRulesChange change = Json.fromJson(content, TargetingRulesChange.class); + return change.getFeatureFlagsChange().splits; } catch (Exception e) { System.out.println("loadAndParseSplitChangeFile: Failed load file content" + e.getLocalizedMessage()); } return null; } - public SplitChange loadSplitChangeFromFile (String name) { + public SplitChange loadSplitChangeFromFile(String name) { try { String content = loadFileContent(name); - SplitChange change = Json.fromJson(content, SplitChange.class); - return change; + TargetingRulesChange change = Json.fromJson(content, TargetingRulesChange.class); + return change.getFeatureFlagsChange(); } catch (Exception e) { } return null; diff --git a/src/test/resources/split_changes_1.json b/src/test/resources/split_changes_1.json index 7d72dbbcf..70de27a0f 100644 --- a/src/test/resources/split_changes_1.json +++ b/src/test/resources/split_changes_1.json @@ -1,2576 +1,2583 @@ { - "splits":[ - { - "trafficTypeName":"account", - "name":"FACUNDO_TEST", - "trafficAllocation":59, - "trafficAllocationSeed":-2108186082, - "seed":-1947050785, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1506703262916, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "nico_test", - "othertest" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "bla" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "ff": { + "splits": [ + { + "trafficTypeName": "account", + "name": "FACUNDO_TEST", + "trafficAllocation": 59, + "trafficAllocationSeed": -2108186082, + "seed": -1947050785, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "bla" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - }, - { - "treatment":"visa", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testing", - "trafficAllocation":100, - "trafficAllocationSeed":527505678, - "seed":-1716462249, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1506440189077, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"test_definition_as_of", - "treatments":[ - "on" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":true, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"Identify_UI", - "treatments":[ - "on" - ] - }, - "stringMatcherData":null + "treatment": "off", + "size": 100 + }, + { + "treatment": "visa", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testing", + "trafficAllocation": 100, + "trafficAllocationSeed": 527505678, + "seed": -1716462249, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506440189077, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "test_definition_as_of", + "treatments": [ + "on" + ] + }, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "Identify_UI", + "treatments": [ + "on" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in split test_definition_as_of treatment [on] and not in split Identify_UI treatment [on]" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"test_definition_as_of", - "treatments":[ - "off" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] + ], + "label": "in split test_definition_as_of treatment [on] and not in split Identify_UI treatment [on]" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "test_definition_as_of", + "treatments": [ + "off" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in split test_definition_as_of treatment [off]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testing222", - "trafficAllocation":100, - "trafficAllocationSeed":-397360967, - "seed":1058132210, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1505162627437, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 - }, - { - "treatment":"off", - "size":100 + ], + "label": "in split test_definition_as_of treatment [off]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testing222", + "trafficAllocation": 100, + "trafficAllocationSeed": -397360967, + "seed": 1058132210, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1505162627437, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"test222", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"a_new_split_2", - "trafficAllocation":99, - "trafficAllocationSeed":-1349440646, - "seed":-1536389703, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1505161671620, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "adil", - "bb", - "bbb", - "dd3c0800-30f1-11e7-ba78-12395d4a9634", - "pato", - "tito" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment2" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"test_copy" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "test222", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":52 - }, - { - "treatment":"off", - "size":48 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "a_new_split_2", + "trafficAllocation": 99, + "trafficAllocationSeed": -1349440646, + "seed": -1536389703, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1505161671620, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "adil", + "bb", + "bbb", + "dd3c0800-30f1-11e7-ba78-12395d4a9634", + "pato", + "tito" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"in segment all" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"asda" - }, - "matcherType":"STARTS_WITH", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "ee", - "aa" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - }, + "partitions": [ { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":true, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment2" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"asda starts with [ee, aa] and not in segment segment2" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"pp" - }, - "matcherType":"PART_OF_SET", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "pato", - "adil" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":100 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "test_copy" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"pp part of [pato, adil]" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"eee" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "1", - "2", - "trevorrr" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":20 - }, - { - "treatment":"off", - "size":80 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"testo", - "size":0 - } - ], - "label":"eee in list [1, 2, ...]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"test_string_without_attr", - "trafficAllocation":100, - "trafficAllocationSeed":-782597068, - "seed":-1682478887, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1504805281437, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"Segment3" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 52 }, { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "something" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 48 + }, + { + "treatment": "testo", + "size": 0 } - ] + ], + "label": "in segment all" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "asda" + }, + "matcherType": "STARTS_WITH", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "ee", + "aa" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": true, + "userDefinedSegmentMatcherData": { + "segmentName": "segment2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment Segment3 and in list [something]" - } - ] - }, - { - "trafficTypeName":"user", - "name":"Test", - "trafficAllocation":100, - "trafficAllocationSeed":217539832, - "seed":52164426, - "status":"ACTIVE", - "killed":true, - "defaultTreatment":"off", - "changeNumber":1504206031141, - "algo":2, - "configurations": { - "off": "{\"f1\":\"v1\"}" - }, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"sample-segment" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null - } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"demo" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "testo", + "size": 0 } - ] + ], + "label": "asda starts with [ee, aa] and not in segment segment2" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"employees" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "pp" + }, + "matcherType": "PART_OF_SET", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "pato", + "adil" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "testo", + "size": 0 } - ] + ], + "label": "pp part of [pato, adil]" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"sample-segment" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "eee" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "1", + "2", + "trevorrr" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 20 }, { - "keySelector":{ - "trafficType":"user", - "attribute":"fsdfsd" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a", - "b", - "c", - "d" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 80 }, { - "keySelector":{ - "trafficType":"user", - "attribute":"asdasdasd" - }, - "matcherType":"STARTS_WITH", - "negate":true, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "asdad", - "sa", - "das" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "testo", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":10 + ], + "label": "eee in list [1, 2, ...]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "test_string_without_attr", + "trafficAllocation": 100, + "trafficAllocationSeed": -782597068, + "seed": -1682478887, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1504805281437, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Segment3" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "something" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":90 - } - ], - "label":"in segment sample-segment and fsdfsd in list [a, b, ...] and asdasdasd does not start with [asdad, sa, ...]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Test_Save_1", - "trafficAllocation":100, - "trafficAllocationSeed":-257595325, - "seed":-665945237, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1503956389520, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "1", - "12", - "123", - "23" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" + ], + "label": "in segment Segment3 and in list [something]" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Test", + "trafficAllocation": 100, + "trafficAllocationSeed": 217539832, + "seed": 52164426, + "status": "ACTIVE", + "killed": true, + "defaultTreatment": "off", + "changeNumber": 1504206031141, + "algo": 2, + "configurations": { + "off": "{\"f1\":\"v1\"}" }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "asd" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample-segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"Segment3" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "demo" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "sample-segment" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "user", + "attribute": "fsdfsd" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a", + "b", + "c", + "d" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "user", + "attribute": "asdasdasd" + }, + "matcherType": "STARTS_WITH", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "asdad", + "sa", + "das" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 + "partitions": [ + { + "treatment": "on", + "size": 10 + }, + { + "treatment": "off", + "size": 90 + } + ], + "label": "in segment sample-segment and fsdfsd in list [a, b, ...] and asdasdasd does not start with [asdad, sa, ...]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Test_Save_1", + "trafficAllocation": 100, + "trafficAllocationSeed": -257595325, + "seed": -665945237, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1503956389520, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "1", + "12", + "123", + "23" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"v1", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"TEST", - "trafficAllocation":100, - "trafficAllocationSeed":-673356676, - "seed":-511119211, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1503942404754, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":100 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "asd" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_1", - "trafficAllocation":100, - "trafficAllocationSeed":987354894, - "seed":1292874260, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1503356075822, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":"atrib" - }, - "matcherType":"BETWEEN", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":{ - "dataType":"NUMBER", - "start":1474990940, - "end":1474990949 - }, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":95 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Segment3" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":5 - } - ], - "label":"atrib between 1474990940 and 1474990949" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":90 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":10 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"nico_tests", - "trafficAllocation":100, - "trafficAllocationSeed":1409699192, - "seed":-1997241870, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1501791316810, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "nico_test_browser__key" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "v1", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"employees" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "TEST", + "trafficAllocation": 100, + "trafficAllocationSeed": -673356676, + "seed": -511119211, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1503942404754, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_1", + "trafficAllocation": 100, + "trafficAllocationSeed": 987354894, + "seed": 1292874260, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1503356075822, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "atrib" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": "NUMBER", + "start": 1474990940, + "end": 1474990949 + }, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment employees" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo2222", - "trafficAllocation":100, - "trafficAllocationSeed":1164474086, - "seed":1270508512, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1501012403336, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "aasd" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 95 + }, + { + "treatment": "off", + "size": 5 } - ] + ], + "label": "atrib between 1474990940 and 1474990949" }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment2" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 90 + }, + { + "treatment": "off", + "size": 10 } - ] - }, - "partitions":[ - { - "treatment":"off", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "ddddd" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "nico_tests", + "trafficAllocation": 100, + "trafficAllocationSeed": 1409699192, + "seed": -1997241870, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1501791316810, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test_browser__key" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"Segment3" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "ppp", - "ppppp" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment employees" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo2222", + "trafficAllocation": 100, + "trafficAllocationSeed": 1164474086, + "seed": 1270508512, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1501012403336, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "aasd" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"pesto", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"test_copy" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment2" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"pesto", - "size":100 - } - ], - "label":"whitelisted segment" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "ddddd" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"off", - "size":0 - }, - { - "treatment":"on", - "size":100 - }, - { - "treatment":"pesto", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "Segment3" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"arse", - "size":0 - }, - { - "treatment":"zzzzick", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"Tagging", - "trafficAllocation":100, - "trafficAllocationSeed":1910132597, - "seed":-311493896, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500590774768, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":50 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "ppp", + "ppppp" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":50 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Welcome_Page_UI", - "trafficAllocation":100, - "trafficAllocationSeed":1848523960, - "seed":1608586361, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500577256901, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "pesto", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "test_copy" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"test", - "name":"pato_test_3", - "trafficAllocation":100, - "trafficAllocationSeed":458647735, - "seed":95677506, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500510847849, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"test", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "pesto", + "size": 100 } - ] + ], + "label": "whitelisted segment" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo23", - "trafficAllocation":100, - "trafficAllocationSeed":-689658216, - "seed":1711156051, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500064145947, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"sdsd" - }, - "matcherType":"EQUAL_TO_BOOLEAN", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":true, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "off", + "size": 0 + }, + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "pesto", + "size": 0 + }, + { + "treatment": "arse", + "size": 0 + }, + { + "treatment": "zzzzick", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Tagging", + "trafficAllocation": 100, + "trafficAllocationSeed": 1910132597, + "seed": -311493896, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500590774768, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"sdsd is true" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo909090", - "trafficAllocation":100, - "trafficAllocationSeed":-1196467266, - "seed":-1998101827, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1500039488369, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":"a" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 50 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"v" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 50 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Welcome_Page_UI", + "trafficAllocation": 100, + "trafficAllocationSeed": 1848523960, + "seed": 1608586361, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500577256901, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"asdadas" - }, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a", - "b", - "c", - "d" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 100 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "test", + "name": "pato_test_3", + "trafficAllocation": 100, + "trafficAllocationSeed": 458647735, + "seed": 95677506, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500510847849, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "test", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"sds" - }, - "matcherType":"CONTAINS_STRING", - "negate":true, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "a", - "c", - "d" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 100 + } + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo23", + "trafficAllocation": 100, + "trafficAllocationSeed": -689658216, + "seed": 1711156051, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500064145947, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "sdsd" + }, + "matcherType": "EQUAL_TO_BOOLEAN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": true, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 }, { - "keySelector":{ - "trafficType":"account", - "attribute":"xcvxv" - }, - "matcherType":"EQUAL_TO", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":{ - "dataType":"NUMBER", - "value":122 - }, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "sdsd is true" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo909090", + "trafficAllocation": 100, + "trafficAllocationSeed": -1196467266, + "seed": -1998101827, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1500039488369, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": "a" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "v" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "asdadas" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a", + "b", + "c", + "d" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "sds" + }, + "matcherType": "CONTAINS_STRING", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "a", + "c", + "d" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + }, + { + "keySelector": { + "trafficType": "account", + "attribute": "xcvxv" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 122 + }, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"a in list [a] and v in list [a] and asdadas in list [a, b, ...] and sds does not contain [a, c, ...] and xcvxv = 122" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "a in list [a] and v in list [a] and asdadas in list [a, b, ...] and sds does not contain [a, c, ...] and xcvxv = 122" }, - "partitions":[ - { - "treatment":"on", - "size":100 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"testo22", - "trafficAllocation":100, - "trafficAllocationSeed":1223277820, - "seed":-1152948537, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1499721434259, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "testo22", + "trafficAllocation": 100, + "trafficAllocationSeed": 1223277820, + "seed": -1152948537, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1499721434259, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"test-net", - "trafficAllocation":100, - "trafficAllocationSeed":-2038196969, - "seed":-862203077, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1499718635999, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "test-net", + "trafficAllocation": 100, + "trafficAllocationSeed": -2038196969, + "seed": -862203077, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1499718635999, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"test_dep_2", - "trafficAllocation":100, - "trafficAllocationSeed":-806171485, - "seed":922684950, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1499707910800, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"Identify_UI", - "treatments":[ - "on" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "test_dep_2", + "trafficAllocation": 100, + "trafficAllocationSeed": -806171485, + "seed": 922684950, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1499707910800, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "Identify_UI", + "treatments": [ + "on" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in split Identify_UI treatment [on]" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"IN_SPLIT_TREATMENT", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":{ - "split":"Definition_As_Of_Clickable_UI", - "treatments":[ - "off" - ] - }, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] + ], + "label": "in split Identify_UI treatment [on]" }, - "partitions":[ - { - "treatment":"on", - "size":50 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": { + "split": "Definition_As_Of_Clickable_UI", + "treatments": [ + "off" + ] + }, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":50 - } - ], - "label":"in split Definition_As_Of_Clickable_UI treatment [off]" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Definition_As_Of_Clickable_UI", - "trafficAllocation":100, - "trafficAllocationSeed":-198035199, - "seed":-151947071, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1498168847351, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "tito", - "trevor" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 50 + }, + { + "treatment": "off", + "size": 50 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in split Definition_As_Of_Clickable_UI treatment [off]" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Definition_As_Of_Clickable_UI", + "trafficAllocation": 100, + "trafficAllocationSeed": -198035199, + "seed": -151947071, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1498168847351, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "tito", + "trevor" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"Identify_UI", - "trafficAllocation":100, - "trafficAllocationSeed":-139516103, - "seed":1543172523, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1498078888450, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "Identify_UI", + "trafficAllocation": 100, + "trafficAllocationSeed": -139516103, + "seed": 1543172523, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1498078888450, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":0 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"test_definition_as_of", - "trafficAllocation":100, - "trafficAllocationSeed":1025823325, - "seed":-554248124, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1497289730024, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"account", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negatee":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "test_definition_as_of", + "trafficAllocation": 100, + "trafficAllocationSeed": 1025823325, + "seed": -554248124, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1497289730024, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negatee": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"Test-jw-go", - "trafficAllocation":100, - "trafficAllocationSeed":768122971, - "seed":1539205707, - "status":"ACTIVE", - "defaultTreatment":"off", - "changeNumber":1496339112852, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"WHITELIST", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":{ - "whitelist":[ - "test1" - ] - }, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted" - }, - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "Test-jw-go", + "trafficAllocation": 100, + "trafficAllocationSeed": 768122971, + "seed": 1539205707, + "status": "ACTIVE", + "defaultTreatment": "off", + "changeNumber": 1496339112852, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "test1" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } - ] + ], + "label": "whitelisted" }, - "partitions":[ - { - "treatment":"on", - "size":0 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_7", - "trafficAllocation":100, - "trafficAllocationSeed":-1340337178, - "seed":-1091938685, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593464885, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_7", + "trafficAllocation": 100, + "trafficAllocationSeed": -1340337178, + "seed": -1091938685, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593464885, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_6", - "trafficAllocation":100, - "trafficAllocationSeed":-1202331834, - "seed":-48445256, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593448028, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_6", + "trafficAllocation": 100, + "trafficAllocationSeed": -1202331834, + "seed": -48445256, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593448028, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_5", - "trafficAllocation":100, - "trafficAllocationSeed":2119994290, - "seed":-227092192, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593428034, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_5", + "trafficAllocation": 100, + "trafficAllocationSeed": 2119994290, + "seed": -227092192, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593428034, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_4", - "trafficAllocation":100, - "trafficAllocationSeed":1066635158, - "seed":-850704283, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593412226, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_4", + "trafficAllocation": 100, + "trafficAllocationSeed": 1066635158, + "seed": -850704283, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593412226, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_3", - "trafficAllocation":100, - "trafficAllocationSeed":1252392550, - "seed":971538037, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593352077, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_3", + "trafficAllocation": 100, + "trafficAllocationSeed": 1252392550, + "seed": 971538037, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593352077, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"user", - "name":"benchmark_jw_2", - "trafficAllocation":100, - "trafficAllocationSeed":-285565213, - "seed":-1992295819, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593336752, - "algo":2, - "conditions":[ - { - "conditionType":"ROLLOUT", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":{ - "trafficType":"user", - "attribute":null - }, - "matcherType":"ALL_KEYS", - "negate":false, - "userDefinedSegmentMatcherData":null, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":0 + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "user", + "name": "benchmark_jw_2", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593336752, + "algo": 2, + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] }, - { - "treatment":"off", - "size":100 - } - ], - "label":"in segment all" - } - ] - }, - { - "trafficTypeName":"account", - "name":"ls_split", - "trafficAllocation":100, - "trafficAllocationSeed":-285565213, - "seed":-1992295819, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1506703262916, - "algo":2, - "conditions":[ - { - "conditionType":"WHITELIST", - "matcherGroup":{ - "combiner":"AND", - "matchers":[ - { - "keySelector":null, - "matcherType":"IN_LARGE_SEGMENT", - "negate":false, - "userDefinedSegmentMatcherData":{ - "segmentName":"segment1" - }, - "userDefinedLargeSegmentMatcherData":{ - "largeSegmentName":"segment1" - }, - "whitelistMatcherData":null, - "unaryNumericMatcherData":null, - "betweenMatcherData":null, - "booleanMatcherData":null, - "dependencyMatcherData":null, - "stringMatcherData":null + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } - ] - }, - "partitions":[ - { - "treatment":"on", - "size":100 - } - ], - "label":"whitelisted segment" - } - ] - }, - { - "trafficTypeName":"user", - "name":"broken_split", - "trafficAllocation":100, - "trafficAllocationSeed":-285565213, - "status":"ACTIVE", - "killed":false, - "defaultTreatment":"off", - "changeNumber":1494593336752 - } - ], - "since":-1, - "till":1506703262916 -} + ], + "label": "in segment all" + } + ] + }, + { + "trafficTypeName": "account", + "name": "ls_split", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "seed": -1992295819, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "IN_LARGE_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "segment1" + }, + "userDefinedLargeSegmentMatcherData": { + "largeSegmentName": "segment1" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted segment" + } + ] + }, + { + "trafficTypeName": "user", + "name": "broken_split", + "trafficAllocation": 100, + "trafficAllocationSeed": -285565213, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1494593336752 + } + ], + "since": -1, + "till": 1506703262916 + }, + "rbs": { + "d": [], + "s": -1, + "t": 1506703261916 + } +} \ No newline at end of file diff --git a/src/test/resources/split_changes_1_updated.json b/src/test/resources/split_changes_1_updated.json new file mode 100644 index 000000000..e937cc953 --- /dev/null +++ b/src/test/resources/split_changes_1_updated.json @@ -0,0 +1,201 @@ +{ + "ff": { + "splits":[ + { + "trafficTypeName":"account", + "name":"FACUNDO_TEST", + "trafficAllocation":59, + "trafficAllocationSeed":-2108186082, + "seed":-1947050785, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1506703262916, + "algo":2, + "conditions":[ + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"WHITELIST", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":null, + "matcherType":"WHITELIST", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":{ + "whitelist":[ + "bla" + ] + }, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"off", + "size":100 + } + ], + "label":"whitelisted" + }, + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"ALL_KEYS", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":null, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":0 + }, + { + "treatment":"off", + "size":100 + }, + { + "treatment":"visa", + "size":0 + } + ], + "label":"in segment all" + } + ] + }, + { + "trafficTypeName":"account", + "name":"testing", + "trafficAllocation":100, + "trafficAllocationSeed":527505678, + "seed":-1716462249, + "status":"ACTIVE", + "killed":false, + "defaultTreatment":"off", + "changeNumber":1506440189077, + "algo":2, + "conditions":[ + { + "conditionType":"ROLLOUT", + "matcherGroup":{ + "combiner":"AND", + "matchers":[ + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":false, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"test_definition_as_of", + "treatments":[ + "on" + ] + }, + "stringMatcherData":null + }, + { + "keySelector":{ + "trafficType":"account", + "attribute":null + }, + "matcherType":"IN_SPLIT_TREATMENT", + "negate":true, + "userDefinedSegmentMatcherData":null, + "whitelistMatcherData":null, + "unaryNumericMatcherData":null, + "betweenMatcherData":null, + "booleanMatcherData":null, + "dependencyMatcherData":{ + "split":"Identify_UI", + "treatments":[ + "on" + ] + }, + "stringMatcherData":null + } + ] + }, + "partitions":[ + { + "treatment":"on", + "size":100 + }, + { + "treatment":"off", + "size":0 + } + ], + "label":"in split test_definition_as_of treatment [on] and not in split Identify_UI treatment [on]" + } + ] + } + ], + "since":-1, + "till":1506703262916 + }, + "rbs": { + "d": [], + "s": -1, + "t": 1506703262916 + } +} diff --git a/src/test/resources/split_changes_legacy.json b/src/test/resources/split_changes_legacy.json new file mode 100644 index 000000000..f52d7fa54 --- /dev/null +++ b/src/test/resources/split_changes_legacy.json @@ -0,0 +1,121 @@ +{ + "splits": [ + { + "trafficTypeName": "account", + "name": "FACUNDO_TEST", + "trafficAllocation": 59, + "trafficAllocationSeed": -2108186082, + "seed": -1947050785, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "bla" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "visa", + "size": 0 + } + ], + "label": "in segment all" + } + ] + } + ], + "since": -1, + "till": 1506703262916 +} \ No newline at end of file diff --git a/src/test/resources/split_changes_small.json b/src/test/resources/split_changes_small.json new file mode 100644 index 000000000..a5ed67f1d --- /dev/null +++ b/src/test/resources/split_changes_small.json @@ -0,0 +1,211 @@ +{ + "ff": { + "splits": [ + { + "trafficTypeName": "account", + "name": "FACUNDO_TEST", + "trafficAllocation": 59, + "trafficAllocationSeed": -2108186082, + "seed": -1947050785, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "changeNumber": 1506703262916, + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "nico_test", + "othertest" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "bla" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "off", + "size": 100 + } + ], + "label": "whitelisted" + }, + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "account", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "booleanMatcherData": null, + "dependencyMatcherData": null, + "stringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 + }, + { + "treatment": "visa", + "size": 0 + } + ], + "label": "in segment all" + } + ] + } + ], + "s": -1, + "t": 1506703262916 + }, + "rbs": { + "d": [ + { + "changeNumber": 5, + "name": "mauro_rule_based_segment", + "status": "ACTIVE", + "trafficTypeName": "user", + "excluded": { + "keys": [ + "mauro@split.io" + ], + "segments": [ + { + "name": "segment_test", + "type": "standard" + }, + { + "name": "segment_test2", + "type": "large" + }, + { + "name": "segment_test3", + "type": "rule-based" + }, + { + "name": "segment_test4", + "type": "unsupported" + } + ] + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "WHITELIST", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "mdp", + "tandil", + "bsas" + ] + } + }, + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@split.io" + ] + } + } + ] + } + }, + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user" + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "regular_segment" + } + } + ] + } + } + ] + } + ], + "s": 1506703262920, + "t": 1506703263000 + } +} \ No newline at end of file