diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 181fa4af..2b4cd7bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Set up nodejs - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 'lts/*' cache: 'npm' diff --git a/CHANGES.txt b/CHANGES.txt index 2bb7944c..499e296f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +2.4.0 (May 27, 2025) + - Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. + - Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. + 2.3.0 (May 16, 2025) - Updated the Redis storage to: - Avoid lazy require of the `ioredis` dependency when the SDK is initialized, and diff --git a/README.md b/README.md index 133917cb..85f791cd 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Split has built and maintains SDKs for: * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) * Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) +* Elixir thin-client [Github](https://github.com/splitio/elixir-thin-client) [Docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) * Flutter [Github](https://github.com/splitio/flutter-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) diff --git a/package-lock.json b/package-lock.json index f098b430..afd5917c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 3dc299c1..60d02afa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.4.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json b/src/__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json new file mode 100644 index 00000000..bd994511 --- /dev/null +++ b/src/__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"mc4i3NENoA:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:MTM2ODE2NDMxNA==\",\"timestamp\":1457552621899,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw==_splits\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":1457552620999}\"}" +} \ No newline at end of file diff --git a/src/__tests__/mocks/splitchanges.since.-1.json b/src/__tests__/mocks/splitchanges.since.-1.json index 8e24e581..18929ce7 100644 --- a/src/__tests__/mocks/splitchanges.since.-1.json +++ b/src/__tests__/mocks/splitchanges.since.-1.json @@ -1,1441 +1,1446 @@ { - "splits": [ - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "qc_team", - "seed": -1984784937, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "no", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "d": [ + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "qc_team", + "seed": -1984784937, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "no", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "tia@split.io", + "trevor@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": null, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "tia@split.io", - "trevor@split.io" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "yes", + "size": 100 } ] }, - "partitions": [ - { - "treatment": "yes", - "size": 100 - } - ] - }, - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "employees" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "unaryStringMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "yes", - "size": 0 + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "unaryStringMatcherData": null + } + ] }, - { - "treatment": "no", - "size": 100 - } - ] - } - ], - "configurations": {} - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "whitelist", - "seed": 104328192, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "not_allowed", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": null, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "facundo@split.io" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "allowed", - "size": 100 - } - ] - }, - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "treatment": "yes", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "no", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "allowed", - "size": 0 + } + ], + "configurations": {} + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "whitelist", + "seed": 104328192, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "not_allowed", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "facundo@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "not_allowed", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "blacklist", - "seed": -1840071133, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "allowed", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "splitters" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "allowed", + "size": 100 } ] }, - "partitions": [ - { - "treatment": "allowed", - "size": 0 + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "not_allowed", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "splitters", - "seed": 1061596048, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "splitters" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "allowed", + "size": 0 + }, + { + "treatment": "not_allowed", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "blacklist", + "seed": -1840071133, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "allowed", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "splitters" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "developers", - "seed": 1461592538, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "developers" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "allowed", + "size": 0 + }, + { + "treatment": "not_allowed", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "splitters", + "seed": 1061596048, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "splitters" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "employees_between_21_and_50_and_chrome", - "seed": -1073105888, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "splitters" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 }, { - "keySelector": { - "trafficType": "user", - "attribute": "age" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": null, - "start": 21, - "end": 50 + "treatment": "off", + "size": 0 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "developers", + "seed": 1461592538, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "developers" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 }, { - "keySelector": { - "trafficType": "user", - "attribute": "agent" - }, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "chrome" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "off", + "size": 0 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_10_and_user_attr2_is_not_foo", - "seed": 481329258, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "attr" + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "employees_between_21_and_50_and_chrome", + "seed": -1073105888, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "splitters" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 + { + "keySelector": { + "trafficType": "user", + "attribute": "age" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": null, + "start": 21, + "end": 50 + } }, - "betweenMatcherData": null - }, + { + "keySelector": { + "trafficType": "user", + "attribute": "agent" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "chrome" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr2" - }, - "matcherType": "WHITELIST", - "negate": true, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "foo" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_whitelist", - "seed": -2122983143, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "account" - }, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "key_1@split.io", - "key_2@split.io", - "key_3@split.io", - "key_4@split.io", - "key_5@split.io" - ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_10_and_user_attr2_is_not_foo", + "seed": 481329258, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + { + "keySelector": { + "trafficType": "user", + "attribute": "attr2" + }, + "matcherType": "WHITELIST", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "foo" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_employees", - "seed": 1107027749, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_whitelist", + "seed": -2122983143, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "account" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "key_1@split.io", + "key_2@split.io", + "key_3@split.io", + "key_4@split.io", + "key_5@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "account" - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "employees" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_all", - "seed": -790401804, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_employees", + "seed": 1107027749, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "account" + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "account" - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_all_50_50", - "seed": 968686, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_all", + "seed": -790401804, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "account" + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "lower", - "size": 50 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_all_50_50", + "seed": 968686, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "higher", - "size": 50 - } - ] - } - ] - },{ - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_all_50_50_2", - "seed": 96868, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "lower", + "size": 50 + }, + { + "treatment": "higher", + "size": 50 } ] - }, - "partitions": [ - { - "treatment": "lower", - "size": 50 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_all_50_50_2", + "seed": 96868, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "higher", - "size": 50 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_btw_datetime_1458240947021_and_1458246884077", - "seed": 622265394, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": "DATETIME", - "start": 1458240947021, - "end": 1458246884077 - } + "treatment": "lower", + "size": 50 + }, + { + "treatment": "higher", + "size": 50 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_btw_number_10_and_20", - "seed": 1870594950, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": "NUMBER", - "start": 10, - "end": 20 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_btw_datetime_1458240947021_and_1458246884077", + "seed": 622265394, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": "DATETIME", + "start": 1458240947021, + "end": 1458246884077 + } } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_btw_10_and_20", - "seed": -976719381, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": null, - "start": 10, - "end": 20 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_btw_number_10_and_20", + "seed": 1870594950, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": "NUMBER", + "start": 10, + "end": 20 + } } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_lte_datetime_1458240947021", - "seed": 455590578, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_btw_10_and_20", + "seed": -976719381, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": null, + "start": 10, + "end": 20 + } + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "DATETIME", - "value": 1458240947021 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_lte_number_10", - "seed": 1895728928, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_lte_datetime_1458240947021", + "seed": 455590578, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "DATETIME", + "value": 1458240947021 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "NUMBER", - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_lte_10", - "seed": 773481472, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_lte_number_10", + "seed": 1895728928, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_datetime_1458240947021", - "seed": 582849993, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_lte_10", + "seed": 773481472, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "DATETIME", - "value": 1458240947021 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_number_10", - "seed": -1710564342, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_datetime_1458240947021", + "seed": 582849993, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "DATETIME", + "value": 1458240947021 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "NUMBER", - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_10", - "seed": 2016359772, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_number_10", + "seed": -1710564342, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_eq_datetime_1458240947021", - "seed": -1927656676, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_10", + "seed": 2016359772, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "DATETIME", - "value": 1458240947021 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_eq_number_ten", - "seed": 643770303, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_eq_datetime_1458240947021", + "seed": -1927656676, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "DATETIME", + "value": 1458240947021 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "NUMBER", - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_eq_ten", - "seed": 1276593955, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_eq_number_ten", + "seed": 643770303, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "hierarchical_dep_always_on", - "seed": -790396804, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_eq_ten", + "seed": 1276593955, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "hierarchical dependency always on label" - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "hierarchical_dep_hierarchical", - "seed": 1276793945, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "hierarchical_dep_always_on", + "seed": -790396804, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SPLIT_TREATMENT", - "negate": false, - "dependencyMatcherData": { - "split": "hierarchical_dep_always_on", - "treatments": [ - "on", "partial" - ] - }, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "hierarchical dependency label" - } - ], - "configurations": {} - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "hierarchical_splits_test", - "seed": 1276793945, - "changeNumber": 2828282828, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + ], + "label": "hierarchical dependency always on label" + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "hierarchical_dep_hierarchical", + "seed": 1276793945, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "dependencyMatcherData": { + "split": "hierarchical_dep_always_on", + "treatments": [ + "on", + "partial" + ] + }, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SPLIT_TREATMENT", - "negate": false, - "dependencyMatcherData": { - "split": "hierarchical_dep_hierarchical", - "treatments": [ - "on", "partial" - ] - }, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "expected label" - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "always_on", - "seed": -790401604, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + ], + "label": "hierarchical dependency label" + } + ], + "configurations": {} + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "hierarchical_splits_test", + "seed": 1276793945, + "changeNumber": 2828282828, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "dependencyMatcherData": { + "split": "hierarchical_dep_hierarchical", + "treatments": [ + "on", + "partial" + ] + }, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "always_off", - "seed": -790401604, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + ], + "label": "expected label" + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "always_on", + "seed": -790401604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "off", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "ta_bucket1_test", - "algo": 2, - "seed": -1222652054, - "trafficAllocation": 1, - "trafficAllocationSeed": -1667452163, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "default_treatment", - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "always_off", + "seed": -790401604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "off", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "rollout_treatment", - "size": 100 - } - ] - } - ] - }, - { - "trafficTypeName": null, - "name": "split_with_config", - "algo": 2, - "seed": -1222652064, - "trafficAllocation": 100, - "changeNumber": 828282828282, - "trafficAllocationSeed": -1667492163, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "on", - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "ta_bucket1_test", + "algo": 2, + "seed": -1222652054, + "trafficAllocation": 1, + "trafficAllocationSeed": -1667452163, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "default_treatment", + "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 + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "group" - }, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "value_without_config" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "rollout_treatment", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 + } + ] + }, + { + "trafficTypeName": null, + "name": "split_with_config", + "algo": 2, + "seed": -1222652064, + "trafficAllocation": 100, + "changeNumber": 828282828282, + "trafficAllocationSeed": -1667492163, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "group" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "value_without_config" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ] - }, - { - "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 + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } ] }, - "partitions": [ - { - "treatment": "on", - "size": 100 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "another expected label" + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "another expected label" + } + ], + "configurations": { + "on": "{\"color\":\"brown\",\"dimensions\":{\"height\":12,\"width\":14},\"text\":{\"inner\":\"click me\"}}" } - ], - "configurations": { - "on": "{\"color\":\"brown\",\"dimensions\":{\"height\":12,\"width\":14},\"text\":{\"inner\":\"click me\"}}" } - } - ], - "since": -1, - "till": 1457552620999 -} + ], + "s": -1, + "t": 1457552620999 + } +} \ No newline at end of file diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 7573183c..78d62de4 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -66,6 +66,11 @@ interface IInSegmentMatcher extends ISplitMatcherBase { userDefinedSegmentMatcherData: IInSegmentMatcherData } +interface IInRBSegmentMatcher extends ISplitMatcherBase { + matcherType: 'IN_RULE_BASED_SEGMENT', + userDefinedSegmentMatcherData: IInSegmentMatcherData +} + interface IInLargeSegmentMatcher extends ISplitMatcherBase { matcherType: 'IN_LARGE_SEGMENT', userDefinedLargeSegmentMatcherData: IInLargeSegmentMatcherData @@ -176,7 +181,7 @@ export type ISplitMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatc ILessThanOrEqualToMatcher | IBetweenMatcher | IEqualToSetMatcher | IContainsAnyOfSetMatcher | IContainsAllOfSetMatcher | IPartOfSetMatcher | IStartsWithMatcher | IEndsWithMatcher | IContainsStringMatcher | IInSplitTreatmentMatcher | IEqualToBooleanMatcher | IMatchesStringMatcher | IEqualToSemverMatcher | IGreaterThanOrEqualToSemverMatcher | ILessThanOrEqualToSemverMatcher | IBetweenSemverMatcher | IInListSemverMatcher | - IInLargeSegmentMatcher + IInLargeSegmentMatcher | IInRBSegmentMatcher /** Split object */ export interface ISplitPartition { @@ -189,19 +194,39 @@ export interface ISplitCondition { combiner: 'AND', matchers: ISplitMatcher[] } - partitions: ISplitPartition[] - label: string - conditionType: 'ROLLOUT' | 'WHITELIST' + partitions?: ISplitPartition[] + label?: string + conditionType?: 'ROLLOUT' | 'WHITELIST' +} + +export interface IExcludedSegment { + type: 'standard' | 'large' | 'rule-based', + name: string, +} + +export interface IRBSegment { + name: string, + changeNumber: number, + status: 'ACTIVE' | 'ARCHIVED', + conditions?: ISplitCondition[], + excluded?: { + keys?: string[] | null, + segments?: IExcludedSegment[] | null + } } export interface ISplit { name: string, changeNumber: number, + status: 'ACTIVE' | 'ARCHIVED', + conditions: ISplitCondition[], + prerequisites?: { + n: string, + ts: string[] + }[] killed: boolean, defaultTreatment: string, trafficTypeName: string, - conditions: ISplitCondition[], - status: 'ACTIVE' | 'ARCHIVED', seed: number, trafficAllocation?: number, trafficAllocationSeed?: number @@ -217,8 +242,16 @@ export type ISplitPartial = Pick { - return new Engine(splitFlatStructure, evaluator); - } + const parsedKey = keyParser(key); - getKey() { - return this.baseInfo.name; - } + function evaluate(prerequisitesMet: boolean) { + if (!prerequisitesMet) { + log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); + return { + treatment: defaultTreatment, + label: PREREQUISITES_NOT_MET + }; + } - getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { - const { - killed, - seed, - defaultTreatment, - trafficAllocation, - trafficAllocationSeed - } = this.baseInfo; - let parsedKey; - let treatment; - let label; + const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; + + return thenable(evaluation) ? + evaluation.then(result => evaluationResult(result, defaultTreatment)) : + evaluationResult(evaluation, defaultTreatment); + } - try { - parsedKey = keyParser(key); - } catch (err) { - return { + if (status === 'ARCHIVED') return { treatment: CONTROL, - label: EXCEPTION + label: SPLIT_ARCHIVED }; - } - - if (this.isGarbage()) { - treatment = CONTROL; - label = SPLIT_ARCHIVED; - } else if (killed) { - treatment = defaultTreatment; - label = SPLIT_KILLED; - } else { - const evaluation = this.evaluator( - parsedKey, - seed, - trafficAllocation, - trafficAllocationSeed, - attributes, - splitEvaluator - ); - // Evaluation could be async, so we should handle that case checking for a - // thenable object - if (thenable(evaluation)) { - return evaluation.then(result => evaluationResult(result, defaultTreatment)); - } else { - return evaluationResult(evaluation, defaultTreatment); + if (killed) { + log.debug(ENGINE_DEFAULT, ['Flag is killed']); + return { + treatment: defaultTreatment, + label: SPLIT_KILLED + }; } - } - return { - treatment, - label - }; - } + const prerequisitesMet = prerequisiteMatcher({ key, attributes }, splitEvaluator); - isGarbage() { - return this.baseInfo.status === 'ARCHIVED'; - } + return thenable(prerequisitesMet) ? + prerequisitesMet.then(evaluate) : + evaluate(prerequisitesMet); + } + }; - getChangeNumber() { - return this.baseInfo.changeNumber; - } } diff --git a/src/evaluator/__tests__/evaluate-feature.spec.ts b/src/evaluator/__tests__/evaluate-feature.spec.ts index 711c701f..ffda4687 100644 --- a/src/evaluator/__tests__/evaluate-feature.spec.ts +++ b/src/evaluator/__tests__/evaluate-feature.spec.ts @@ -25,7 +25,7 @@ const mockStorage = { } }; -test('EVALUATOR / should return label exception, treatment control and config null on error', async function () { +test('EVALUATOR / should return label exception, treatment control and config null on error', async () => { const expectedOutput = { treatment: 'control', label: EXCEPTION, @@ -46,7 +46,7 @@ test('EVALUATOR / should return label exception, treatment control and config nu }); -test('EVALUATOR / should return right label, treatment and config if storage returns without errors.', async function () { +test('EVALUATOR / should return right label, treatment and config if storage returns without errors.', async () => { const expectedOutput = { treatment: 'on', label: 'in segment all', config: '{color:\'black\'}', changeNumber: 1487277320548 diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index 761f2804..e42fc6d3 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -42,7 +42,7 @@ const mockStorage = { } }; -test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async function () { +test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async () => { const expectedOutput = { throw_exception: { treatment: 'control', @@ -65,7 +65,7 @@ test('EVALUATOR - Multiple evaluations at once / should return label exception, }); -test('EVALUATOR - Multiple evaluations at once / should return right labels, treatments and configs if storage returns without errors.', async function () { +test('EVALUATOR - Multiple evaluations at once / should return right labels, treatments and configs if storage returns without errors.', async () => { const expectedOutput = { config: { treatment: 'on', label: 'in segment all', diff --git a/src/evaluator/combiners/__tests__/and.spec.ts b/src/evaluator/combiners/__tests__/and.spec.ts index 8d31a9d4..58e732d0 100644 --- a/src/evaluator/combiners/__tests__/and.spec.ts +++ b/src/evaluator/combiners/__tests__/and.spec.ts @@ -1,14 +1,14 @@ import { andCombinerContext } from '../and'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('COMBINER AND / should always return true', async function () { +test('COMBINER AND / should always return true', async () => { let AND = andCombinerContext(loggerMock, [() => true, () => true, () => true]); expect(await AND('always true')).toBe(true); // should always return true }); -test('COMBINER AND / should always return false', async function () { +test('COMBINER AND / should always return false', async () => { let AND = andCombinerContext(loggerMock, [() => true, () => true, () => false]); diff --git a/src/evaluator/combiners/__tests__/ifelseif.spec.ts b/src/evaluator/combiners/__tests__/ifelseif.spec.ts index 983b21a1..b890e410 100644 --- a/src/evaluator/combiners/__tests__/ifelseif.spec.ts +++ b/src/evaluator/combiners/__tests__/ifelseif.spec.ts @@ -2,7 +2,7 @@ import { ifElseIfCombinerContext } from '../ifelseif'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('IF ELSE IF COMBINER / should correctly propagate context parameters and predicates returns value', async function () { +test('IF ELSE IF COMBINER / should correctly propagate context parameters and predicates returns value', async () => { let inputKey = 'sample'; let inputSeed = 1234; let inputAttributes = {}; @@ -20,10 +20,9 @@ test('IF ELSE IF COMBINER / should correctly propagate context parameters and pr let ifElseIfEvaluator = ifElseIfCombinerContext(loggerMock, predicates); expect(await ifElseIfEvaluator(inputKey, inputSeed, inputAttributes) === evaluationResult).toBe(true); - console.log(`evaluator should return ${evaluationResult}`); }); -test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a treatment', async function () { +test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a treatment', async () => { let predicates = [ function undef() { return undefined; @@ -41,7 +40,7 @@ test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a tre expect(await ifElseIfEvaluator()).toBe('exclude'); // exclude treatment found }); -test('IF ELSE IF COMBINER / should return undefined if there is none matching rule', async function () { +test('IF ELSE IF COMBINER / should return undefined if there is none matching rule', async () => { const predicates = [ function undef() { return undefined; diff --git a/src/evaluator/combiners/and.ts b/src/evaluator/combiners/and.ts index b229a22b..fd239753 100644 --- a/src/evaluator/combiners/and.ts +++ b/src/evaluator/combiners/and.ts @@ -2,10 +2,11 @@ import { findIndex } from '../../utils/lang'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { MaybeThenable } from '../../dtos/types'; -import { IMatcher } from '../types'; +import { ISplitEvaluator } from '../types'; import { ENGINE_COMBINER_AND } from '../../logger/constants'; +import SplitIO from '../../../types/splitio'; -export function andCombinerContext(log: ILogger, matchers: IMatcher[]) { +export function andCombinerContext(log: ILogger, matchers: Array<(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable>) { function andResults(results: boolean[]): boolean { // Array.prototype.every is supported by target environments @@ -15,8 +16,8 @@ export function andCombinerContext(log: ILogger, matchers: IMatcher[]) { return hasMatchedAll; } - return function andCombiner(...params: any): MaybeThenable { - const matcherResults = matchers.map(matcher => matcher(...params)); + return function andCombiner(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator): MaybeThenable { + const matcherResults = matchers.map(matcher => matcher(key, attributes, splitEvaluator)); // If any matching result is a thenable we should use Promise.all if (findIndex(matcherResults, thenable) !== -1) { diff --git a/src/evaluator/combiners/ifelseif.ts b/src/evaluator/combiners/ifelseif.ts index 68fe5725..aaba4b27 100644 --- a/src/evaluator/combiners/ifelseif.ts +++ b/src/evaluator/combiners/ifelseif.ts @@ -1,4 +1,4 @@ -import { findIndex } from '../../utils/lang'; +import { findIndex, isBoolean } from '../../utils/lang'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { UNSUPPORTED_MATCHER_TYPE } from '../../utils/labels'; @@ -18,14 +18,12 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): }; } - function computeTreatment(predicateResults: Array) { - const len = predicateResults.length; - - for (let i = 0; i < len; i++) { + function computeEvaluation(predicateResults: Array): IEvaluation | boolean | undefined { + for (let i = 0, len = predicateResults.length; i < len; i++) { const evaluation = predicateResults[i]; if (evaluation !== undefined) { - log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); + if (!isBoolean(evaluation)) log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); return evaluation; } @@ -35,7 +33,7 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): return undefined; } - function ifElseIfCombiner(key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + function ifElseIfCombiner(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // In Async environments we are going to have async predicates. There is none way to know // before hand so we need to evaluate all the predicates, verify for thenables, and finally, // define how to return the treatment (wrap result into a Promise or not). @@ -43,10 +41,10 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): // if we find a thenable if (findIndex(predicateResults, thenable) !== -1) { - return Promise.all(predicateResults).then(results => computeTreatment(results)); + return Promise.all(predicateResults).then(results => computeEvaluation(results)); } - return computeTreatment(predicateResults as IEvaluation[]); + return computeEvaluation(predicateResults as IEvaluation[]); } // if there is none predicates, then there was an error in parsing phase diff --git a/src/evaluator/condition/engineUtils.ts b/src/evaluator/condition/engineUtils.ts index bacd3b10..398ea6cc 100644 --- a/src/evaluator/condition/engineUtils.ts +++ b/src/evaluator/condition/engineUtils.ts @@ -5,7 +5,7 @@ import { bucket } from '../../utils/murmur3/murmur3'; /** * Get the treatment name given a key, a seed, and the percentage of each treatment. */ -export function getTreatment(log: ILogger, key: string, seed: number, treatments: { getTreatmentFor: (x: number) => string }) { +export function getTreatment(log: ILogger, key: string, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }) { const _bucket = bucket(key, seed); const treatment = treatments.getTreatmentFor(_bucket); diff --git a/src/evaluator/condition/index.ts b/src/evaluator/condition/index.ts index 7ffaef79..5facaa5c 100644 --- a/src/evaluator/condition/index.ts +++ b/src/evaluator/condition/index.ts @@ -7,14 +7,14 @@ import SplitIO from '../../../types/splitio'; import { ILogger } from '../../logger/types'; // Build Evaluation object if and only if matchingResult is true -function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { +function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string): IEvaluation | boolean | undefined { if (matchingResult) { - const treatment = getTreatment(log, bucketingKey as string, seed, treatments); - - return { - treatment, - label - }; + return treatments ? // Feature flag + { + treatment: getTreatment(log, bucketingKey as string, seed, treatments), + label: label! + } : // Rule-based segment + true; } // else we should notify the engine to continue evaluating @@ -22,12 +22,12 @@ function match(log: ILogger, matchingResult: boolean, bucketingKey: string | und } // Condition factory -export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator { +export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable, treatments?: { getTreatmentFor: (x: number) => string }, label?: string, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator { - return function conditionEvaluator(key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + return function conditionEvaluator(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions. - if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation as number, (key as SplitIO.SplitKeyObject).bucketingKey as string, trafficAllocationSeed as number)) { + if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation!, key.bucketingKey, trafficAllocationSeed!)) { return { treatment: undefined, // treatment value is assigned later label: NOT_IN_SPLIT @@ -41,10 +41,10 @@ export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) const matches = matcherEvaluator(key, attributes, splitEvaluator); if (thenable(matches)) { - return matches.then(result => match(log, result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label)); + return matches.then(result => match(log, result, key.bucketingKey, seed, treatments, label)); } - return match(log, matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label); + return match(log, matches, key.bucketingKey, seed, treatments, label); }; } diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 2d06ad10..e465b9bf 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,4 +1,4 @@ -import { Engine } from './Engine'; +import { engineParser } from './Engine'; import { thenable } from '../utils/promise/thenable'; import { EXCEPTION, SPLIT_NOT_FOUND } from '../utils/labels'; import { CONTROL } from '../utils/constants'; @@ -43,8 +43,8 @@ export function evaluateFeature( if (thenable(parsedSplit)) { return parsedSplit.then((split) => getEvaluation( log, - split, key, + split, attributes, storage, )).catch( @@ -56,8 +56,8 @@ export function evaluateFeature( return getEvaluation( log, - parsedSplit, key, + parsedSplit, attributes, storage, ); @@ -80,13 +80,13 @@ export function evaluateFeatures( } return thenable(parsedSplits) ? - parsedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) + parsedSplits.then(splits => getEvaluations(log, key, splitNames, splits, attributes, storage)) .catch(() => { // Exception on async `getSplits` storage. For example, when the storage is redis or // pluggable and there is a connection issue and we can't retrieve the split to be evaluated return treatmentsException(splitNames); }) : - getEvaluations(log, splitNames, parsedSplits, key, attributes, storage); + getEvaluations(log, key, splitNames, parsedSplits, attributes, storage); } export function evaluateFeaturesByFlagSets( @@ -99,9 +99,7 @@ export function evaluateFeaturesByFlagSets( ): MaybeThenable> { let storedFlagNames: MaybeThenable[]>; - function evaluate( - featureFlagsByFlagSets: Set[], - ) { + function evaluate(featureFlagsByFlagSets: Set[]) { let featureFlags = new Set(); for (let i = 0; i < flagSets.length; i++) { const featureFlagByFlagSet = featureFlagsByFlagSets[i]; @@ -136,8 +134,8 @@ export function evaluateFeaturesByFlagSets( function getEvaluation( log: ILogger, - splitJSON: ISplit | null, key: SplitIO.SplitKey, + splitJSON: ISplit | null, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, ): MaybeThenable { @@ -148,20 +146,20 @@ function getEvaluation( }; if (splitJSON) { - const split = Engine.parse(log, splitJSON, storage); + const split = engineParser(log, splitJSON, storage); evaluation = split.getTreatment(key, attributes, evaluateFeature); // If the storage is async and the evaluated flag uses segments or dependencies, evaluation is thenable if (thenable(evaluation)) { return evaluation.then(result => { - result.changeNumber = split.getChangeNumber(); + result.changeNumber = splitJSON.changeNumber; result.config = splitJSON.configurations && splitJSON.configurations[result.treatment] || null; result.impressionsDisabled = splitJSON.impressionsDisabled; return result; }); } else { - evaluation.changeNumber = split.getChangeNumber(); // Always sync and optional + evaluation.changeNumber = splitJSON.changeNumber; evaluation.config = splitJSON.configurations && splitJSON.configurations[evaluation.treatment] || null; evaluation.impressionsDisabled = splitJSON.impressionsDisabled; } @@ -172,9 +170,9 @@ function getEvaluation( function getEvaluations( log: ILogger, + key: SplitIO.SplitKey, splitNames: string[], splits: Record, - key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, ): MaybeThenable> { @@ -183,8 +181,8 @@ function getEvaluations( splitNames.forEach(splitName => { const evaluation = getEvaluation( log, - splits[splitName], key, + splits[splitName], attributes, storage ); diff --git a/src/evaluator/matchers/__tests__/all.spec.ts b/src/evaluator/matchers/__tests__/all.spec.ts index 106dea8b..2c6a5f72 100644 --- a/src/evaluator/matchers/__tests__/all.spec.ts +++ b/src/evaluator/matchers/__tests__/all.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER ALL_KEYS / should always return true', function () { +test('MATCHER ALL_KEYS / should always return true', () => { const matcher = matcherFactory(loggerMock, { type: matcherTypes.ALL_KEYS, value: undefined diff --git a/src/evaluator/matchers/__tests__/between.spec.ts b/src/evaluator/matchers/__tests__/between.spec.ts index 5b76186b..34d44eeb 100644 --- a/src/evaluator/matchers/__tests__/between.spec.ts +++ b/src/evaluator/matchers/__tests__/between.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and 20', function () { +test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and 20', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.BETWEEN, @@ -19,6 +19,6 @@ test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and expect(matcher(15)).toBe(true); // 15 is between 10 and 20 expect(matcher(20)).toBe(true); // 20 is between 10 and 20 expect(matcher(21)).toBe(false); // 21 is not between 10 and 20 - expect(matcher(undefined)).toBe(false); // undefined is not between 10 and 20 - expect(matcher(null)).toBe(false); // null is not between 10 and 20 + expect(matcher(undefined as any)).toBe(false); // undefined is not between 10 and 20 + expect(matcher(null as any)).toBe(false); // null is not between 10 and 20 }); diff --git a/src/evaluator/matchers/__tests__/boolean.spec.ts b/src/evaluator/matchers/__tests__/boolean.spec.ts index 8a166e9c..a5f9d5bc 100644 --- a/src/evaluator/matchers/__tests__/boolean.spec.ts +++ b/src/evaluator/matchers/__tests__/boolean.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER BOOLEAN / should return true ONLY when the value is true', function () { +test('MATCHER BOOLEAN / should return true ONLY when the value is true', () => { const matcher = matcherFactory(loggerMock, { type: matcherTypes.EQUAL_TO_BOOLEAN, value: true diff --git a/src/evaluator/matchers/__tests__/cont_all.spec.ts b/src/evaluator/matchers/__tests__/cont_all.spec.ts index 353877db..3b99cff1 100644 --- a/src/evaluator/matchers/__tests__/cont_all.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_all.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER CONTAINS_ALL_OF_SET / should return true ONLY when value contains all of set ["update", "add"]', function () { +test('MATCHER CONTAINS_ALL_OF_SET / should return true ONLY when value contains all of set ["update", "add"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_ALL_OF_SET, diff --git a/src/evaluator/matchers/__tests__/cont_any.spec.ts b/src/evaluator/matchers/__tests__/cont_any.spec.ts index 478d6e1c..f8ff75fa 100644 --- a/src/evaluator/matchers/__tests__/cont_any.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_any.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER CONTAINS_ANY_OF_SET / should return true ONLY when value contains any of set ["update", "add"]', function () { +test('MATCHER CONTAINS_ANY_OF_SET / should return true ONLY when value contains any of set ["update", "add"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_ANY_OF_SET, diff --git a/src/evaluator/matchers/__tests__/cont_str.spec.ts b/src/evaluator/matchers/__tests__/cont_str.spec.ts index be7c2870..a24fe2bc 100644 --- a/src/evaluator/matchers/__tests__/cont_str.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_str.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER CONTAINS_STRING / should return true ONLY when the value is contained in ["roni", "bad", "ar"]', function () { +test('MATCHER CONTAINS_STRING / should return true ONLY when the value is contained in ["roni", "bad", "ar"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_STRING, diff --git a/src/evaluator/matchers/__tests__/dependency.spec.ts b/src/evaluator/matchers/__tests__/dependency.spec.ts index 74d833d6..7cb184d6 100644 --- a/src/evaluator/matchers/__tests__/dependency.spec.ts +++ b/src/evaluator/matchers/__tests__/dependency.spec.ts @@ -5,9 +5,7 @@ import { IMatcher, IMatcherDto } from '../../types'; import { IStorageSync } from '../../../storages/types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { ISplit } from '../../../dtos/types'; - -const ALWAYS_ON_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, '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 }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets':[] } as ISplit; -const ALWAYS_OFF_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-off', 'trafficAllocation': 100, 'trafficAllocationSeed': -331690370, 'seed': 403891040, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'on', 'changeNumber': 1494365020316, '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 }] }, 'partitions': [{ 'treatment': 'on', 'size': 0 }, { 'treatment': 'off', 'size': 100 }], 'label': 'in segment all' }], 'sets':[] } as ISplit; +import { ALWAYS_ON_SPLIT, ALWAYS_OFF_SPLIT } from '../../../storages/__tests__/testUtils'; const STORED_SPLITS: Record = { 'always-on': ALWAYS_ON_SPLIT, @@ -20,7 +18,7 @@ const mockStorage = { } }; -test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split returns one of the expected treatments', function () { +test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split returns one of the expected treatments', () => { const matcherTrueAlwaysOn = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { @@ -59,7 +57,7 @@ test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split ret expect(matcherFalseAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(false); // Parent split returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false }); -test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { +test('MATCHER IN_SPLIT_TREATMENT / Edge cases', () => { const matcherParentNotExist = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { diff --git a/src/evaluator/matchers/__tests__/eq.spec.ts b/src/evaluator/matchers/__tests__/eq.spec.ts index 6527b874..b9921ff7 100644 --- a/src/evaluator/matchers/__tests__/eq.spec.ts +++ b/src/evaluator/matchers/__tests__/eq.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER EQUAL / should return true ONLY when the value is equal to 10', function () { +test('MATCHER EQUAL / should return true ONLY when the value is equal to 10', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.EQUAL_TO, diff --git a/src/evaluator/matchers/__tests__/eq_set.spec.ts b/src/evaluator/matchers/__tests__/eq_set.spec.ts index dc60bdc3..ff575675 100644 --- a/src/evaluator/matchers/__tests__/eq_set.spec.ts +++ b/src/evaluator/matchers/__tests__/eq_set.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER EQUAL_TO_SET / should return true ONLY when value is equal to set ["update", "add"]', function () { +test('MATCHER EQUAL_TO_SET / should return true ONLY when value is equal to set ["update", "add"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.EQUAL_TO_SET, diff --git a/src/evaluator/matchers/__tests__/ew.spec.ts b/src/evaluator/matchers/__tests__/ew.spec.ts index beefef29..bc13e7ed 100644 --- a/src/evaluator/matchers/__tests__/ew.spec.ts +++ b/src/evaluator/matchers/__tests__/ew.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", "b", "c"]', function () { +test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", "b", "c"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.ENDS_WITH, @@ -17,7 +17,7 @@ test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", expect(matcher('manager')).toBe(false); // manager doesn't end with ["a", "b", "c"] }); -test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["demo.test.org"]', function () { +test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["demo.test.org"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.ENDS_WITH, diff --git a/src/evaluator/matchers/__tests__/gte.spec.ts b/src/evaluator/matchers/__tests__/gte.spec.ts index f38bd62f..9ecba7b0 100644 --- a/src/evaluator/matchers/__tests__/gte.spec.ts +++ b/src/evaluator/matchers/__tests__/gte.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER GREATER THAN OR EQUAL / should return true ONLY when the value is greater than or equal to 10', function () { +test('MATCHER GREATER THAN OR EQUAL / should return true ONLY when the value is greater than or equal to 10', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.GREATER_THAN_OR_EQUAL_TO, diff --git a/src/evaluator/matchers/__tests__/lte.spec.ts b/src/evaluator/matchers/__tests__/lte.spec.ts index b6aef174..84b190fe 100644 --- a/src/evaluator/matchers/__tests__/lte.spec.ts +++ b/src/evaluator/matchers/__tests__/lte.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER LESS THAN OR EQUAL / should return true ONLY when the value is less than or equal to 10', function () { +test('MATCHER LESS THAN OR EQUAL / should return true ONLY when the value is less than or equal to 10', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.LESS_THAN_OR_EQUAL_TO, diff --git a/src/evaluator/matchers/__tests__/part_of.spec.ts b/src/evaluator/matchers/__tests__/part_of.spec.ts index 3b740f82..89c249e3 100644 --- a/src/evaluator/matchers/__tests__/part_of.spec.ts +++ b/src/evaluator/matchers/__tests__/part_of.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER PART_OF_SET / should return true ONLY when value is part of of set ["update", "add", "delete"]', function () { +test('MATCHER PART_OF_SET / should return true ONLY when value is part of of set ["update", "add", "delete"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.PART_OF_SET, diff --git a/src/evaluator/matchers/__tests__/prerequisites.spec.ts b/src/evaluator/matchers/__tests__/prerequisites.spec.ts new file mode 100644 index 00000000..82059fc9 --- /dev/null +++ b/src/evaluator/matchers/__tests__/prerequisites.spec.ts @@ -0,0 +1,106 @@ +import { evaluateFeature } from '../../index'; +import { IStorageSync } from '../../../storages/types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { ISplit } from '../../../dtos/types'; +import { ALWAYS_ON_SPLIT, ALWAYS_OFF_SPLIT } from '../../../storages/__tests__/testUtils'; +import { prerequisitesMatcherContext } from '../prerequisites'; + +const STORED_SPLITS: Record = { + 'always-on': ALWAYS_ON_SPLIT, + 'always-off': ALWAYS_OFF_SPLIT +}; + +const mockStorage = { + splits: { + getSplit: (name: string) => STORED_SPLITS[name] + } +} as IStorageSync; + +test('MATCHER PREREQUISITES / should return true when all prerequisites are met', () => { + // A single prerequisite + const matcherTrueAlwaysOn = prerequisitesMatcherContext([{ + n: 'always-on', + ts: ['not-existing', 'on', 'other'] // We should match from a list of treatments + }], mockStorage, loggerMock); + expect(matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true + + const matcherFalseAlwaysOn = prerequisitesMatcherContext([{ + n: 'always-on', + ts: ['off', 'v1'] + }], mockStorage, loggerMock); + expect(matcherFalseAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false + + const matcherTrueAlwaysOff = prerequisitesMatcherContext([{ + n: 'always-off', + ts: ['not-existing', 'off'] + }], mockStorage, loggerMock); + expect(matcherTrueAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true + + const matcherFalseAlwaysOff = prerequisitesMatcherContext([{ + n: 'always-off', + ts: ['v1', 'on'] + }], mockStorage, loggerMock); + expect(matcherFalseAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false + + // Multiple prerequisites + const matcherTrueMultiplePrerequisites = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: ['on'] + }, + { + n: 'always-off', + ts: ['off'] + } + ], mockStorage, loggerMock); + expect(matcherTrueMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); // All prerequisites are met, so the matcher returns true + + const matcherFalseMultiplePrerequisites = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: ['on'] + }, + { + n: 'always-off', + ts: ['on'] + } + ], mockStorage, loggerMock); + expect(matcherFalseMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(false); // One of the prerequisites is not met, so the matcher returns false +}); + +test('MATCHER PREREQUISITES / Edge cases', () => { + // No prerequisites + const matcherTrueNoPrerequisites = prerequisitesMatcherContext(undefined, mockStorage, loggerMock); + expect(matcherTrueNoPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); + + const matcherTrueEmptyPrerequisites = prerequisitesMatcherContext([], mockStorage, loggerMock); + expect(matcherTrueEmptyPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); + + // Non existent feature flag + const matcherParentNotExist = prerequisitesMatcherContext([{ + n: 'not-existent-feature-flag', + ts: ['on', 'off'] + }], mockStorage, loggerMock); + expect(matcherParentNotExist({ key: 'a-key' }, evaluateFeature)).toBe(false); // If the feature flag does not exist, matcher should return false + + // Empty treatments list + const matcherNoTreatmentsExpected = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: [] + }], mockStorage, loggerMock); + expect(matcherNoTreatmentsExpected({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list is empty, matcher should return false (no treatment will match) + + const matcherExpectedTreatmentWrongTypeMatching = prerequisitesMatcherContext([{ + n: 'always-on', // @ts-ignore + ts: [null, [1, 2], 3, {}, true, 'on'] + + }], mockStorage, loggerMock); + expect(matcherExpectedTreatmentWrongTypeMatching({ key: 'a-key' }, evaluateFeature)).toBe(true); // If treatments expectation list has elements of the wrong type, those elements are overlooked. + + const matcherExpectedTreatmentWrongTypeNotMatching = prerequisitesMatcherContext([{ + n: 'always-off', // @ts-ignore + ts: [null, [1, 2], 3, {}, true, 'on'] + }], mockStorage, loggerMock); + expect(matcherExpectedTreatmentWrongTypeNotMatching({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list has elements of the wrong type, those elements are overlooked. +}); diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts new file mode 100644 index 00000000..db597738 --- /dev/null +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -0,0 +1,310 @@ +import { matcherTypes } from '../matcherTypes'; +import { matcherFactory } from '..'; +import { evaluateFeature } from '../../index'; +import { IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { IRBSegment, ISplit } from '../../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../../storages/types'; +import { thenable } from '../../../utils/promise/thenable'; +import { ALWAYS_ON_SPLIT } from '../../../storages/__tests__/testUtils'; + +const STORED_SPLITS: Record = { + 'always-on': ALWAYS_ON_SPLIT +}; + +const STORED_SEGMENTS: Record> = { + 'excluded_standard_segment': new Set(['emi@split.io']), + 'regular_segment': new Set(['nadia@split.io']) +}; + +const STORED_LARGE_SEGMENTS: Record> = { + 'excluded_large_segment': new Set(['emi-large@split.io']) +}; + +const STORED_RBSEGMENTS: Record = { + 'mauro_rule_based_segment': { + changeNumber: 5, + name: 'mauro_rule_based_segment', + status: 'ACTIVE', + excluded: { + keys: ['mauro@split.io', 'gaston@split.io'], + segments: [ + { type: 'standard', name: 'excluded_standard_segment' }, + { type: 'large', name: 'excluded_large_segment' }, + { type: 'rule-based', name: 'excluded_rule_based_segment' } + ] + }, + conditions: [ + { + matcherGroup: { + combiner: 'AND', + matchers: [ + { + keySelector: { + trafficType: 'user', + attribute: 'location', + }, + matcherType: 'WHITELIST', + negate: false, + whitelistMatcherData: { + whitelist: [ + 'mdp', + 'tandil', + 'bsas' + ] + } + }, + { + keySelector: { + trafficType: 'user', + attribute: null + }, + matcherType: 'ENDS_WITH', + negate: false, + whitelistMatcherData: { + whitelist: [ + '@split.io' + ] + } + } + ] + } + }, + { + matcherGroup: { + combiner: 'AND', + matchers: [ + { + keySelector: { + trafficType: 'user', + attribute: null + }, + matcherType: 'IN_SEGMENT', + negate: false, + userDefinedSegmentMatcherData: { + segmentName: 'regular_segment' + } + } + ] + } + } + ] + }, + 'depend_on_always_on': { + name: 'depend_on_always_on', + changeNumber: 123, + status: 'ACTIVE', + excluded: { + keys: null, + segments: null, + }, + conditions: [{ + matcherGroup: { + combiner: 'AND', + matchers: [{ + matcherType: 'IN_SPLIT_TREATMENT', + keySelector: { + trafficType: 'user', + attribute: null + }, + negate: false, + dependencyMatcherData: { + split: 'always-on', + treatments: [ + 'on', + ] + } + }] + } + }] + }, + 'depend_on_mauro_rule_based_segment': { + name: 'depend_on_mauro_rule_based_segment', + changeNumber: 123, + status: 'ACTIVE', + excluded: { + keys: [], + segments: [] + }, + conditions: [{ + matcherGroup: { + combiner: 'AND', + matchers: [{ + matcherType: 'IN_RULE_BASED_SEGMENT', + keySelector: { + trafficType: 'user', + attribute: null + }, + negate: false, + userDefinedSegmentMatcherData: { + segmentName: 'mauro_rule_based_segment' + } + }] + } + }] + }, + 'excluded_rule_based_segment': { + name: 'excluded_rule_based_segment', + changeNumber: 123, + status: 'ACTIVE', + conditions: [ + { + matcherGroup: { + combiner: 'AND', + matchers: [ + { + keySelector: null, + matcherType: 'WHITELIST', + negate: false, + userDefinedSegmentMatcherData: null, + whitelistMatcherData: { + whitelist: ['emi-rule-based@split.io'] + }, + unaryNumericMatcherData: null, + betweenMatcherData: null + } + ] + } + } + ], + }, + 'rule_based_segment_without_conditions': { + name: 'rule_based_segment_without_conditions', + changeNumber: 123, + status: 'ACTIVE', + conditions: [] + } +}; + +const mockStorageSync = { + isSync: true, + splits: { + getSplit(name: string) { + return STORED_SPLITS[name]; + } + }, + segments: { + isInSegment(segmentName: string, matchingKey: string) { + return STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false; + } + }, + largeSegments: { + isInSegment(segmentName: string, matchingKey: string) { + return STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false; + } + }, + rbSegments: { + get(rbsegmentName: string) { + return STORED_RBSEGMENTS[rbsegmentName]; + } + } +} as unknown as IStorageSync; + +const mockStorageAsync = { + isSync: false, + splits: { + getSplit(name: string) { + return Promise.resolve(STORED_SPLITS[name]); + } + }, + segments: { + isInSegment(segmentName: string, matchingKey: string) { + return Promise.resolve(STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false); + } + }, + largeSegments: { + isInSegment(segmentName: string, matchingKey: string) { + return Promise.resolve(STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false); + } + }, + rbSegments: { + get(rbsegmentName: string) { + return Promise.resolve(STORED_RBSEGMENTS[rbsegmentName]); + } + } +} as unknown as IStorageAsync; + +describe.each([ + { mockStorage: mockStorageSync, isAsync: false }, + { mockStorage: mockStorageAsync, isAsync: true } +])('MATCHER IN_RULE_BASED_SEGMENT', ({ mockStorage, isAsync }) => { + test('should support excluded keys, excluded segments, and multiple conditions', async () => { + const matcher = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'mauro_rule_based_segment' + } as IMatcherDto, mockStorage)!; + + const dependentMatcher = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'depend_on_mauro_rule_based_segment' + } as IMatcherDto, mockStorage)!; + + [matcher, dependentMatcher].forEach(async (matcher) => { + + // should return false if the provided key is excluded (even if some condition is met) + let match = matcher({ key: 'mauro@split.io', attributes: { location: 'mdp' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + + // should return false if the provided key is in some excluded standard segment (even if some condition is met) + match = matcher({ key: 'emi@split.io', attributes: { location: 'tandil' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + + // should return false if the provided key is in some excluded large segment (even if some condition is met) + match = matcher({ key: 'emi-large@split.io', attributes: { location: 'tandil' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + + // should return false if the provided key is in some excluded rule-based segment (even if some condition is met) + match = matcher({ key: 'emi-rule-based@split.io', attributes: { location: 'tandil' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + + // should return false if doesn't match any condition + match = matcher({ key: 'zeta@split.io' }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + match = matcher({ key: { matchingKey: 'zeta@split.io', bucketingKey: '123' }, attributes: { location: 'italy' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + + // should return true if match the first condition: location attribute in whitelist and key ends with '@split.io' + match = matcher({ key: 'emma@split.io', attributes: { location: 'tandil' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(true); + + // should return true if match the second condition: key in regular_segment + match = matcher({ key: { matchingKey: 'nadia@split.io', bucketingKey: '123' }, attributes: { location: 'mdp' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(true); + }); + }); + + test('edge cases', async () => { + const matcherNotExist = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'non_existent_segment' + } as IMatcherDto, mockStorageSync)!; + + // should return false if the provided segment does not exist + expect(await matcherNotExist({ key: 'a-key' }, evaluateFeature)).toBe(false); + + const matcherTrueAlwaysOn = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'depend_on_always_on' + } as IMatcherDto, mockStorageSync)!; + + // should support feature flag dependency matcher + expect(await matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Parent split returns one of the expected treatments, so the matcher returns true + + const matcherTrueRuleBasedSegmentWithoutConditions = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'rule_based_segment_without_conditions' + } as IMatcherDto, mockStorageSync)!; + + // should support rule-based segment without conditions + expect(await matcherTrueRuleBasedSegmentWithoutConditions({ key: 'a-key' }, evaluateFeature)).toBe(false); + }); + +}); diff --git a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts index 5e192829..7cb25079 100644 --- a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts @@ -4,7 +4,7 @@ import { IMatcher, IMatcherDto } from '../../../types'; import { IStorageSync } from '../../../../storages/types'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { +test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async () => { const segment = 'employees'; const matcherTrue = matcherFactory(loggerMock, { @@ -29,11 +29,11 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i } } as IStorageSync) as IMatcher; - expect(await matcherTrue()).toBe(true); // segment found in mySegments list - expect(await matcherFalse()).toBe(false); // segment not found in mySegments list + expect(await matcherTrue('key')).toBe(true); // segment found in mySegments list + expect(await matcherFalse('key')).toBe(false); // segment not found in mySegments list }); -test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { +test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async () => { const segment = 'employees'; const matcherTrue = matcherFactory(loggerMock, { @@ -54,6 +54,6 @@ test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is def largeSegments: undefined } as IStorageSync) as IMatcher; - expect(await matcherTrue()).toBe(true); // large segment found in mySegments list - expect(await matcherFalse()).toBe(false); // large segment storage is not defined + expect(await matcherTrue('key')).toBe(true); // large segment found in mySegments list + expect(await matcherFalse('key')).toBe(false); // large segment storage is not defined }); diff --git a/src/evaluator/matchers/__tests__/segment/server_side.spec.ts b/src/evaluator/matchers/__tests__/segment/server_side.spec.ts index 906aec62..d0819c11 100644 --- a/src/evaluator/matchers/__tests__/segment/server_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/server_side.spec.ts @@ -4,7 +4,7 @@ import { IMatcher, IMatcherDto } from '../../../types'; import { IStorageSync } from '../../../../storages/types'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER IN_SEGMENT / should return true ONLY when the key is defined inside the segment', async function () { +test('MATCHER IN_SEGMENT / should return true ONLY when the key is defined inside the segment', async () => { const segment = 'employees'; const matcher = matcherFactory(loggerMock, { diff --git a/src/evaluator/matchers/__tests__/sw.spec.ts b/src/evaluator/matchers/__tests__/sw.spec.ts index ef8c4f80..72e22258 100644 --- a/src/evaluator/matchers/__tests__/sw.spec.ts +++ b/src/evaluator/matchers/__tests__/sw.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER STARTS_WITH / should return true ONLY when the value starts with ["a", "b", "c"]', function () { +test('MATCHER STARTS_WITH / should return true ONLY when the value starts with ["a", "b", "c"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.STARTS_WITH, diff --git a/src/evaluator/matchers/__tests__/whitelist.spec.ts b/src/evaluator/matchers/__tests__/whitelist.spec.ts index 5ed12e77..1553c3bf 100644 --- a/src/evaluator/matchers/__tests__/whitelist.spec.ts +++ b/src/evaluator/matchers/__tests__/whitelist.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER WHITELIST / should return true ONLY when the value is in the list', function () { +test('MATCHER WHITELIST / should return true ONLY when the value is in the list', () => { const matcher = matcherFactory(loggerMock, { type: matcherTypes.WHITELIST, value: ['key'] diff --git a/src/evaluator/matchers/index.ts b/src/evaluator/matchers/index.ts index d50c38dd..f54cc313 100644 --- a/src/evaluator/matchers/index.ts +++ b/src/evaluator/matchers/index.ts @@ -24,6 +24,7 @@ import { inListSemverMatcherContext } from './semver_inlist'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { IMatcher, IMatcherDto } from '../types'; import { ILogger } from '../../logger/types'; +import { ruleBasedSegmentMatcherContext } from './rbsegment'; const matchers = [ undefined, // UNDEFINED: 0 @@ -50,6 +51,7 @@ const matchers = [ betweenSemverMatcherContext, // BETWEEN_SEMVER: 21 inListSemverMatcherContext, // IN_LIST_SEMVER: 22 largeSegmentMatcherContext, // IN_LARGE_SEGMENT: 23 + ruleBasedSegmentMatcherContext // IN_RULE_BASED_SEGMENT: 24 ]; /** @@ -64,5 +66,5 @@ export function matcherFactory(log: ILogger, matcherDto: IMatcherDto, storage?: let matcherFn; // @ts-ignore if (matchers[type]) matcherFn = matchers[type](value, storage, log); // There is no index-out-of-bound exception in JavaScript - return matcherFn; + return matcherFn as IMatcher; } diff --git a/src/evaluator/matchers/matcherTypes.ts b/src/evaluator/matchers/matcherTypes.ts index f09d50bf..0c5faf4b 100644 --- a/src/evaluator/matchers/matcherTypes.ts +++ b/src/evaluator/matchers/matcherTypes.ts @@ -23,6 +23,7 @@ export const matcherTypes: Record = { BETWEEN_SEMVER: 21, IN_LIST_SEMVER: 22, IN_LARGE_SEGMENT: 23, + IN_RULE_BASED_SEGMENT: 24, }; export const matcherDataTypes = { diff --git a/src/evaluator/matchers/prerequisites.ts b/src/evaluator/matchers/prerequisites.ts new file mode 100644 index 00000000..9bee45b3 --- /dev/null +++ b/src/evaluator/matchers/prerequisites.ts @@ -0,0 +1,24 @@ +import { ISplit, MaybeThenable } from '../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../storages/types'; +import { ILogger } from '../../logger/types'; +import { thenable } from '../../utils/promise/thenable'; +import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; + +export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger) { + + return function prerequisitesMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { + + function evaluatePrerequisite(prerequisite: { n: string; ts: string[] }): MaybeThenable { + const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); + return thenable(evaluation) ? + evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) !== -1) : + prerequisite.ts.indexOf(evaluation.treatment!) !== -1; + } + + return prerequisites.reduce>((prerequisitesMet, prerequisite) => { + return thenable(prerequisitesMet) ? + prerequisitesMet.then(prerequisitesMet => prerequisitesMet ? evaluatePrerequisite(prerequisite) : false) : + prerequisitesMet ? evaluatePrerequisite(prerequisite) : false; + }, true); + }; +} diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts new file mode 100644 index 00000000..f9cc12e4 --- /dev/null +++ b/src/evaluator/matchers/rbsegment.ts @@ -0,0 +1,74 @@ +import { IExcludedSegment, IRBSegment, MaybeThenable } from '../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../storages/types'; +import { ILogger } from '../../logger/types'; +import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; +import { thenable } from '../../utils/promise/thenable'; +import { getMatching, keyParser } from '../../utils/key'; +import { parser } from '../parser'; +import { STANDARD_SEGMENT, RULE_BASED_SEGMENT, LARGE_SEGMENT } from '../../utils/constants'; + + +export function ruleBasedSegmentMatcherContext(segmentName: string, storage: IStorageSync | IStorageAsync, log: ILogger) { + + return function ruleBasedSegmentMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { + const matchingKey = getMatching(key); + + function matchConditions(rbsegment: IRBSegment) { + const conditions = rbsegment.conditions || []; + + if (!conditions.length) return false; + + const evaluator = parser(log, conditions, storage); + + const evaluation = evaluator( + keyParser(key), + undefined, + undefined, + undefined, + attributes, + splitEvaluator + ); + + return thenable(evaluation) ? + evaluation.then(evaluation => evaluation ? true : false) : + evaluation ? true : false; + } + + function isInExcludedSegment({ type, name }: IExcludedSegment) { + return type === STANDARD_SEGMENT ? + storage.segments.isInSegment(name, matchingKey) : + type === RULE_BASED_SEGMENT ? + ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) : + type === LARGE_SEGMENT && storage.largeSegments ? + storage.largeSegments.isInSegment(name, matchingKey) : + false; + } + + function isExcluded(rbSegment: IRBSegment) { + const excluded = rbSegment.excluded || {}; + + if (excluded.keys && excluded.keys.indexOf(matchingKey) !== -1) return true; + + return (excluded.segments || []).reduce>((result, excludedSegment) => { + return thenable(result) ? + result.then(result => result || isInExcludedSegment(excludedSegment)) : + result || isInExcludedSegment(excludedSegment); + }, false); + } + + function isInRBSegment(rbSegment: IRBSegment | null) { + if (!rbSegment) return false; + const excluded = isExcluded(rbSegment); + + return thenable(excluded) ? + excluded.then(excluded => excluded ? false : matchConditions(rbSegment)) : + excluded ? false : matchConditions(rbSegment); + } + + const rbSegment = storage.rbSegments.get(segmentName); + + return thenable(rbSegment) ? + rbSegment.then(isInRBSegment) : + isInRBSegment(rbSegment); + }; +} diff --git a/src/evaluator/matchersTransform/__tests__/segment.spec.ts b/src/evaluator/matchersTransform/__tests__/segment.spec.ts index 1d25811e..cb9dd8fb 100644 --- a/src/evaluator/matchersTransform/__tests__/segment.spec.ts +++ b/src/evaluator/matchersTransform/__tests__/segment.spec.ts @@ -1,6 +1,6 @@ import { segmentTransform } from '../segment'; -test('TRANSFORMS / a segment object should be flatten to a string', function () { +test('TRANSFORMS / a segment object should be flatten to a string', () => { const segmentName = 'employees'; const sample = { segmentName @@ -11,7 +11,7 @@ test('TRANSFORMS / a segment object should be flatten to a string', function () expect(segmentName).toBe(plainSegmentName); // extracted segmentName matches }); -test('TRANSFORMS / if there is none segmentName entry, returns undefined', function () { +test('TRANSFORMS / if there is none segmentName entry, returns undefined', () => { const sample = undefined; const undefinedSegmentName = segmentTransform(sample); diff --git a/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts b/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts index 07483817..e4ed770a 100644 --- a/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts +++ b/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts @@ -1,6 +1,6 @@ import { whitelistTransform } from '../whitelist'; -test('TRANSFORMS / the whitelist array should be extracted', function () { +test('TRANSFORMS / the whitelist array should be extracted', () => { let sample = { whitelist: [ 'u1', diff --git a/src/evaluator/matchersTransform/index.ts b/src/evaluator/matchersTransform/index.ts index a5be15e3..6219c4dc 100644 --- a/src/evaluator/matchersTransform/index.ts +++ b/src/evaluator/matchersTransform/index.ts @@ -95,6 +95,9 @@ export function matchersTransform(matchers: ISplitMatcher[]): IMatcherDto[] { type === matcherTypes.LESS_THAN_OR_EQUAL_TO_SEMVER ) { value = stringMatcherData; + } else if (type === matcherTypes.IN_RULE_BASED_SEGMENT) { + value = segmentTransform(userDefinedSegmentMatcherData as IInSegmentMatcherData); + dataType = matcherDataTypes.NOT_SPECIFIED; } return { diff --git a/src/evaluator/parser/__tests__/boolean.spec.ts b/src/evaluator/parser/__tests__/boolean.spec.ts index 7d304ec9..255a5cf6 100644 --- a/src/evaluator/parser/__tests__/boolean.spec.ts +++ b/src/evaluator/parser/__tests__/boolean.spec.ts @@ -4,7 +4,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user.boolean is true then split 100%:on', async function () { +test('PARSER / if user.boolean is true then split 100%:on', async () => { // @ts-ignore const evaluator = parser(loggerMock, [{ diff --git a/src/evaluator/parser/__tests__/index.spec.ts b/src/evaluator/parser/__tests__/index.spec.ts index 30c10631..c3829a3d 100644 --- a/src/evaluator/parser/__tests__/index.spec.ts +++ b/src/evaluator/parser/__tests__/index.spec.ts @@ -4,7 +4,7 @@ import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user is in segment all 100%:on', async function () { +test('PARSER / if user is in segment all 100%:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -30,7 +30,7 @@ test('PARSER / if user is in segment all 100%:on', async function () { expect(evaluation.label).toBe('in segment all'); // in segment all }); -test('PARSER / if user is in segment all 100%:off', async function () { +test('PARSER / if user is in segment all 100%:off', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -58,7 +58,7 @@ test('PARSER / if user is in segment all 100%:off', async function () { expect(evaluation.label === 'in segment all').toBe(true); // in segment all }); -test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async function () { +test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -83,7 +83,7 @@ test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on', async function () { +test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -122,7 +122,7 @@ test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on expect(evaluation.label === 'whitelisted').toBe(true); // whitelisted }); -test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on, negated results', async function () { +test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -161,7 +161,7 @@ test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split expect(evaluation).toBe(undefined); // evaluation should throw undefined }); -test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', async function () { +test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -207,7 +207,7 @@ test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', expect(evaluation === undefined).toBe(true); // v4 is not defined inside the whitelist }); -test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -254,7 +254,7 @@ test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split expect(evaluation.label === 'whitelisted').toBe(true); // label should be "whitelisted" }); -test('PARSER / if user.account is in segment all then split 100:on', async function () { +test('PARSER / if user.account is in segment all then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', @@ -279,7 +279,7 @@ test('PARSER / if user.account is in segment all then split 100:on', async funct expect(evaluation.treatment === 'on').toBe(true); // ALL_KEYS always matches }); -test('PARSER / if user.attr is between 10 and 20 then split 100:on', async function () { +test('PARSER / if user.attr is between 10 and 20 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -320,7 +320,7 @@ test('PARSER / if user.attr is between 10 and 20 then split 100:on', async funct expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // undefined is not between 10 and 20 }); -test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -362,7 +362,7 @@ test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, nega expect(evaluation.treatment === 'on').toBe(true); // undefined is not between 10 and 20 }); -test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async function () { +test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -407,7 +407,7 @@ test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // missing attributes in the parameters list }); -test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -453,7 +453,7 @@ test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, expect(evaluation.treatment === 'on').toBe(true); // missing attributes in the parameters list }); -test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async function () { +test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -498,7 +498,7 @@ test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // missing attributes in the parameters list }); -test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -544,7 +544,7 @@ test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, expect(evaluation.treatment === 'on').toBe(true); // missing attributes in the parameters list }); -test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async function () { +test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -589,7 +589,7 @@ test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async f expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // missing attributes should be evaluated to false }); -test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -635,7 +635,7 @@ test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, expect(evaluation.treatment).toBe('on'); // missing attributes should be evaluated to false }); -test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async function () { +test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', diff --git a/src/evaluator/parser/__tests__/invalidMatcher.spec.ts b/src/evaluator/parser/__tests__/invalidMatcher.spec.ts index c69c8ded..87cfc422 100644 --- a/src/evaluator/parser/__tests__/invalidMatcher.spec.ts +++ b/src/evaluator/parser/__tests__/invalidMatcher.spec.ts @@ -3,7 +3,7 @@ import { parser } from '..'; import { ISplitCondition } from '../../../dtos/types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / handle invalid matcher as control', async function () { +test('PARSER / handle invalid matcher as control', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', @@ -32,7 +32,7 @@ test('PARSER / handle invalid matcher as control', async function () { expect(evaluation.label).toBe('targeting rule type unsupported by sdk'); // track invalid as targeting rule type unsupported by sdk }); -test('PARSER / handle invalid matcher as control (complex example)', async function () { +test('PARSER / handle invalid matcher as control (complex example)', async () => { const evaluator = parser(loggerMock, [ { 'conditionType': 'WHITELIST', @@ -132,7 +132,7 @@ test('PARSER / handle invalid matcher as control (complex example)', async funct } }); -test('PARSER / handle invalid matcher as control (complex example mixing invalid and valid matchers)', async function () { +test('PARSER / handle invalid matcher as control (complex example mixing invalid and valid matchers)', async () => { const evaluator = parser(loggerMock, [ { 'conditionType': 'WHITELIST', diff --git a/src/evaluator/parser/__tests__/regex.spec.ts b/src/evaluator/parser/__tests__/regex.spec.ts index 176c961a..736e93d3 100644 --- a/src/evaluator/parser/__tests__/regex.spec.ts +++ b/src/evaluator/parser/__tests__/regex.spec.ts @@ -4,7 +4,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user.string is true then split 100%:on', async function () { +test('PARSER / if user.string is true then split 100%:on', async () => { // @ts-ignore const evaluator = parser(loggerMock, [{ matcherGroup: { diff --git a/src/evaluator/parser/__tests__/set.spec.ts b/src/evaluator/parser/__tests__/set.spec.ts index 5d46abb8..6a6d8c35 100644 --- a/src/evaluator/parser/__tests__/set.spec.ts +++ b/src/evaluator/parser/__tests__/set.spec.ts @@ -7,7 +7,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; // // EQUAL_TO_SET // -test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on', async function () { +test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -42,7 +42,7 @@ test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "writ expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "write"] then split 100:on', async function () { +test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "write"] then split 100:on', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -77,7 +77,7 @@ test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "writ expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 100:on', async function () { +test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 100:on', async () => { const label = 'permissions = ["1", "2"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -112,7 +112,7 @@ test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 1 expect(evaluation.label).toBe(label); // label should be correct }); -test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["read", "write"] then not match', async function () { +test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["read", "write"] then not match', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -146,7 +146,7 @@ test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["re expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then not match', async function () { +test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then not match', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -180,7 +180,7 @@ test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "write"] then not match', async function () { +test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "write"] then not match', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -214,7 +214,7 @@ test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "wri expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argentina"] then split 100:on', async function () { +test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argentina"] then split 100:on', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -248,7 +248,7 @@ test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argen expect(evaluation.label).toBe(label); // label should match }); -test('PARSER / if attribute is not an array we should not match equal to set', async function () { +test('PARSER / if attribute is not an array we should not match equal to set', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -283,7 +283,7 @@ test('PARSER / if attribute is not an array we should not match equal to set', a expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match equal to set', async function () { +test('PARSER / if attribute is an EMPTY array we should not match equal to set', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -316,7 +316,7 @@ test('PARSER / if attribute is an EMPTY array we should not match equal to set', expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on should not match', async () => { const label = 'not permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -349,7 +349,7 @@ test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write"] false, then match', async function () { +test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write"] false, then match', async () => { const label = 'not permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -383,7 +383,7 @@ test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match equal to set, so match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match equal to set, so match', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -420,7 +420,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match equal to expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal to set, so match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal to set, so match', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -457,7 +457,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal // // CONTAINS_ALL_OF_SET // -test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on', async function () { +test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -492,7 +492,7 @@ test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of se expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of set ["read", "edit"] then split 100:on', async function () { +test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of set ["read", "edit"] then split 100:on', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -527,7 +527,7 @@ test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of se expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1", "edit"] then split 100:on', async function () { +test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1", "edit"] then split 100:on', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -562,7 +562,7 @@ test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1 expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] then not match', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -596,7 +596,7 @@ test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of set ["read", "edit"] then not match', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -630,7 +630,7 @@ test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is not an array we should not match contains all', async function () { +test('PARSER / if attribute is not an array we should not match contains all', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -665,7 +665,7 @@ test('PARSER / if attribute is not an array we should not match contains all', a expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match contains all', async function () { +test('PARSER / if attribute is an EMPTY array we should not match contains all', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -698,7 +698,7 @@ test('PARSER / if attribute is an EMPTY array we should not match contains all', expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on should not match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -731,7 +731,7 @@ test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains a expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", "edit"] false, so match', async function () { +test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", "edit"] false, so match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -765,7 +765,7 @@ test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match contains all, so match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match contains all, so match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -802,7 +802,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match contains expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains all, so match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains all, so match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -839,7 +839,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match contai // // PART_OF_SET // -test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -874,7 +874,7 @@ test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edi expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -909,7 +909,7 @@ test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edi expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["1", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -944,7 +944,7 @@ test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "del expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "edit"] then not match', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -978,7 +978,7 @@ test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "e expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if attribute is not an array we should not match part of', async function () { +test('PARSER / if attribute is not an array we should not match part of', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1013,7 +1013,7 @@ test('PARSER / if attribute is not an array we should not match part of', async expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match part of', async function () { +test('PARSER / if attribute is an EMPTY array we should not match part of', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1046,7 +1046,7 @@ test('PARSER / if attribute is an EMPTY array we should not match part of', asyn expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on should not match', async () => { const label = 'not permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1079,7 +1079,7 @@ test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["rea expect(evaluation).toBe(undefined); // evaluation should return treatment undefined }); -test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["read", "edit"] false, then match', async function () { +test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["read", "edit"] false, then match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1113,7 +1113,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["r expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match part of, so match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match part of, so match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1150,7 +1150,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match part of, expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match part of, so match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match part of, so match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1187,7 +1187,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match part o // // CONTAINS_ANY_OF_SET // -test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1222,7 +1222,7 @@ test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read" expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "1", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1257,7 +1257,7 @@ test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1" expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] then not match', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1291,7 +1291,7 @@ test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if attribute is not an array we should not match contains any', async function () { +test('PARSER / if attribute is not an array we should not match contains any', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1326,7 +1326,7 @@ test('PARSER / if attribute is not an array we should not match contains any', a expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match contains any', async function () { +test('PARSER / if attribute is an EMPTY array we should not match contains any', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1359,7 +1359,7 @@ test('PARSER / if attribute is an EMPTY array we should not match contains any', expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on should not match', async () => { const label = 'not permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1392,7 +1392,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] false, then should match', async function () { +test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] false, then should match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1426,7 +1426,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of se expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match contains any, then should match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match contains any, then should match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1458,7 +1458,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match contains expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains any, then should match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains any, then should match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { diff --git a/src/evaluator/parser/__tests__/string.spec.ts b/src/evaluator/parser/__tests__/string.spec.ts index dc10f3c6..81fe9b9d 100644 --- a/src/evaluator/parser/__tests__/string.spec.ts +++ b/src/evaluator/parser/__tests__/string.spec.ts @@ -7,7 +7,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; // // STARTS WITH // -test('PARSER / if user.email starts with ["nico"] then split 100:on', async function () { +test('PARSER / if user.email starts with ["nico"] then split 100:on', async () => { const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -41,7 +41,7 @@ test('PARSER / if user.email starts with ["nico"] then split 100:on', async func expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should match', async function () { +test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should match', async () => { const label = 'email starts with ["1"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -75,7 +75,7 @@ test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should m expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async function () { +test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async () => { const label = 'email starts with ["nico", "marcio", "facu"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -109,7 +109,7 @@ test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 1 expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async function () { +test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async () => { const label = 'email starts with ["nico", "marcio", "facu"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -143,7 +143,7 @@ test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 1 expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email does not start with ["nico"] then not match', async function () { +test('PARSER / if user.email does not start with ["nico"] then not match', async () => { // const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -175,7 +175,7 @@ test('PARSER / if user.email does not start with ["nico"] then not match', async expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not match', async function () { +test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not match', async () => { // const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -205,7 +205,7 @@ test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is not a string, start with ["nico"] should not match', async function () { +test('PARSER / if user.email is not a string, start with ["nico"] should not match', async () => { // const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -238,7 +238,7 @@ test('PARSER / if user.email is not a string, start with ["nico"] should not mat expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so not match', async function () { +test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so not match', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -271,7 +271,7 @@ test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email does not start with ["nico"] should not match, then match', async function () { +test('PARSER / NEGATED if user.email does not start with ["nico"] should not match, then match', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -305,7 +305,7 @@ test('PARSER / NEGATED if user.email does not start with ["nico"] should not mat expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] should not match, so negation should', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -337,7 +337,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] sho expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should not match, so negation should', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -376,7 +376,7 @@ test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should // // ENDS WITH // -test('PARSER / if user.email ends with ["split.io"] then split 100:on', async function () { +test('PARSER / if user.email ends with ["split.io"] then split 100:on', async () => { const label = 'email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -410,7 +410,7 @@ test('PARSER / if user.email ends with ["split.io"] then split 100:on', async fu expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should match', async function () { +test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should match', async () => { const label = 'email starts with ["3"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -443,7 +443,7 @@ test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should mat expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async function () { +test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async () => { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -477,7 +477,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async function () { +test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async () => { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -511,7 +511,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] but attribute is "" then split 100:on', async function () { +test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] but attribute is "" then split 100:on', async () => { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -544,7 +544,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email does not end with ["split.io"] then not match', async function () { +test('PARSER / if user.email does not end with ["split.io"] then not match', async () => { const label = 'email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -577,7 +577,7 @@ test('PARSER / if user.email does not end with ["split.io"] then not match', asy expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not match', async function () { +test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not match', async () => { // const label = 'email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -607,7 +607,7 @@ test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not ma expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is not a string, end with ["nico"] should not match', async function () { +test('PARSER / if user.email is not a string, end with ["nico"] should not match', async () => { // const label = 'email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -642,7 +642,7 @@ test('PARSER / if user.email is not a string, end with ["nico"] should not match expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, so not match', async function () { +test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, so not match', async () => { const label = 'not email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -674,7 +674,7 @@ test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, s expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email does not end with ["split.io"] then no match, so match', async function () { +test('PARSER / NEGATED if user.email does not end with ["split.io"] then no match, so match', async () => { const label = 'not email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -707,7 +707,7 @@ test('PARSER / NEGATED if user.email does not end with ["split.io"] then no matc expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] should not match, so negation should', async () => { const label = 'not email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -739,7 +739,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] shoul expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should not match, so negation should', async () => { const label = 'not email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -778,7 +778,7 @@ test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should n // // CONTAINS STRING // -test('PARSER / if user.email contains ["@split"] then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -812,7 +812,7 @@ test('PARSER / if user.email contains ["@split"] then split 100:on', async funct expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email = 123, contains ["2"] then split 100:on should match', async function () { +test('PARSER / if user.email = 123, contains ["2"] then split 100:on should match', async () => { const label = 'email contains ["2"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -846,7 +846,7 @@ test('PARSER / if user.email = 123, contains ["2"] then split 100:on should matc expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -880,7 +880,7 @@ test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -914,7 +914,7 @@ test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split"] (whole string matches) then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] (whole string matches) then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -948,7 +948,7 @@ test('PARSER / if user.email contains ["@split"] (whole string matches) then spl expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async function () { +test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async () => { const label = 'email contains ["@split", "@gmail", "@hotmail"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -982,7 +982,7 @@ test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then spli expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async function () { +test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async () => { const label = 'email contains ["@split", "@gmail", "@hotmail"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1016,7 +1016,7 @@ test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then spli expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email does not contain ["@split"] then not match', async function () { +test('PARSER / if user.email does not contain ["@split"] then not match', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1049,7 +1049,7 @@ test('PARSER / if user.email does not contain ["@split"] then not match', async expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not match', async function () { +test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not match', async () => { // const label = 'email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1079,7 +1079,7 @@ test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not ma expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is not a string, contains ["nico"] should not match', async function () { +test('PARSER / if user.email is not a string, contains ["nico"] should not match', async () => { // const label = 'email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1114,7 +1114,7 @@ test('PARSER / if user.email is not a string, contains ["nico"] should not match expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then no match', async function () { +test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then no match', async () => { const label = 'not email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1147,7 +1147,7 @@ test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, so match', async function () { +test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, so match', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1180,7 +1180,7 @@ test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, expect(evaluation.label).toBe(label); // }); -test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] should not match, so negation should', async () => { const label = 'not email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1212,7 +1212,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] shoul expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is not a string, contains ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is not a string, contains ["nico"] should not match, so negation should', async () => { const label = 'not email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { diff --git a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts index d9af5ca9..a71d6dee 100644 --- a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts +++ b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts @@ -5,7 +5,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', async function () { +test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', @@ -32,7 +32,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', a expect(evaluation.label).toBe('not in split'); // label should be fixed string }); -test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% with bucket below 99', async function () { +test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% with bucket below 99', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', @@ -59,7 +59,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% wi expect(evaluation.label).toBe('in segment all'); // in segment all }); -test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% and bucket returns 100', async function () { +test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% and bucket returns 100', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', @@ -86,7 +86,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% an expect(evaluation.label).toBe('not in split'); // label should be fixed string }); -test('PARSER / if user is whitelisted and in segment all 100%:off with trafficAllocation as 0%', async function () { +test('PARSER / if user is whitelisted and in segment all 100%:off with trafficAllocation as 0%', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'WHITELIST', diff --git a/src/evaluator/parser/index.ts b/src/evaluator/parser/index.ts index a398aa0b..d12edf1a 100644 --- a/src/evaluator/parser/index.ts +++ b/src/evaluator/parser/index.ts @@ -37,7 +37,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt } // Evaluator function. - return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => { + return (key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => { const value = sanitizeValue(log, key, matcherDto, attributes); let result: MaybeThenable = false; @@ -71,12 +71,12 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt predicates.push(conditionContext( log, andCombinerContext(log, expressions), - Treatments.parse(partitions), + partitions && Treatments.parse(partitions), label, conditionType )); } - // Instanciate evaluator given the set of conditions using if else if logic + // Instantiate evaluator given the set of conditions using if else if logic return ifElseIfCombinerContext(log, predicates); } diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 79bcda18..92806ddf 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -29,6 +29,6 @@ export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDi export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable -export type IEvaluator = (key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable +export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable -export type IMatcher = (...args: any) => MaybeThenable +export type IMatcher = (value: string | number | boolean | string[] | IDependencyMatcherValue, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index 95b4000c..06184aa5 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types'; import { sanitize } from './sanitize'; import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants'; -function parseValue(log: ILogger, key: string, attributeName: string | null, attributes?: SplitIO.Attributes) { +function parseValue(log: ILogger, key: SplitIO.SplitKey, attributeName: string | null, attributes?: SplitIO.Attributes) { let value = undefined; if (attributeName) { if (attributes) { @@ -23,7 +23,7 @@ function parseValue(log: ILogger, key: string, attributeName: string | null, att /** * Defines value to be matched (key / attribute). */ -export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { +export function sanitizeValue(log: ILogger, key: SplitIO.SplitKey, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { const attributeName = matcherDto.attribute; const valueToMatch = parseValue(log, key, attributeName, attributes); const sanitizedValue = sanitize(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes); diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 9fbf74f7..de92efc7 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -41,7 +41,7 @@ function sanitizeBoolean(val: any): boolean | undefined { return undefined; } -function dependencyProcessor(sanitizedValue: string, attributes?: SplitIO.Attributes): IDependencyMatcherValue { +function dependencyProcessor(sanitizedValue: SplitIO.SplitKey, attributes?: SplitIO.Attributes): IDependencyMatcherValue { return { key: sanitizedValue, attributes @@ -60,6 +60,7 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { case matcherTypes.BETWEEN: return dataType === 'DATETIME' ? zeroSinceSS : undefined; case matcherTypes.IN_SPLIT_TREATMENT: + case matcherTypes.IN_RULE_BASED_SEGMENT: return dependencyProcessor; default: return undefined; @@ -69,9 +70,9 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { /** * Sanitize matcher value */ -export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes?: SplitIO.Attributes) { +export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | SplitIO.SplitKey | undefined, dataType: string, attributes?: SplitIO.Attributes) { const processor = getProcessingFunction(matcherTypeID, dataType); - let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; + let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; switch (dataType) { case matcherDataTypes.NUMBER: @@ -88,7 +89,7 @@ export function sanitize(log: ILogger, matcherTypeID: number, value: string | nu sanitizedValue = sanitizeBoolean(value); break; case matcherDataTypes.NOT_SPECIFIED: - sanitizedValue = value; + sanitizedValue = value as any; break; default: sanitizedValue = undefined; diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 855675ff..729da6e1 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -21,12 +21,14 @@ export const RETRIEVE_MANAGER = 29; export const SYNC_OFFLINE_DATA = 30; export const SYNC_SPLITS_FETCH = 31; export const SYNC_SPLITS_UPDATE = 32; +export const SYNC_RBS_UPDATE = 33; export const STREAMING_NEW_MESSAGE = 35; export const SYNC_TASK_START = 36; export const SYNC_TASK_EXECUTE = 37; export const SYNC_TASK_STOP = 38; export const SETTINGS_SPLITS_FILTER = 39; export const ENGINE_MATCHER_RESULT = 40; +export const ENGINE_DEFAULT = 41; export const CLIENT_READY_FROM_CACHE = 100; export const CLIENT_READY = 101; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index 5dfcace3..c5e67dff 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -12,6 +12,7 @@ export const codesDebug: [number, string][] = codesInfo.concat([ [c.ENGINE_VALUE, c.LOG_PREFIX_ENGINE_VALUE + 'Extracted attribute `%s`. %s will be used for matching.'], [c.ENGINE_SANITIZE, c.LOG_PREFIX_ENGINE + ':sanitize: Attempted to sanitize %s which should be of type %s. Sanitized and processed value => %s'], [c.ENGINE_MATCHER_RESULT, c.LOG_PREFIX_ENGINE_MATCHER + '[%s] Result: %s. Rule value: %s. Evaluation value: %s'], + [c.ENGINE_DEFAULT, c.LOG_PREFIX_ENGINE + 'Evaluates to default treatment. %s'], // SDK [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'], [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'], @@ -20,8 +21,9 @@ export const codesDebug: [number, string][] = codesInfo.concat([ [c.RETRIEVE_MANAGER, 'Retrieving manager instance.'], // synchronizer [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Feature flags data: \n%s'], - [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up feature flags update using since = %s'], - [c.SYNC_SPLITS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags %s. Removed feature flags %s. Segment names collected %s'], + [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up feature flags update using since = %s and rbSince = %s.'], + [c.SYNC_SPLITS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags: %s. Removed feature flags: %s.'], + [c.SYNC_RBS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New rule-based segments: %s. Removed rule-based segments: %s.'], [c.STREAMING_NEW_MESSAGE, c.LOG_PREFIX_SYNC_STREAMING + 'New SSE message received, with data: %s.'], [c.SYNC_TASK_START, c.LOG_PREFIX_SYNC + ': Starting %s. Running each %s millis'], [c.SYNC_TASK_EXECUTE, c.LOG_PREFIX_SYNC + ': Running %s'], diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 568771a8..81cfda1a 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -33,7 +33,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_SDK_KEY, c.LOG_PREFIX_SETTINGS + ': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'], [c.STREAMING_PARSING_MEMBERSHIPS_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching Memberships due to an error processing %s notification: %s'], - [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'], + [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing %s notification: %s'], [c.WARN_INVALID_FLAGSET, '%s: you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. %s was discarded.'], [c.WARN_LOWERCASE_FLAGSET, '%s: flag set %s should be all lowercase - converting string to lowercase.'], [c.WARN_FLAGSET_WITHOUT_FLAGS, '%s: you passed %s flag set that does not contain cached feature flag names. Please double check what flag sets are in use in the Split user interface.'], diff --git a/src/sdkManager/__tests__/mocks/input.json b/src/sdkManager/__tests__/mocks/input.json index 7aea3413..313dd38e 100644 --- a/src/sdkManager/__tests__/mocks/input.json +++ b/src/sdkManager/__tests__/mocks/input.json @@ -41,5 +41,15 @@ "configurations": { "on": "\"color\": \"green\"" }, - "sets": ["set_a"] + "sets": [ + "set_a" + ], + "prerequisites": [ + { + "n": "some_flag", + "ts": [ + "on" + ] + } + ] } diff --git a/src/sdkManager/__tests__/mocks/output.json b/src/sdkManager/__tests__/mocks/output.json index 52a0b636..67948bf9 100644 --- a/src/sdkManager/__tests__/mocks/output.json +++ b/src/sdkManager/__tests__/mocks/output.json @@ -9,5 +9,9 @@ }, "sets": ["set_a"], "defaultTreatment": "off", - "impressionsDisabled": false + "impressionsDisabled": false, + "prerequisites": [{ + "flagName": "some_flag", + "treatments": ["on"] + }] } diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 82423016..d241b82e 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -17,7 +17,7 @@ function collectTreatments(splitObject: ISplit) { // Localstorage mode could fall into a no rollout conditions state. Take the first condition in that case. if (!allTreatmentsCondition) allTreatmentsCondition = conditions[0]; // Then extract the treatments from the partitions - return allTreatmentsCondition ? allTreatmentsCondition.partitions.map(v => v.treatment) : []; + return allTreatmentsCondition ? allTreatmentsCondition.partitions!.map(v => v.treatment) : []; } function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { @@ -32,7 +32,8 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { configs: splitObject.configurations || {}, sets: splitObject.sets || [], defaultTreatment: splitObject.defaultTreatment, - impressionsDisabled: splitObject.impressionsDisabled === true + impressionsDisabled: splitObject.impressionsDisabled === true, + prerequisites: (splitObject.prerequisites || []).map(p => ({ flagName: p.n, treatments: p.ts })), }; } diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index d935c6de..196266a3 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -40,10 +40,10 @@ describe('splitApi', () => { assertHeaders(settings, headers); expect(url).toBe('sdk/segmentChanges/segmentName?since=-1&till=90'); - splitApi.fetchSplitChanges(-1, false, 100); + splitApi.fetchSplitChanges(-1, false, 100, -1); [url, { headers }] = fetchMock.mock.calls[3]; assertHeaders(settings, headers); - expect(url).toBe(expecteFlagsUrl(-1, 100, settings.validateFilters || false, settings)); + expect(url).toBe(expectedFlagsUrl(-1, 100, settings.validateFilters || false, settings, -1)); splitApi.postEventsBulk('fake-body'); assertHeaders(settings, fetchMock.mock.calls[4][1].headers); @@ -66,9 +66,9 @@ describe('splitApi', () => { fetchMock.mockClear(); - function expecteFlagsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings) { + function expectedFlagsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings, rbSince?: number) { const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; - return `sdk/splitChanges?s=1.1&since=${since}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`; + return `sdk/splitChanges?s=1.1&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`; } }); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 0b86b58d..6860b022 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -29,7 +29,6 @@ export function splitApiFactory( const urls = settings.urls; const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; const SplitSDKImpressionsMode = settings.sync.impressionsMode; - const flagSpecVersion = settings.sync.flagSpecVersion; const splitHttpClient = splitHttpClientFactory(settings, platform); return { @@ -45,7 +44,7 @@ export function splitApiFactory( }, fetchAuth(userMatchingKeys?: string[]) { - let url = `${urls.auth}/v2/auth?s=${flagSpecVersion}`; + let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`; if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&'); if (queryParams) url += '&' + queryParams; @@ -53,8 +52,8 @@ export function splitApiFactory( return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN)); }, - fetchSplitChanges(since: number, noCache?: boolean, till?: number) { - const url = `${urls.sdk}/splitChanges?s=${flagSpecVersion}&since=${since}${filterQueryString || ''}${till ? '&till=' + till : ''}`; + fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) { + const url = `${urls.sdk}/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) .catch((err) => { if (err.statusCode === 414) settings.log.error(ERROR_TOO_MANY_SETS); diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index 47566c0c..dcb841c8 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -47,7 +47,7 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful .then(response => { if (!response.ok) { - // `text()` promise might not settle in some fetch implementations and cases (e.g. no content) + // timeout since `text()` promise might not settle in some fetch implementations and cases (e.g. no content) return timeout(PENDING_FETCH_ERROR_TIMEOUT, response.text()).then(message => Promise.reject({ response, message }), () => Promise.reject({ response })); } latencyTracker(); diff --git a/src/services/types.ts b/src/services/types.ts index 34708f90..b747dbb5 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -35,7 +35,7 @@ export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyT export type IFetchAuth = (userKeys?: string[]) => Promise -export type IFetchSplitChanges = (since: number, noCache?: boolean, till?: number) => Promise +export type IFetchSplitChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean, till?: number) => Promise diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 483deb42..761c5cb9 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -1,5 +1,5 @@ import { ISplitsCacheSync } from './types'; -import { ISplit } from '../dtos/types'; +import { IRBSegment, ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; @@ -72,8 +72,8 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { * Given a parsed split, it returns a boolean flagging if its conditions use segments matchers (rules & whitelists). * This util is intended to simplify the implementation of `splitsCache::usesSegments` method */ -export function usesSegments(split: ISplit) { - const conditions = split.conditions || []; +export function usesSegments(ruleEntity: ISplit | IRBSegment) { + const conditions = ruleEntity.conditions || []; for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; @@ -83,5 +83,8 @@ export function usesSegments(split: ISplit) { } } + const excluded = (ruleEntity as IRBSegment).excluded; + if (excluded && excluded.segments && excluded.segments.length > 0) return true; + return false; } diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 2f5dc800..4167d860 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -37,6 +37,18 @@ export class KeyBuilder { return `${this.prefix}.split.`; } + buildRBSegmentKey(rbsegmentName: string) { + return `${this.prefix}.rbsegment.${rbsegmentName}`; + } + + buildRBSegmentsTillKey() { + return `${this.prefix}.rbsegments.till`; + } + + buildRBSegmentKeyPrefix() { + return `${this.prefix}.rbsegment.`; + } + buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index d3404ed1..deae16af 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -47,6 +47,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { return startsWith(key, `${this.prefix}.split.`); } + isRBSegmentKey(key: string) { + return startsWith(key, `${this.prefix}.rbsegment.`); + } + buildSplitsWithSegmentCountKey() { return `${this.prefix}.splits.usingSegments`; } diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 6232d88a..cf8d2156 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -53,6 +53,10 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.buildSplitKeyPrefix()}*`; } + searchPatternForRBSegmentKeys() { + return `${this.buildRBSegmentKeyPrefix()}*`; + } + /* Telemetry keys */ buildLatencyKey(method: Method, bucket: number) { diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index 45af194c..bd21fa66 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -105,17 +105,22 @@ test('KEYS / latency and exception keys (telemetry)', () => { test('getStorageHash', () => { expect(getStorageHash({ - core: { authorizationKey: '' }, - sync: { __splitFiltersValidation: { queryString: '&names=p1__split,p2__split' }, flagSpecVersion: '1.2' } - } as ISettings)).toBe('7ccd6b31'); + core: { authorizationKey: 'sdk-key' }, + sync: { __splitFiltersValidation: { queryString: '&names=p1__split,p2__split' }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('d700da23'); expect(getStorageHash({ - core: { authorizationKey: '' }, - sync: { __splitFiltersValidation: { queryString: '&names=p2__split,p3__split' }, flagSpecVersion: '1.2' } - } as ISettings)).toBe('2a25d0e1'); + core: { authorizationKey: 'sdk-key' }, + sync: { __splitFiltersValidation: { queryString: '&names=p2__split,p3__split' }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('8c8a8789'); expect(getStorageHash({ - core: { authorizationKey: '' }, - sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.2' } - } as ISettings)).toBe('db8943b4'); + core: { authorizationKey: 'aaaabbbbcccc1234' }, + sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('dc1f9817'); + + expect(getStorageHash({ + core: { authorizationKey: 'another-sdk-key' }, + sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('45c6ba5d'); }); diff --git a/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts new file mode 100644 index 00000000..2e222f32 --- /dev/null +++ b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts @@ -0,0 +1,60 @@ +import { RBSegmentsCacheInRedis } from '../inRedis/RBSegmentsCacheInRedis'; +import { RBSegmentsCachePluggable } from '../pluggable/RBSegmentsCachePluggable'; +import { KeyBuilderSS } from '../KeyBuilderSS'; +import { rbSegment, rbSegmentWithInSegmentMatcher } from '../__tests__/testUtils'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; +import { metadata } from './KeyBuilder.spec'; +import { RedisAdapter } from '../inRedis/RedisAdapter'; +import { wrapperMockFactory } from '../pluggable/__tests__/wrapper.mock'; + +const keys = new KeyBuilderSS('RBSEGMENT', metadata); + +const redisClient = new RedisAdapter(loggerMock); +const cacheInRedis = new RBSegmentsCacheInRedis(loggerMock, keys, redisClient); + +const storageWrapper = wrapperMockFactory(); +const cachePluggable = new RBSegmentsCachePluggable(loggerMock, keys, storageWrapper); + +describe.each([{ cache: cacheInRedis, wrapper: redisClient }, { cache: cachePluggable, wrapper: storageWrapper }])('Rule-based segments cache async (Redis & Pluggable)', ({ cache, wrapper }) => { + + afterAll(async () => { + await wrapper.del(keys.buildRBSegmentsTillKey()); + await wrapper.disconnect(); + }); + + test('update should add and remove segments correctly', async () => { + // Add segments + expect(await cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1)).toBe(true); + expect(await cache.get(rbSegment.name)).toEqual(rbSegment); + expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(await cache.getChangeNumber()).toBe(1); + + // Remove a segment + expect(await cache.update([], [rbSegment], 2)).toBe(true); + expect(await cache.get(rbSegment.name)).toBeNull(); + expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(await cache.getChangeNumber()).toBe(2); + + // Remove remaining segment + expect(await cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true); + expect(await cache.get(rbSegment.name)).toBeNull(); + expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull(); + expect(await cache.getChangeNumber()).toBe(3); + + // No changes + expect(await cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false); + expect(await cache.getChangeNumber()).toBe(4); + }); + + test('contains should check for segment existence correctly', async () => { + await cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + + expect(await cache.contains(new Set())).toBe(true); + expect(await cache.contains(new Set([rbSegment.name]))).toBe(true); + expect(await cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); + expect(await cache.contains(new Set(['nonexistent']))).toBe(false); + expect(await cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); + + await cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2); + }); +}); diff --git a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts new file mode 100644 index 00000000..03579351 --- /dev/null +++ b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts @@ -0,0 +1,75 @@ +import { RBSegmentsCacheInMemory } from '../inMemory/RBSegmentsCacheInMemory'; +import { RBSegmentsCacheInLocal } from '../inLocalStorage/RBSegmentsCacheInLocal'; +import { KeyBuilderCS } from '../KeyBuilderCS'; +import { rbSegment, rbSegmentWithInSegmentMatcher } from '../__tests__/testUtils'; +import { IRBSegmentsCacheSync } from '../types'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; + +const cacheInMemory = new RBSegmentsCacheInMemory(); +const cacheInLocal = new RBSegmentsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + +describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Memory & LocalStorage)', (cache: IRBSegmentsCacheSync) => { + + beforeEach(() => { + cache.clear(); + }); + + test('clear should reset the cache state', () => { + cache.update([rbSegment], [], 1); + expect(cache.getChangeNumber()).toBe(1); + expect(cache.get(rbSegment.name)).not.toBeNull(); + + cache.clear(); + expect(cache.getChangeNumber()).toBe(-1); + expect(cache.get(rbSegment.name)).toBeNull(); + }); + + test('update should add and remove segments correctly', () => { + // Add segments + expect(cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1)).toBe(true); + expect(cache.get(rbSegment.name)).toEqual(rbSegment); + expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(cache.getChangeNumber()).toBe(1); + + // Remove a segment + expect(cache.update([], [rbSegment], 2)).toBe(true); + expect(cache.get(rbSegment.name)).toBeNull(); + expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(cache.getChangeNumber()).toBe(2); + + // Remove remaining segment + expect(cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true); + expect(cache.get(rbSegment.name)).toBeNull(); + expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull(); + expect(cache.getChangeNumber()).toBe(3); + + // No changes + expect(cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false); + expect(cache.getChangeNumber()).toBe(4); + }); + + test('contains should check for segment existence correctly', () => { + cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + + expect(cache.contains(new Set())).toBe(true); + expect(cache.contains(new Set([rbSegment.name]))).toBe(true); + expect(cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); + expect(cache.contains(new Set(['nonexistent']))).toBe(false); + expect(cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); + + cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2); + }); + + test('usesSegments should track segments usage correctly', () => { + expect(cache.usesSegments()).toBe(false); // No rbSegments, so false + + cache.update([rbSegment], [], 1); // rbSegment doesn't have IN_SEGMENT matcher + expect(cache.usesSegments()).toBe(false); + + cache.update([rbSegmentWithInSegmentMatcher], [], 2); // rbSegmentWithInSegmentMatcher has IN_SEGMENT matcher + expect(cache.usesSegments()).toBe(true); + + cache.clear(); + expect(cache.usesSegments()).toBe(false); // False after clear since there are no rbSegments + }); +}); diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index fa38944f..b2ae79dc 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -1,4 +1,4 @@ -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; import { IStorageSync, IStorageAsync, IImpressionsCacheSync, IEventsCacheSync } from '../types'; // Assert that instances created by storage factories have the expected interface @@ -22,15 +22,13 @@ export function assertSyncRecorderCacheInterface(cache: IEventsCacheSync | IImpr // Split mocks -//@ts-ignore -export const splitWithUserTT: ISplit = { name: 'user_ff', trafficTypeName: 'user_tt', conditions: [] }; -//@ts-ignore -export const splitWithAccountTT: ISplit = { name: 'account_ff', trafficTypeName: 'account_tt', conditions: [] }; -//@ts-ignore -export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'account_tt', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; -//@ts-ignore -export const something: ISplit = { name: 'something' }; -//@ts-ignore + +export const ALWAYS_ON_SPLIT: ISplit = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, '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 }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets': [] }; +export const ALWAYS_OFF_SPLIT: ISplit = { 'trafficTypeName': 'user', 'name': 'always-off', 'trafficAllocation': 100, 'trafficAllocationSeed': -331690370, 'seed': 403891040, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'on', 'changeNumber': 1494365020316, '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 }] }, 'partitions': [{ 'treatment': 'on', 'size': 0 }, { 'treatment': 'off', 'size': 100 }], 'label': 'in segment all' }], 'sets': [] }; //@ts-ignore +export const splitWithUserTT: ISplit = { name: 'user_ff', trafficTypeName: 'user_tt', conditions: [] }; //@ts-ignore +export const splitWithAccountTT: ISplit = { name: 'account_ff', trafficTypeName: 'account_tt', conditions: [] }; //@ts-ignore +export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'account_tt', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; //@ts-ignore +export const something: ISplit = { name: 'something' }; //@ts-ignore export const somethingElse: ISplit = { name: 'something else' }; // - With flag sets @@ -38,10 +36,16 @@ export const somethingElse: ISplit = { name: 'something else' }; //@ts-ignore export const featureFlagWithEmptyFS: ISplit = { name: 'ff_empty', sets: [] }; //@ts-ignore -export const featureFlagOne: ISplit = { name: 'ff_one', sets: ['o','n','e'] }; +export const featureFlagOne: ISplit = { name: 'ff_one', sets: ['o', 'n', 'e'] }; //@ts-ignore -export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t','w','o'] }; +export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t', 'w', 'o'] }; //@ts-ignore -export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t','h','r','e'] }; +export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t', 'h', 'r', 'e'] }; //@ts-ignore export const featureFlagWithoutFS: ISplit = { name: 'ff_four' }; + +// Rule-based segments +//@ts-ignore +export const rbSegment: IRBSegment = { name: 'rb_segment', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'EQUAL_TO', unaryNumericMatcherData: { value: 10 } }] } }] }; +//@ts-ignore +export const rbSegmentWithInSegmentMatcher: IRBSegment = { name: 'rb_segment_with_in_segment_matcher', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts new file mode 100644 index 00000000..37f6ad8e --- /dev/null +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -0,0 +1,136 @@ +import { IRBSegment } from '../../dtos/types'; +import { ILogger } from '../../logger/types'; +import { ISettings } from '../../types'; +import { isFiniteNumber, isNaNNumber, toNumber } from '../../utils/lang'; +import { setToArray } from '../../utils/lang/sets'; +import { usesSegments } from '../AbstractSplitsCacheSync'; +import { KeyBuilderCS } from '../KeyBuilderCS'; +import { IRBSegmentsCacheSync } from '../types'; +import { LOG_PREFIX } from './constants'; + +export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { + + private readonly keys: KeyBuilderCS; + private readonly log: ILogger; + + constructor(settings: ISettings, keys: KeyBuilderCS) { + this.keys = keys; + this.log = settings.log; + } + + clear() { + this.getNames().forEach(name => this.remove(name)); + localStorage.removeItem(this.keys.buildRBSegmentsTillKey()); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + this.setChangeNumber(changeNumber); + const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); + return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + } + + private setChangeNumber(changeNumber: number) { + try { + localStorage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); + localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); + } catch (e) { + this.log.error(LOG_PREFIX + e); + } + } + + private updateSegmentCount(diff: number) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + const count = toNumber(localStorage.getItem(segmentsCountKey)) + diff; + // @ts-expect-error + if (count > 0) localStorage.setItem(segmentsCountKey, count); + else localStorage.removeItem(segmentsCountKey); + } + + private add(rbSegment: IRBSegment): boolean { + try { + const name = rbSegment.name; + const rbSegmentKey = this.keys.buildRBSegmentKey(name); + const rbSegmentFromLocalStorage = localStorage.getItem(rbSegmentKey); + const previous = rbSegmentFromLocalStorage ? JSON.parse(rbSegmentFromLocalStorage) : null; + + localStorage.setItem(rbSegmentKey, JSON.stringify(rbSegment)); + + let usesSegmentsDiff = 0; + if (previous && usesSegments(previous)) usesSegmentsDiff--; + if (usesSegments(rbSegment)) usesSegmentsDiff++; + if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff); + + return true; + } catch (e) { + this.log.error(LOG_PREFIX + e); + return false; + } + } + + private remove(name: string): boolean { + try { + const rbSegment = this.get(name); + if (!rbSegment) return false; + + localStorage.removeItem(this.keys.buildRBSegmentKey(name)); + + if (usesSegments(rbSegment)) this.updateSegmentCount(-1); + + return true; + } catch (e) { + this.log.error(LOG_PREFIX + e); + return false; + } + } + + private getNames(): string[] { + const len = localStorage.length; + const accum = []; + + let cur = 0; + + while (cur < len) { + const key = localStorage.key(cur); + + if (key != null && this.keys.isRBSegmentKey(key)) accum.push(this.keys.extractKey(key)); + + cur++; + } + + return accum; + } + + get(name: string): IRBSegment | null { + const item = localStorage.getItem(this.keys.buildRBSegmentKey(name)); + return item && JSON.parse(item); + } + + contains(names: Set): boolean { + const namesArray = setToArray(names); + const namesInStorage = this.getNames(); + return namesArray.every(name => namesInStorage.indexOf(name) !== -1); + } + + getChangeNumber(): number { + const n = -1; + let value: string | number | null = localStorage.getItem(this.keys.buildRBSegmentsTillKey()); + + if (value !== null) { + value = parseInt(value, 10); + + return isNaNNumber(value) ? n : value; + } + + return n; + } + + usesSegments(): boolean { + const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); + const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); + + return isFiniteNumber(splitsWithSegmentsCount) ? + splitsWithSegmentsCount > 0 : + true; + } + +} diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index c3cb3142..2fb6183c 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -47,16 +47,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private _incrementCounts(split: ISplit) { try { - if (split) { - const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - // @ts-expect-error - localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1); + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + // @ts-expect-error + localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1); - if (usesSegments(split)) { - const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - // @ts-expect-error - localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1); - } + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + // @ts-expect-error + localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1); } } catch (e) { this.log.error(LOG_PREFIX + e); @@ -185,11 +183,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); - if (isFiniteNumber(splitsWithSegmentsCount)) { - return splitsWithSegmentsCount > 0; - } else { - return true; - } + return isFiniteNumber(splitsWithSegmentsCount) ? + splitsWithSegmentsCount > 0 : + true; } getNamesByFlagSets(flagSets: string[]): Set[] { diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index 27050a56..b87fa67b 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -5,8 +5,9 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; import { nearlyEqual } from '../../../__tests__/testUtils'; import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; +import { RBSegmentsCacheInLocal } from '../RBSegmentsCacheInLocal'; -const FULL_SETTINGS_HASH = '404832b3'; +const FULL_SETTINGS_HASH = 'dc1f9817'; describe('validateCache', () => { const keys = new KeyBuilderCS('SPLITIO', 'user'); @@ -14,9 +15,11 @@ describe('validateCache', () => { const segments = new MySegmentsCacheInLocal(fullSettings.log, keys); const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys); const splits = new SplitsCacheInLocal(fullSettings, keys); + const rbSegments = new RBSegmentsCacheInLocal(fullSettings, keys); - jest.spyOn(splits, 'clear'); jest.spyOn(splits, 'getChangeNumber'); + jest.spyOn(splits, 'clear'); + jest.spyOn(rbSegments, 'clear'); jest.spyOn(segments, 'clear'); jest.spyOn(largeSegments, 'clear'); @@ -26,11 +29,12 @@ describe('validateCache', () => { }); test('if there is no cache, it should return false', () => { - expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).not.toHaveBeenCalled(); expect(splits.clear).not.toHaveBeenCalled(); + expect(rbSegments.clear).not.toHaveBeenCalled(); expect(segments.clear).not.toHaveBeenCalled(); expect(largeSegments.clear).not.toHaveBeenCalled(); expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); @@ -43,11 +47,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); expect(logSpy).not.toHaveBeenCalled(); expect(splits.clear).not.toHaveBeenCalled(); + expect(rbSegments.clear).not.toHaveBeenCalled(); expect(segments.clear).not.toHaveBeenCalled(); expect(largeSegments.clear).not.toHaveBeenCalled(); expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); @@ -61,11 +66,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago - expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); + expect(rbSegments.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); @@ -77,15 +83,16 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); + expect(rbSegments.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); - expect(localStorage.getItem(keys.buildHashKey())).toBe('aa4877c2'); + expect(localStorage.getItem(keys.buildHashKey())).toBe('45c6ba5d'); expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); }); @@ -94,11 +101,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); + expect(rbSegments.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); @@ -109,15 +117,16 @@ describe('validateCache', () => { // If cache is cleared, it should not clear again until a day has passed logSpy.mockClear(); localStorage.setItem(keys.buildSplitsTillKey(), '1'); - expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); expect(logSpy).not.toHaveBeenCalled(); expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed // If a day has passed, it should clear again localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); - expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(2); + expect(rbSegments.clear).toHaveBeenCalledTimes(2); expect(segments.clear).toHaveBeenCalledTimes(2); expect(largeSegments.clear).toHaveBeenCalledTimes(2); expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 616bb7d7..8924b84d 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -6,6 +6,7 @@ import { validatePrefix } from '../KeyBuilder'; import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; import { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal'; import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; @@ -36,11 +37,13 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt const keys = new KeyBuilderCS(prefix, matchingKey); const splits = new SplitsCacheInLocal(settings, keys); + const rbSegments = new RBSegmentsCacheInLocal(settings, keys); const segments = new MySegmentsCacheInLocal(log, keys); const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)); return { splits, + rbSegments, segments, largeSegments, impressions: new ImpressionsCacheInMemory(impressionsQueueSize), @@ -50,7 +53,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt uniqueKeys: new UniqueKeysCacheInMemoryCS(), validateCache() { - return validateCache(options, settings, keys, splits, segments, largeSegments); + return validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments); }, destroy() { }, @@ -60,6 +63,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt return { splits: this.splits, + rbSegments: this.rbSegments, segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)), largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)), impressions: this.impressions, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index c9bd78d2..93d3144c 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -3,6 +3,7 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import { getStorageHash } from '../KeyBuilder'; import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal'; import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; import SplitIO from '../../../types/splitio'; @@ -66,13 +67,14 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS * * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) */ -export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { +export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { const currentTimestamp = Date.now(); const isThereCache = splits.getChangeNumber() > -1; if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) { splits.clear(); + rbSegments.clear(); segments.clear(); largeSegments.clear(); diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 7ec099d1..e89a875d 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -7,6 +7,7 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory'; import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory'; +import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory'; /** * InMemory storage factory for standalone server-side SplitFactory @@ -17,10 +18,12 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { __splitFiltersValidation } } } = params; const splits = new SplitsCacheInMemory(__splitFiltersValidation); + const rbSegments = new RBSegmentsCacheInMemory(); const segments = new SegmentsCacheInMemory(); const storage = { splits, + rbSegments, segments, impressions: new ImpressionsCacheInMemory(impressionsQueueSize), impressionCounts: new ImpressionCountsCacheInMemory(), diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index bfaec159..5ae8351c 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -7,6 +7,7 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS'; +import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory'; /** * InMemory storage factory for standalone client-side SplitFactory @@ -17,11 +18,13 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize }, sync: { __splitFiltersValidation } } } = params; const splits = new SplitsCacheInMemory(__splitFiltersValidation); + const rbSegments = new RBSegmentsCacheInMemory(); const segments = new MySegmentsCacheInMemory(); const largeSegments = new MySegmentsCacheInMemory(); const storage = { splits, + rbSegments, segments, largeSegments, impressions: new ImpressionsCacheInMemory(impressionsQueueSize), @@ -36,6 +39,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag shared() { return { splits: this.splits, + rbSegments: this.rbSegments, segments: new MySegmentsCacheInMemory(), largeSegments: new MySegmentsCacheInMemory(), impressions: this.impressions, diff --git a/src/storages/inMemory/RBSegmentsCacheInMemory.ts b/src/storages/inMemory/RBSegmentsCacheInMemory.ts new file mode 100644 index 00000000..568b0deb --- /dev/null +++ b/src/storages/inMemory/RBSegmentsCacheInMemory.ts @@ -0,0 +1,68 @@ +import { IRBSegment } from '../../dtos/types'; +import { setToArray } from '../../utils/lang/sets'; +import { usesSegments } from '../AbstractSplitsCacheSync'; +import { IRBSegmentsCacheSync } from '../types'; + +export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync { + + private cache: Record = {}; + private changeNumber: number = -1; + private segmentsCount: number = 0; + + clear() { + this.cache = {}; + this.changeNumber = -1; + this.segmentsCount = 0; + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + this.changeNumber = changeNumber; + const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); + return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + } + + private add(rbSegment: IRBSegment): boolean { + const name = rbSegment.name; + const previous = this.get(name); + if (previous && usesSegments(previous)) this.segmentsCount--; + + this.cache[name] = rbSegment; + if (usesSegments(rbSegment)) this.segmentsCount++; + + return true; + } + + private remove(name: string): boolean { + const rbSegment = this.get(name); + if (!rbSegment) return false; + + delete this.cache[name]; + + if (usesSegments(rbSegment)) this.segmentsCount--; + + return true; + } + + private getNames(): string[] { + return Object.keys(this.cache); + } + + get(name: string): IRBSegment | null { + return this.cache[name] || null; + } + + contains(names: Set): boolean { + const namesArray = setToArray(names); + const namesInStorage = this.getNames(); + return namesArray.every(name => namesInStorage.indexOf(name) !== -1); + } + + getChangeNumber(): number { + return this.changeNumber; + } + + usesSegments(): boolean { + return this.segmentsCount > 0; + } + +} diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index a8be688a..461d15e6 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -24,6 +24,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { this.ttCache = {}; this.changeNumber = -1; this.segmentsCount = 0; + this.flagSetsCache = {}; } addSplit(split: ISplit): boolean { diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 2f907eca..56ca1300 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -161,6 +161,9 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { cache.addSplit(featureFlagWithoutFS); expect(cache.getNamesByFlagSets([])).toEqual([]); + + cache.clear(); + expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([emptySet, emptySet, emptySet]); }); // if FlagSets are not defined, it should store all FlagSets in memory. diff --git a/src/storages/inRedis/RBSegmentsCacheInRedis.ts b/src/storages/inRedis/RBSegmentsCacheInRedis.ts new file mode 100644 index 00000000..dc36f64c --- /dev/null +++ b/src/storages/inRedis/RBSegmentsCacheInRedis.ts @@ -0,0 +1,79 @@ +import { isNaNNumber } from '../../utils/lang'; +import { IRBSegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { IRBSegment } from '../../dtos/types'; +import { LOG_PREFIX } from './constants'; +import { setToArray } from '../../utils/lang/sets'; +import { RedisAdapter } from './RedisAdapter'; +import { KeyBuilderSS } from '../KeyBuilderSS'; + +export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilderSS; + private readonly redis: RedisAdapter; + + constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter) { + this.log = log; + this.keys = keys; + this.redis = redis; + } + + get(name: string): Promise { + return this.redis.get(this.keys.buildRBSegmentKey(name)) + .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment)); + } + + private getNames(): Promise { + return this.redis.keys(this.keys.searchPatternForRBSegmentKeys()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + contains(names: Set): Promise { + const namesArray = setToArray(names); + return this.getNames().then(namesInStorage => { + return namesArray.every(name => namesInStorage.includes(name)); + }); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(toAdd => { + const key = this.keys.buildRBSegmentKey(toAdd.name); + const stringifiedNewRBSegment = JSON.stringify(toAdd); + return this.redis.set(key, stringifiedNewRBSegment).then(() => true); + })), + Promise.all(toRemove.map(toRemove => { + const key = this.keys.buildRBSegmentKey(toRemove.name); + return this.redis.del(key).then(status => status === 1); + })) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + + setChangeNumber(changeNumber: number) { + return this.redis.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '').then( + status => status === 'OK' + ); + } + + getChangeNumber(): Promise { + return this.redis.get(this.keys.buildRBSegmentsTillKey()).then((value: string | null) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO implement if required by DataLoader or producer mode + clear() { + return Promise.resolve(); + } + +} diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 054f91ce..e5d86d6e 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -11,6 +11,7 @@ import { TelemetryCacheInRedis } from './TelemetryCacheInRedis'; import { UniqueKeysCacheInRedis } from './UniqueKeysCacheInRedis'; import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis'; import { metadataBuilder } from '../utils'; +import { RBSegmentsCacheInRedis } from './RBSegmentsCacheInRedis'; export interface InRedisStorageOptions { prefix?: string @@ -59,6 +60,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy return { splits: new SplitsCacheInRedis(log, keys, redisClient, settings.sync.__splitFiltersValidation), + rbSegments: new RBSegmentsCacheInRedis(log, keys, redisClient), segments: new SegmentsCacheInRedis(log, keys, redisClient), impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata), impressionCounts: impressionCountsCache, diff --git a/src/storages/pluggable/RBSegmentsCachePluggable.ts b/src/storages/pluggable/RBSegmentsCachePluggable.ts new file mode 100644 index 00000000..c1967f6d --- /dev/null +++ b/src/storages/pluggable/RBSegmentsCachePluggable.ts @@ -0,0 +1,76 @@ +import { isNaNNumber } from '../../utils/lang'; +import { KeyBuilder } from '../KeyBuilder'; +import { IPluggableStorageWrapper, IRBSegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { IRBSegment } from '../../dtos/types'; +import { LOG_PREFIX } from './constants'; +import { setToArray } from '../../utils/lang/sets'; + +export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilder; + private readonly wrapper: IPluggableStorageWrapper; + + constructor(log: ILogger, keys: KeyBuilder, wrapper: IPluggableStorageWrapper) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + } + + get(name: string): Promise { + return this.wrapper.get(this.keys.buildRBSegmentKey(name)) + .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment)); + } + + private getNames(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildRBSegmentKeyPrefix()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + contains(names: Set): Promise { + const namesArray = setToArray(names); + return this.getNames().then(namesInStorage => { + return namesArray.every(name => namesInStorage.includes(name)); + }); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(toAdd => { + const key = this.keys.buildRBSegmentKey(toAdd.name); + const stringifiedNewRBSegment = JSON.stringify(toAdd); + return this.wrapper.set(key, stringifiedNewRBSegment).then(() => true); + })), + Promise.all(toRemove.map(toRemove => { + const key = this.keys.buildRBSegmentKey(toRemove.name); + return this.wrapper.del(key); + })) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + + setChangeNumber(changeNumber: number) { + return this.wrapper.set(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); + } + + getChangeNumber(): Promise { + return this.wrapper.get(this.keys.buildRBSegmentsTillKey()).then((value) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO implement if required by DataLoader or producer mode + clear() { + return Promise.resolve(); + } + +} diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index cc16bceb..a5dba66e 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -20,6 +20,7 @@ import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { metadataBuilder } from '../utils'; import { LOG_PREFIX } from '../pluggable/constants'; +import { RBSegmentsCachePluggable } from './RBSegmentsCachePluggable'; const NO_VALID_WRAPPER = 'Expecting pluggable storage `wrapper` in options, but no valid wrapper instance was provided.'; const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.'; @@ -117,6 +118,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn return { splits: new SplitsCachePluggable(log, keys, wrapper, settings.sync.__splitFiltersValidation), + rbSegments: new RBSegmentsCachePluggable(log, keys, wrapper), segments: new SegmentsCachePluggable(log, keys, wrapper), impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: impressionCountsCache, diff --git a/src/storages/types.ts b/src/storages/types.ts index 115aebcf..8e93daca 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,5 +1,5 @@ import SplitIO from '../../types/splitio'; -import { MaybeThenable, ISplit, IMySegmentsResponse } from '../dtos/types'; +import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse } from '../dtos/types'; import { MySegmentsData } from '../sync/polling/types'; import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types'; import { ISettings } from '../types'; @@ -221,6 +221,34 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { getNamesByFlagSets(flagSets: string[]): Promise[]> } +/** Rule-Based Segments cache */ + +export interface IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): MaybeThenable, + get(name: string): MaybeThenable, + getChangeNumber(): MaybeThenable, + clear(): MaybeThenable, + contains(names: Set): MaybeThenable, +} + +export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean, + get(name: string): IRBSegment | null, + getChangeNumber(): number, + clear(): void, + contains(names: Set): boolean, + // Used only for smart pausing in client-side standalone. Returns true if the storage contains a RBSegment using segments or large segments matchers + usesSegments(): boolean, +} + +export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise, + get(name: string): Promise, + getChangeNumber(): Promise, + clear(): Promise, + contains(names: Set): Promise, +} + /** Segments cache */ export interface ISegmentsCacheBase { @@ -419,6 +447,7 @@ export interface ITelemetryCacheAsync extends ITelemetryEvaluationProducerAsync, export interface IStorageBase< TSplitsCache extends ISplitsCacheBase = ISplitsCacheBase, + TRBSegmentsCache extends IRBSegmentsCacheBase = IRBSegmentsCacheBase, TSegmentsCache extends ISegmentsCacheBase = ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase = IImpressionsCacheBase, TImpressionsCountCache extends IImpressionCountsCacheBase = IImpressionCountsCacheBase, @@ -427,7 +456,9 @@ export interface IStorageBase< TUniqueKeysCache extends IUniqueKeysCacheBase = IUniqueKeysCacheBase > { splits: TSplitsCache, + rbSegments: TRBSegmentsCache, segments: TSegmentsCache, + largeSegments?: TSegmentsCache, impressions: TImpressionsCache, impressionCounts: TImpressionsCountCache, events: TEventsCache, @@ -439,6 +470,7 @@ export interface IStorageBase< export interface IStorageSync extends IStorageBase< ISplitsCacheSync, + IRBSegmentsCacheSync, ISegmentsCacheSync, IImpressionsCacheSync, IImpressionCountsCacheSync, @@ -453,6 +485,7 @@ export interface IStorageSync extends IStorageBase< export interface IStorageAsync extends IStorageBase< ISplitsCacheAsync, + IRBSegmentsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, IImpressionCountsCacheBase, diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 9d1b27eb..79fd971c 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,24 +1,85 @@ +import { ISettings } from '../../../types'; +import { ISplitChangesResponse } from '../../../dtos/types'; import { IFetchSplitChanges, IResponse } from '../../../services/types'; +import { IStorageBase } from '../../../storages/types'; +import { FLAG_SPEC_VERSION } from '../../../utils/constants'; +import { base } from '../../../utils/settingsValidation'; import { ISplitChangesFetcher } from './types'; +import { LOG_PREFIX_SYNC_SPLITS } from '../../../logger/constants'; + +const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side +const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side + +function sdkEndpointOverridden(settings: ISettings) { + return settings.urls.sdk !== base.urls.sdk; +} /** * Factory of SplitChanges fetcher. * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors. */ -export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges): ISplitChangesFetcher { +// @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation +export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { + + const log = settings.log; + const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; + let lastProxyCheckTimestamp: number | undefined; return function splitChangesFetcher( since: number, noCache?: boolean, till?: number, + rbSince?: number, // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise - ) { + ): Promise { + + // Recheck proxy + if (lastProxyCheckTimestamp && (Date.now() - lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { + settings.sync.flagSpecVersion = FLAG_SPEC_VERSION; + } + + let splitsPromise = fetchSplitChanges(since, noCache, till, settings.sync.flagSpecVersion === FLAG_SPEC_VERSION ? rbSince : undefined) + // Handle proxy error with spec 1.3 + .catch((err) => { + if (err.statusCode === 400 && sdkEndpointOverridden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { + log.error(LOG_PREFIX_SYNC_SPLITS + 'Proxy error detected. Retrying with spec 1.2. If you are using Split Proxy, please upgrade to latest version'); + lastProxyCheckTimestamp = Date.now(); + settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec + return fetchSplitChanges(since, noCache, till); // retry request without rbSince + } + throw err; + }); - let splitsPromise = fetchSplitChanges(since, noCache, till); if (decorator) splitsPromise = decorator(splitsPromise); - return splitsPromise.then(resp => resp.json()); + return splitsPromise + .then(resp => resp.json()) + .then(data => { + // Using flag spec version 1.2 or below + if (data.splits) { + return { + ff: { + d: data.splits, + s: data.since, + t: data.till + } + }; + } + + // Proxy recovery + if (lastProxyCheckTimestamp) { + log.info(LOG_PREFIX_SYNC_SPLITS + 'Proxy error recovered'); + lastProxyCheckTimestamp = undefined; + return splitChangesFetcher(-1, undefined, undefined, -1) + .then((splitChangesResponse: ISplitChangesResponse) => + Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) + .then(() => splitChangesResponse) + ); + } + + return data; + }); }; } diff --git a/src/sync/polling/fetchers/types.ts b/src/sync/polling/fetchers/types.ts index 72968a5f..8fe922ce 100644 --- a/src/sync/polling/fetchers/types.ts +++ b/src/sync/polling/fetchers/types.ts @@ -5,6 +5,7 @@ export type ISplitChangesFetcher = ( since: number, noCache?: boolean, till?: number, + rbSince?: number, decorator?: (promise: Promise) => Promise ) => Promise diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 4ce0882a..6a5ba679 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -43,10 +43,10 @@ export function pollingManagerCSFactory( // smart pausing readiness.splits.on(SDK_SPLITS_ARRIVED, () => { if (!splitsSyncTask.isRunning()) return; // noop if not doing polling - const splitsHaveSegments = storage.splits.usesSegments(); - if (splitsHaveSegments !== mySegmentsSyncTask.isRunning()) { - log.info(POLLING_SMART_PAUSING, [splitsHaveSegments ? 'ON' : 'OFF']); - if (splitsHaveSegments) { + const usingSegments = storage.splits.usesSegments() || storage.rbSegments.usesSegments(); + if (usingSegments !== mySegmentsSyncTask.isRunning()) { + log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']); + if (usingSegments) { startMySegmentsSyncTasks(); } else { stopMySegmentsSyncTasks(); @@ -59,9 +59,9 @@ export function pollingManagerCSFactory( // smart ready function smartReady() { - if (!readiness.isReady() && !storage.splits.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + if (!readiness.isReady() && !storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); } - if (!storage.splits.usesSegments()) setTimeout(smartReady, 0); + if (!storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) setTimeout(smartReady, 0); else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady); mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask; @@ -77,7 +77,7 @@ export function pollingManagerCSFactory( log.info(POLLING_START); splitsSyncTask.start(); - if (storage.splits.usesSegments()) startMySegmentsSyncTasks(); + if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) startMySegmentsSyncTasks(); }, // Stop periodic fetching (polling) diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index d6fed5a2..d385bf77 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -21,7 +21,7 @@ export function splitsSyncTaskFactory( settings.log, splitChangesUpdaterFactory( settings.log, - splitChangesFetcherFactory(fetchSplitChanges), + splitChangesFetcherFactory(fetchSplitChanges, settings, storage), storage, settings.sync.__splitFiltersValidation, readiness.splits, diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index c542fec9..4ff29c83 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -1,10 +1,10 @@ -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; import { IReadinessManager } from '../../readiness/types'; import { IStorageSync } from '../../storages/types'; import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../streaming/types'; import { ITask, ISyncTask } from '../types'; -export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }], boolean> { } +export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }], boolean> { } export interface ISegmentsSyncTask extends ISyncTask<[fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number], boolean> { } diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index d59c7013..b93a7176 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -1,10 +1,10 @@ -import { ISplit } from '../../../../dtos/types'; +import { IRBSegment, ISplit } from '../../../../dtos/types'; import { readinessManagerFactory } from '../../../../readiness/readinessManager'; import { splitApiFactory } from '../../../../services/splitApi'; import { SegmentsCacheInMemory } from '../../../../storages/inMemory/SegmentsCacheInMemory'; import { SplitsCacheInMemory } from '../../../../storages/inMemory/SplitsCacheInMemory'; import { splitChangesFetcherFactory } from '../../fetchers/splitChangesFetcher'; -import { splitChangesUpdaterFactory, parseSegments, computeSplitsMutation } from '../splitChangesUpdater'; +import { splitChangesUpdaterFactory, parseSegments, computeMutation } from '../splitChangesUpdater'; import splitChangesMock1 from '../../../../__tests__/mocks/splitchanges.since.-1.json'; import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { fullSettings, settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; @@ -12,6 +12,9 @@ import { EventEmitter } from '../../../../utils/MinEvents'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; import { splitNotifications } from '../../../streaming/__tests__/dataMocks'; +import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory'; +import { RB_SEGMENT_UPDATE, SPLIT_UPDATE } from '../../../streaming/constants'; +import { IN_RULE_BASED_SEGMENT } from '../../../../utils/constants'; const ARCHIVED_FF = 'ARCHIVED'; @@ -82,31 +85,51 @@ const testFFEmptySet: ISplit = conditions: [], sets: [] }; +// @ts-ignore +const rbsWithExcludedSegment: IRBSegment = { + name: 'rbs', + status: 'ACTIVE', + conditions: [], + excluded: { + segments: [{ + type: 'standard', + name: 'C' + }, { + type: 'rule-based', + name: 'D' + }] + } +}; test('splitChangesUpdater / segments parser', () => { + let segments = parseSegments(activeSplitWithSegments as ISplit); + expect(segments).toEqual(new Set(['A', 'B'])); - const segments = parseSegments(activeSplitWithSegments as ISplit); + segments = parseSegments(rbsWithExcludedSegment); + expect(segments).toEqual(new Set(['C'])); - expect(segments.has('A')).toBe(true); - expect(segments.has('B')).toBe(true); + segments = parseSegments(rbsWithExcludedSegment, IN_RULE_BASED_SEGMENT); + expect(segments).toEqual(new Set(['D'])); }); test('splitChangesUpdater / compute splits mutation', () => { const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); + let segments = new Set(); + let splitsMutation = computeMutation([activeSplitWithSegments, archivedSplit] as ISplit[], segments, splitFiltersValidation); expect(splitsMutation.added).toEqual([activeSplitWithSegments]); expect(splitsMutation.removed).toEqual([archivedSplit]); - expect(splitsMutation.segments).toEqual(['A', 'B']); + expect(Array.from(segments)).toEqual(['A', 'B']); // SDK initialization without sets // should process all the notifications - splitsMutation = computeSplitsMutation([testFFSetsAB, test2FFSetsX] as ISplit[], splitFiltersValidation); + segments = new Set(); + splitsMutation = computeMutation([testFFSetsAB, test2FFSetsX] as ISplit[], segments, splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFSetsAB, test2FFSetsX]); expect(splitsMutation.removed).toEqual([]); - expect(splitsMutation.segments).toEqual([]); + expect(Array.from(segments)).toEqual([]); }); test('splitChangesUpdater / compute splits mutation with filters', () => { @@ -114,57 +137,59 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a', 'set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; // fetching new feature flag in sets A & B - let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); + let splitsMutation = computeMutation([testFFSetsAB], new Set(), splitFiltersValidation); // should add it to mutations expect(splitsMutation.added).toEqual([testFFSetsAB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B - splitsMutation = computeSplitsMutation([testFFRemoveSetB], splitFiltersValidation); + splitsMutation = computeMutation([testFFRemoveSetB], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFRemoveSetB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B - splitsMutation = computeSplitsMutation([testFFRemoveSetA], splitFiltersValidation); + splitsMutation = computeMutation([testFFRemoveSetA], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFRemoveSetA]); // fetching existing test feature flag removed from set B - splitsMutation = computeSplitsMutation([testFFEmptySet], splitFiltersValidation); + splitsMutation = computeMutation([testFFEmptySet], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFEmptySet]); // SDK initialization with names: ['test2'] splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; - splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); + splitsMutation = computeMutation([testFFSetsAB], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFSetsAB]); - splitsMutation = computeSplitsMutation([test2FFSetsX, testFFEmptySet], splitFiltersValidation); + splitsMutation = computeMutation([test2FFSetsX, testFFEmptySet], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([test2FFSetsX]); expect(splitsMutation.removed).toEqual([testFFEmptySet]); }); describe('splitChangesUpdater', () => { - - fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore - const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); - const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); - const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); - const splits = new SplitsCacheInMemory(); const updateSplits = jest.spyOn(splits, 'update'); + const rbSegments = new RBSegmentsCacheInMemory(); + const updateRbSegments = jest.spyOn(rbSegments, 'update'); + const segments = new SegmentsCacheInMemory(); const registerSegments = jest.spyOn(segments, 'registerSegments'); - const storage = { splits, segments }; + const storage = { splits, rbSegments, segments }; + + fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore + const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); + const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); + const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges, fullSettings, storage); const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); @@ -179,22 +204,29 @@ describe('splitChangesUpdater', () => { test('test without payload', async () => { const result = await splitChangesUpdater(); + + expect(fetchSplitChanges).toBeCalledTimes(1); + expect(fetchSplitChanges).lastCalledWith(-1, undefined, undefined, -1); expect(updateSplits).toBeCalledTimes(1); - expect(updateSplits).lastCalledWith(splitChangesMock1.splits, [], splitChangesMock1.till); + expect(updateSplits).lastCalledWith(splitChangesMock1.ff.d, [], splitChangesMock1.ff.t); + expect(updateRbSegments).toBeCalledTimes(0); // no rbSegments to update expect(registerSegments).toBeCalledTimes(1); expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); expect(result).toBe(true); }); - test('test with payload', async () => { + test('test with ff payload', async () => { let index = 0; for (const notification of splitNotifications) { const payload = notification.decoded as Pick; const changeNumber = payload.changeNumber; - await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); - // fetch not being called + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: SPLIT_UPDATE })).resolves.toBe(true); + + // fetch and RBSegments.update not being called expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateRbSegments).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(index + 1); // Change number being updated expect(updateSplits.mock.calls[index][2]).toEqual(changeNumber); @@ -209,6 +241,23 @@ describe('splitChangesUpdater', () => { } }); + test('test with rbsegment payload', async () => { + const payload = { name: 'rbsegment', status: 'ACTIVE', changeNumber: 1684329854385, conditions: [] } as unknown as IRBSegment; + const changeNumber = payload.changeNumber; + + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: RB_SEGMENT_UPDATE })).resolves.toBe(true); + + // fetch and Splits.update not being called + expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(0); + + expect(updateRbSegments).toBeCalledTimes(1); + expect(updateRbSegments).toBeCalledWith([payload], [], changeNumber); + + expect(registerSegments).toBeCalledTimes(1); + expect(registerSegments).toBeCalledWith([]); + }); + test('flag sets splits-arrived emission', async () => { const payload = splitNotifications[3].decoded as Pick; const setMocks = [ @@ -226,7 +275,7 @@ describe('splitChangesUpdater', () => { let calls = 0; // emit always if not configured sets for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index, type: SPLIT_UPDATE })).resolves.toBe(true); expect(splitsEmitSpy.mock.calls[index][0]).toBe('state::splits-arrived'); index++; } @@ -238,7 +287,7 @@ describe('splitChangesUpdater', () => { splitsEmitSpy.mockReset(); index = 0; for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index, type: SPLIT_UPDATE })).resolves.toBe(true); if (setMock.shouldEmit) calls++; expect(splitsEmitSpy.mock.calls.length).toBe(calls); index++; diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 32d9f78e..501e3b7a 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -27,7 +27,7 @@ export function mySegmentsUpdaterFactory( matchingKey: string ): IMySegmentsUpdater { - const { splits, segments, largeSegments } = storage; + const { splits, rbSegments, segments, largeSegments } = storage; let readyOnAlreadyExistentState = true; let startingUp = true; @@ -51,7 +51,7 @@ export function mySegmentsUpdaterFactory( } // Notify update if required - if (splits.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { + if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { readyOnAlreadyExistentState = false; segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); } diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index c1009077..ab951b24 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -51,7 +51,7 @@ export function segmentChangesUpdaterFactory( * Returned promise will not be rejected. * * @param fetchOnlyNew - if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1. - * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. + * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE or RB_SEGMENT_UPDATE notifications. * @param segmentName - segment name to fetch. By passing `undefined` it fetches the list of segments registered at the storage * @param noCache - true to revalidate data to fetch on a SEGMENT_UPDATE notifications. * @param till - till target for the provided segmentName, for CDN bypass. diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 7a341cd0..ea5e5e44 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -1,16 +1,18 @@ import { ISegmentsCacheBase, IStorageBase } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; -import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types'; +import { IRBSegment, ISplit, ISplitChangesResponse, ISplitFiltersValidation, MaybeThenable } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; -import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; +import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; -import { IN_SEGMENT } from '../../../utils/constants'; +import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; +import { SPLIT_UPDATE } from '../../streaming/constants'; -type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise +export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; +type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment). // Returns a promise that could be rejected. @@ -24,27 +26,35 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { } /** - * Collect segments from a raw split definition. + * Collect segments from a raw FF or RBS definition. * Exported for testing purposes. */ -export function parseSegments({ conditions }: ISplit): Set { - let segments = new Set(); +export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set { + const { conditions = [], excluded } = ruleEntity as IRBSegment; + + const segments = new Set(); + if (excluded && excluded.segments) { + excluded.segments.forEach(({ type, name }) => { + if ((type === STANDARD_SEGMENT && matcherType === IN_SEGMENT) || (type === RULE_BASED_SEGMENT && matcherType === IN_RULE_BASED_SEGMENT)) { + segments.add(name); + } + }); + } for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; matchers.forEach(matcher => { - if (matcher.matcherType === IN_SEGMENT) segments.add(matcher.userDefinedSegmentMatcherData.segmentName); + if (matcher.matcherType === matcherType) segments.add(matcher.userDefinedSegmentMatcherData.segmentName); }); } return segments; } -interface ISplitMutations { - added: ISplit[], - removed: ISplit[], - segments: string[] +interface ISplitMutations { + added: T[], + removed: T[] } /** @@ -73,25 +83,21 @@ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { * i.e., an object with added splits, removed splits and used segments. * Exported for testing purposes. */ -export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersValidation): ISplitMutations { - const segments = new Set(); - const computed = entries.reduce((accum, split) => { - if (split.status === 'ACTIVE' && matchFilters(split, filters)) { - accum.added.push(split); +export function computeMutation(rules: Array, segments: Set, filters?: ISplitFiltersValidation): ISplitMutations { + + return rules.reduce((accum, ruleEntity) => { + if (ruleEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleEntity as ISplit, filters))) { + accum.added.push(ruleEntity); - parseSegments(split).forEach((segmentName: string) => { + parseSegments(ruleEntity).forEach((segmentName: string) => { segments.add(segmentName); }); } else { - accum.removed.push(split); + accum.removed.push(ruleEntity); } return accum; - }, { added: [], removed: [], segments: [] } as ISplitMutations); - - computed.segments = setToArray(segments); - - return computed; + }, { added: [], removed: [] } as ISplitMutations); } /** @@ -111,14 +117,14 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - storage: Pick, + storage: Pick, splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, isClientSide?: boolean -): ISplitChangesUpdater { - const { splits, segments } = storage; +): SplitChangesUpdater { + const { splits, rbSegments, segments } = storage; let startingUp = true; @@ -135,32 +141,53 @@ export function splitChangesUpdaterFactory( * @param noCache - true to revalidate data to fetch * @param till - query param to bypass CDN requests */ - return function splitChangesUpdater(noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) { + return function splitChangesUpdater(noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) { /** * @param since - current changeNumber at splitsCache * @param retry - current number of retry attempts */ - function _splitChangesUpdater(since: number, retry = 0): Promise { - log.debug(SYNC_SPLITS_FETCH, [since]); - return Promise.resolve(splitUpdateNotification ? - { splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } : - splitChangesFetcher(since, noCache, till, _promiseDecorator) + function _splitChangesUpdater(sinces: [number, number], retry = 0): Promise { + const [since, rbSince] = sinces; + log.debug(SYNC_SPLITS_FETCH, sinces); + return Promise.resolve( + instantUpdate ? + instantUpdate.type === SPLIT_UPDATE ? + // IFFU edge case: a change to a flag that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet + Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { + return contains ? + { ff: { d: [instantUpdate.payload as ISplit], t: instantUpdate.changeNumber } } : + splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator); + }) : + { rbs: { d: [instantUpdate.payload as IRBSegment], t: instantUpdate.changeNumber } } : + splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((splitChanges: ISplitChangesResponse) => { startingUp = false; - const mutation = computeSplitsMutation(splitChanges.splits, splitFiltersValidation); + const usedSegments = new Set(); - log.debug(SYNC_SPLITS_UPDATE, [mutation.added.length, mutation.removed.length, mutation.segments.length]); + let ffUpdate: MaybeThenable = false; + if (splitChanges.ff) { + const { added, removed } = computeMutation(splitChanges.ff.d, usedSegments, splitFiltersValidation); + log.debug(SYNC_SPLITS_UPDATE, [added.length, removed.length]); + ffUpdate = splits.update(added, removed, splitChanges.ff.t); + } + + let rbsUpdate: MaybeThenable = false; + if (splitChanges.rbs) { + const { added, removed } = computeMutation(splitChanges.rbs.d, usedSegments); + log.debug(SYNC_RBS_UPDATE, [added.length, removed.length]); + rbsUpdate = rbSegments.update(added, removed, splitChanges.rbs.t); + } - return Promise.all([ - splits.update(mutation.added, mutation.removed, splitChanges.till), - segments.registerSegments(mutation.segments) - ]).then(([isThereUpdate]) => { + return Promise.all([ffUpdate, rbsUpdate, + // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted + segments.registerSegments(setToArray(usedSegments)) + ]).then(([ffChanged, rbsChanged]) => { if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate && (isClientSide || checkAllSegmentsExist(segments)))) + return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments)))) .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events @@ -177,7 +204,7 @@ export function splitChangesUpdaterFactory( if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]); - return _splitChangesUpdater(since, retry); + return _splitChangesUpdater(sinces, retry); } else { startingUp = false; } @@ -185,7 +212,7 @@ export function splitChangesUpdaterFactory( }); } - let sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error - return sincePromise.then(_splitChangesUpdater); + // `getChangeNumber` never rejects or throws error + return Promise.all([splits.getChangeNumber(), rbSegments.getChangeNumber()]).then(_splitChangesUpdater); }; } diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index e85b22d8..90bdc8cd 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -1,10 +1,11 @@ // @ts-nocheck import { SSEHandlerFactory } from '..'; -import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, ControlType } from '../../constants'; +import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RB_SEGMENT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, ControlType } from '../../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; // update messages import splitUpdateMessage from '../../../../__tests__/mocks/message.SPLIT_UPDATE.1457552620999.json'; +import rbsegmentUpdateMessage from '../../../../__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json'; import splitKillMessage from '../../../../__tests__/mocks/message.SPLIT_KILL.1457552650000.json'; import segmentUpdateMessage from '../../../../__tests__/mocks/message.SEGMENT_UPDATE.1457552640000.json'; @@ -144,6 +145,10 @@ test('`handlerMessage` for update notifications (NotificationProcessor) and stre sseHandler.handleMessage(splitUpdateMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, ...expectedParams); // must emit SPLIT_UPDATE with the message change number + expectedParams = [{ type: 'RB_SEGMENT_UPDATE', changeNumber: 1457552620999 }]; + sseHandler.handleMessage(rbsegmentUpdateMessage); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(RB_SEGMENT_UPDATE, ...expectedParams); // must emit RB_SEGMENT_UPDATE with the message change number + expectedParams = [{ type: 'SPLIT_KILL', changeNumber: 1457552650000, splitName: 'whitelist', defaultTreatment: 'not_allowed' }]; sseHandler.handleMessage(splitKillMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_KILL, ...expectedParams); // must emit SPLIT_KILL with the message change number, split name and default treatment diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index fbbe329c..f7a39c8b 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,6 +1,6 @@ import { errorParser, messageParser } from './NotificationParser'; import { notificationKeeperFactory } from './NotificationKeeper'; -import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE } from '../constants'; +import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; @@ -84,6 +84,7 @@ export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, case MEMBERSHIPS_MS_UPDATE: case MEMBERSHIPS_LS_UPDATE: case SPLIT_KILL: + case RB_SEGMENT_UPDATE: pushEmitter.emit(parsedData.type, parsedData); break; diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index 192583c3..a39b8000 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -1,5 +1,5 @@ import { ControlType } from '../constants'; -import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../types'; +import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RB_SEGMENT_UPDATE } from '../types'; export enum Compression { None = 0, @@ -42,7 +42,7 @@ export interface ISegmentUpdateData { } export interface ISplitUpdateData { - type: SPLIT_UPDATE, + type: SPLIT_UPDATE | RB_SEGMENT_UPDATE, changeNumber: number, pcn?: number, d?: string, diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index 580fe9cb..dc5cb7dc 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -1,12 +1,16 @@ -import { ISplit } from '../../../dtos/types'; +import { IRBSegment, ISplit } from '../../../dtos/types'; +import { STREAMING_PARSING_SPLIT_UPDATE } from '../../../logger/constants'; import { ILogger } from '../../../logger/types'; import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ISplitsEventEmitter } from '../../../readiness/types'; -import { ISplitsCacheSync } from '../../../storages/types'; +import { IRBSegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; import { ITelemetryTracker } from '../../../trackers/types'; import { Backoff } from '../../../utils/Backoff'; import { SPLITS } from '../../../utils/constants'; import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types'; +import { InstantUpdate } from '../../polling/updaters/splitChangesUpdater'; +import { RB_SEGMENT_UPDATE } from '../constants'; +import { parseFFUpdatePayload } from '../parseUtils'; import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types'; import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT, FETCH_BACKOFF_MAX_RETRIES } from './constants'; import { IUpdateWorker } from './types'; @@ -14,102 +18,128 @@ import { IUpdateWorker } from './types'; /** * SplitsUpdateWorker factory */ -export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData, payload?: ISplit]> & { killSplit(event: ISplitKillData): void } { +export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killSplit(event: ISplitKillData): void } { - let maxChangeNumber = 0; - let handleNewEvent = false; - let isHandlingEvent: boolean; - let cdnBypass: boolean; - let payload: ISplit | undefined; - const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT); + const ff = SplitsUpdateWorker(storage.splits); + const rbs = SplitsUpdateWorker(storage.rbSegments); - function __handleSplitUpdateCall() { - isHandlingEvent = true; - if (maxChangeNumber > splitsCache.getChangeNumber()) { - handleNewEvent = false; - const splitUpdateNotification = payload ? { payload, changeNumber: maxChangeNumber } : undefined; - // fetch splits revalidating data if cached - splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, splitUpdateNotification).then(() => { - if (!isHandlingEvent) return; // halt if `stop` has been called - if (handleNewEvent) { - __handleSplitUpdateCall(); - } else { - if (splitUpdateNotification) telemetryTracker.trackUpdatesFromSSE(SPLITS); - // fetch new registered segments for server-side API. Not retrying on error - if (segmentsSyncTask) segmentsSyncTask.execute(true); + function SplitsUpdateWorker(cache: ISplitsCacheSync | IRBSegmentsCacheSync) { + let maxChangeNumber = -1; + let handleNewEvent = false; + let isHandlingEvent: boolean; + let cdnBypass: boolean; + let instantUpdate: InstantUpdate | undefined; + const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT); - const attempts = backoff.attempts + 1; + function __handleSplitUpdateCall() { + isHandlingEvent = true; + if (maxChangeNumber > cache.getChangeNumber()) { + handleNewEvent = false; + // fetch splits revalidating data if cached + splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, instantUpdate).then(() => { + if (!isHandlingEvent) return; // halt if `stop` has been called + if (handleNewEvent) { + __handleSplitUpdateCall(); + } else { + if (instantUpdate) telemetryTracker.trackUpdatesFromSSE(SPLITS); + // fetch new registered segments for server-side API. Not retrying on error + if (segmentsSyncTask) segmentsSyncTask.execute(true); - if (maxChangeNumber <= splitsCache.getChangeNumber()) { - log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); - isHandlingEvent = false; - return; - } + const attempts = backoff.attempts + 1; - if (attempts < FETCH_BACKOFF_MAX_RETRIES) { - backoff.scheduleCall(); - return; - } + if (ff.isSync() && rbs.isSync()) { + log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); + isHandlingEvent = false; + return; + } - if (cdnBypass) { - log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`); - isHandlingEvent = false; - } else { - backoff.reset(); - cdnBypass = true; - __handleSplitUpdateCall(); + if (attempts < FETCH_BACKOFF_MAX_RETRIES) { + backoff.scheduleCall(); + return; + } + + if (cdnBypass) { + log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`); + isHandlingEvent = false; + } else { + backoff.reset(); + cdnBypass = true; + __handleSplitUpdateCall(); + } } - } - }); - } else { - isHandlingEvent = false; + }); + } else { + isHandlingEvent = false; + } } - } - /** - * Invoked by NotificationProcessor on SPLIT_UPDATE event - * - * @param changeNumber - change number of the SPLIT_UPDATE notification - */ - function put({ changeNumber, pcn }: ISplitUpdateData, _payload?: ISplit) { - const currentChangeNumber = splitsCache.getChangeNumber(); + return { + /** + * Invoked by NotificationProcessor on SPLIT_UPDATE or RB_SEGMENT_UPDATE event + * + * @param changeNumber - change number of the notification + */ + put({ changeNumber, pcn, type }: ISplitUpdateData, payload?: ISplit | IRBSegment) { + const currentChangeNumber = cache.getChangeNumber(); - if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return; + if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return; - maxChangeNumber = changeNumber; - handleNewEvent = true; - cdnBypass = false; - payload = undefined; + maxChangeNumber = changeNumber; + handleNewEvent = true; + cdnBypass = false; + instantUpdate = undefined; - if (_payload && currentChangeNumber === pcn) { - payload = _payload; - } + if (payload && currentChangeNumber === pcn) { + instantUpdate = { payload, changeNumber, type }; + } - if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall(); - backoff.reset(); + if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall(); + backoff.reset(); + }, + stop() { + isHandlingEvent = false; + backoff.reset(); + }, + isSync() { + return maxChangeNumber <= cache.getChangeNumber(); + } + }; } return { - put, + put(parsedData) { + if (parsedData.d && parsedData.c !== undefined) { + try { + const payload = parseFFUpdatePayload(parsedData.c, parsedData.d); + if (payload) { + (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData, payload); + return; + } + } catch (e) { + log.warn(STREAMING_PARSING_SPLIT_UPDATE, [parsedData.type, e]); + } + } + (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData); + }, /** * Invoked by NotificationProcessor on SPLIT_KILL event * - * @param changeNumber - change number of the SPLIT_UPDATE notification + * @param changeNumber - change number of the notification * @param splitName - name of split to kill * @param defaultTreatment - default treatment value */ killSplit({ changeNumber, splitName, defaultTreatment }: ISplitKillData) { - if (splitsCache.killLocally(splitName, defaultTreatment, changeNumber)) { + if (storage.splits.killLocally(splitName, defaultTreatment, changeNumber)) { // trigger an SDK_UPDATE if Split was killed locally splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true); } // queues the SplitChanges fetch (only if changeNumber is newer) - put({ changeNumber } as ISplitUpdateData); + ff.put({ changeNumber } as ISplitUpdateData); }, stop() { - isHandlingEvent = false; - backoff.reset(); + ff.stop(); + rbs.stop(); } }; } diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index 4de69ca0..2bcffbd4 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { SDK_SPLITS_ARRIVED } from '../../../../readiness/constants'; import { SplitsCacheInMemory } from '../../../../storages/inMemory/SplitsCacheInMemory'; +import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory'; import { SplitsUpdateWorker } from '../SplitsUpdateWorker'; import { FETCH_BACKOFF_MAX_RETRIES } from '../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; @@ -53,19 +54,26 @@ const telemetryTracker = telemetryTrackerFactory(); // no-op telemetry tracker describe('SplitsUpdateWorker', () => { + const storage = { + splits: new SplitsCacheInMemory(), + rbSegments: new RBSegmentsCacheInMemory() + }; + afterEach(() => { // restore Backoff.__TEST__BASE_MILLIS = undefined; Backoff.__TEST__MAX_MILLIS = undefined; + + storage.splits.clear(); + storage.rbSegments.clear(); }); test('put', async () => { // setup - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache); + const splitsSyncTask = splitsSyncTaskMock(storage.splits); Backoff.__TEST__BASE_MILLIS = 1; // retry immediately - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); // assert calling `splitsSyncTask.execute` if `isExecuting` is false expect(splitsSyncTask.isExecuting()).toBe(false); @@ -102,9 +110,8 @@ describe('SplitsUpdateWorker', () => { test('put, backoff', async () => { // setup Backoff.__TEST__BASE_MILLIS = 50; - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, [90, 90, 90]); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, [90, 90, 90]); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); // while fetch fails, should retry with backoff splitUpdateWorker.put({ changeNumber: 100 }); @@ -121,9 +128,8 @@ describe('SplitsUpdateWorker', () => { // setup Backoff.__TEST__BASE_MILLIS = 10; // 10 millis instead of 10 sec Backoff.__TEST__MAX_MILLIS = 60; // 60 millis instead of 1 min - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, [...Array(FETCH_BACKOFF_MAX_RETRIES).fill(90), 90, 100]); // 12 executions. Last one is valid - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, [...Array(FETCH_BACKOFF_MAX_RETRIES).fill(90), 90, 100]); // 12 executions. Last one is valid + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); // queued @@ -146,9 +152,8 @@ describe('SplitsUpdateWorker', () => { // setup Backoff.__TEST__BASE_MILLIS = 10; // 10 millis instead of 10 sec Backoff.__TEST__MAX_MILLIS = 60; // 60 millis instead of 1 min - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, Array(FETCH_BACKOFF_MAX_RETRIES * 2).fill(90)); // 20 executions. No one is valid - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, Array(FETCH_BACKOFF_MAX_RETRIES * 2).fill(90)); // 20 executions. No one is valid + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); // queued @@ -168,18 +173,17 @@ describe('SplitsUpdateWorker', () => { test('killSplit', async () => { // setup - const cache = new SplitsCacheInMemory(); - cache.addSplit({ name: 'something'}); - cache.addSplit({ name: 'something else'}); + storage.splits.addSplit({ name: 'something' }); + storage.splits.addSplit({ name: 'something else' }); - const splitsSyncTask = splitsSyncTaskMock(cache); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); // assert killing split locally, emitting SDK_SPLITS_ARRIVED event, and synchronizing splits if changeNumber is new splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'something', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous expect(splitsSyncTask.execute).toBeCalledTimes(1); // synchronizes splits if `isExecuting` is false expect(splitsEventEmitterMock.emit.mock.calls).toEqual([[SDK_SPLITS_ARRIVED, true]]); // emits `SDK_SPLITS_ARRIVED` with `isSplitKill` flag in true, if split kill resolves with update - assertKilledSplit(cache, 100, 'something', 'off'); + assertKilledSplit(storage.splits, 100, 'something', 'off'); // assert not killing split locally, not emitting SDK_SPLITS_ARRIVED event, and not synchronizes splits, if changeNumber is old splitsSyncTask.__resolveSplitsUpdaterCall(100); @@ -192,15 +196,14 @@ describe('SplitsUpdateWorker', () => { expect(splitsSyncTask.execute).toBeCalledTimes(0); // doesn't synchronize splits if killLocally resolved without update expect(splitsEventEmitterMock.emit).toBeCalledTimes(0); // doesn't emit `SDK_SPLITS_ARRIVED` if killLocally resolved without update - assertKilledSplit(cache, 100, 'something', 'off'); // calling `killLocally` with an old changeNumber made no effect + assertKilledSplit(storage.splits, 100, 'something', 'off'); // calling `killLocally` with an old changeNumber made no effect }); test('stop', async () => { // setup - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, [95]); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, [95]); Backoff.__TEST__BASE_MILLIS = 1; - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); @@ -212,32 +215,29 @@ describe('SplitsUpdateWorker', () => { test('put, avoid fetching if payload sent', async () => { - const cache = new SplitsCacheInMemory(); splitNotifications.forEach(notification => { - const pcn = cache.getChangeNumber(); - const splitsSyncTask = splitsSyncTaskMock(cache); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const pcn = storage.splits.getChangeNumber(); + const splitsSyncTask = splitsSyncTaskMock(storage.splits); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); const payload = notification.decoded; const changeNumber = payload.changeNumber; - splitUpdateWorker.put({ changeNumber, pcn }, payload); // queued + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); // queued expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, { changeNumber, payload }]); }); }); test('put, ccn and pcn validation for IFF', () => { - const cache = new SplitsCacheInMemory(); - // ccn = 103 & pcn = 104: Something was missed -> fetch split changes let ccn = 103; let pcn = 104; let changeNumber = 105; - cache.setChangeNumber(ccn); + storage.splits.setChangeNumber(ccn); const notification = splitNotifications[0]; - let splitsSyncTask = splitsSyncTaskMock(cache); - let splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); - splitUpdateWorker.put({ changeNumber, pcn }, notification.decoded); + let splitsSyncTask = splitsSyncTaskMock(storage.splits); + let splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); splitsSyncTask.execute.mockClear(); @@ -246,11 +246,11 @@ describe('SplitsUpdateWorker', () => { ccn = 110; pcn = 0; changeNumber = 111; - cache.setChangeNumber(ccn); + storage.splits.setChangeNumber(ccn); - splitsSyncTask = splitsSyncTaskMock(cache); - splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); - splitUpdateWorker.put({ changeNumber, pcn }, notification.decoded); + splitsSyncTask = splitsSyncTaskMock(storage.splits); + splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); splitsSyncTask.execute.mockClear(); @@ -259,11 +259,11 @@ describe('SplitsUpdateWorker', () => { ccn = 120; pcn = 120; changeNumber = 121; - cache.setChangeNumber(ccn); + storage.splits.setChangeNumber(ccn); - splitsSyncTask = splitsSyncTaskMock(cache); - splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); - splitUpdateWorker.put({ changeNumber, pcn }, notification.decoded); + splitsSyncTask = splitsSyncTaskMock(storage.splits); + splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, { payload: notification.decoded, changeNumber }]); diff --git a/src/sync/streaming/__tests__/dataMocks.ts b/src/sync/streaming/__tests__/dataMocks.ts index 289843b5..cb7007d8 100644 --- a/src/sync/streaming/__tests__/dataMocks.ts +++ b/src/sync/streaming/__tests__/dataMocks.ts @@ -255,21 +255,21 @@ export const splitNotifications = [ { compression: 0, data: 'eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684329854385,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'off',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]} + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684329854385, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'off', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] } }, { compression: 1, // GZIP data: 'H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684333081259,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'v5',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]} + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684333081259, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'v5', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] } }, { compression: 2, // ZLIB data: 'eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684265694505,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'v5',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]}, + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684265694505, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'v5', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] }, }, { compression: 2, // ZLIB data: 'eJxsUdFu4jAQ/JVqnx3JDjTh/JZCrj2JBh0EqtOBIuNswKqTIMeuxKH8+ykhiKrqiyXvzM7O7lzAGlEUSqbnEyaiRODgGjRAQOXAIQ/puPB96tHHIPQYQ/QmFNErxEgG44DKnI2AQHXtTOI0my6WcXZAmxoUtsTKvil7nNZVoQ5RYdFERh7VBwK5TY60rqWwqq6AM0q/qa8Qc+As/EHZ5HHMCDR9wQ/9kIajcEygscK6BjhEy+nLr008AwLvSuuOVgjdIIEcC+H03RZw2Hg/n88JEJBHUR0wceUeDXAWTAIWPAYsZEFAQOhDDdwnIPslnOk9NcAvNwEOly3IWtdmC3wLe+1wCy0Q2Hh/zNvTV9xg3sFtr5irQe3v5f7twgAOy8V8vlinQKAUVh7RPJvanbrBsi73qurMQpTM7oSrzjueV6hR2tp05E8J39MV1hq1d7YrWWxsZ2cQGYjzeLXK0pcoyRbLLP69juZZuuiyxoPo2oa7ukqYc+JKNEq+XgVmwopucC6sGMSS9etTvAQCH0I7BO7Ttt21BE7C2E8XsN+l06h/CJy25CveH/eGM0rbHQEt9qiHnR62jtKR7N/8wafQ7tr/AQAA//8S4fPB', - decoded: {trafficTypeName:'user',id:'d704f220-0567-11ee-80ee-fa3c6460cd13',name:'NET_CORE_getTreatmentWithConfigAfterArchive',trafficAllocation:100,trafficAllocationSeed:179018541,seed:272707374,status:'ARCHIVED',killed:false,defaultTreatment:'V-FGyN',changeNumber:1686165617166,algo:2,configurations:{'V-FGyN':'{"color":"blue"}','V-YrWB':'{"color":"red"}'},conditions:[{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user',attribute:'test'},matcherType:'LESS_THAN_OR_EQUAL_TO',negate:false,unaryNumericMatcherData:{dataType:'NUMBER',value:20}}]},partitions:[{treatment:'V-FGyN',size:0},{treatment:'V-YrWB',size:100}],label:'test \u003c\u003d 20'}]} + decoded: { trafficTypeName: 'user', id: 'd704f220-0567-11ee-80ee-fa3c6460cd13', name: 'NET_CORE_getTreatmentWithConfigAfterArchive', trafficAllocation: 100, trafficAllocationSeed: 179018541, seed: 272707374, status: 'ARCHIVED', killed: false, defaultTreatment: 'V-FGyN', changeNumber: 1686165617166, algo: 2, configurations: { 'V-FGyN': '{"color":"blue"}', 'V-YrWB': '{"color":"red"}' }, conditions: [{ conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user', attribute: 'test' }, matcherType: 'LESS_THAN_OR_EQUAL_TO', negate: false, unaryNumericMatcherData: { dataType: 'NUMBER', value: 20 } }] }, partitions: [{ treatment: 'V-FGyN', size: 0 }, { treatment: 'V-YrWB', size: 100 }], label: 'test \u003c\u003d 20' }] } } ]; diff --git a/src/sync/streaming/constants.ts b/src/sync/streaming/constants.ts index ed958ee7..dd230a61 100644 --- a/src/sync/streaming/constants.ts +++ b/src/sync/streaming/constants.ts @@ -30,6 +30,7 @@ export const MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export const SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export const SPLIT_KILL = 'SPLIT_KILL'; export const SPLIT_UPDATE = 'SPLIT_UPDATE'; +export const RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export const CONTROL = 'CONTROL'; diff --git a/src/sync/streaming/parseUtils.ts b/src/sync/streaming/parseUtils.ts index 97fde935..a34f2dc9 100644 --- a/src/sync/streaming/parseUtils.ts +++ b/src/sync/streaming/parseUtils.ts @@ -2,7 +2,7 @@ import { algorithms } from '../../utils/decompress'; import { decodeFromBase64 } from '../../utils/base64'; import { hash } from '../../utils/murmur3/murmur3'; import { Compression, IMembershipMSUpdateData, KeyList } from './SSEHandler/types'; -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; const GZIP = 1; const ZLIB = 2; @@ -82,7 +82,7 @@ export function isInBitmap(bitmap: Uint8Array, hash64hex: string) { /** * Parse feature flags notifications for instant feature flag updates */ -export function parseFFUpdatePayload(compression: Compression, data: string): ISplit | undefined { +export function parseFFUpdatePayload(compression: Compression, data: string): ISplit | IRBSegment | undefined { return compression > 0 ? parseKeyList(data, compression, false) : JSON.parse(decodeFromBase64(data)); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index c87f4945..9122c176 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -11,10 +11,10 @@ import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; import { getMatching } from '../../utils/key'; -import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; -import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE, STREAMING_PARSING_SPLIT_UPDATE } from '../../logger/constants'; +import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RB_SEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; +import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE } from '../../logger/constants'; import { IMembershipMSUpdateData, IMembershipLSUpdateData, KeyList, UpdateStrategy } from './SSEHandler/types'; -import { getDelay, isInBitmap, parseBitmap, parseFFUpdatePayload, parseKeyList } from './parseUtils'; +import { getDelay, isInBitmap, parseBitmap, parseKeyList } from './parseUtils'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; import { IAuthTokenPushEnabled } from './AuthClient/types'; import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants'; @@ -56,7 +56,7 @@ export function pushManagerFactory( // MySegmentsUpdateWorker (client-side) are initiated in `add` method const segmentsUpdateWorker = userKey ? undefined : SegmentsUpdateWorker(log, pollingManager.segmentsSyncTask as ISegmentsSyncTask, storage.segments); // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments - const splitsUpdateWorker = SplitsUpdateWorker(log, storage.splits, pollingManager.splitsSyncTask, readiness.splits, telemetryTracker, userKey ? undefined : pollingManager.segmentsSyncTask as ISegmentsSyncTask); + const splitsUpdateWorker = SplitsUpdateWorker(log, storage, pollingManager.splitsSyncTask, readiness.splits, telemetryTracker, userKey ? undefined : pollingManager.segmentsSyncTask as ISegmentsSyncTask); // [Only for client-side] map of hashes to user keys, to dispatch membership update events to the corresponding MySegmentsUpdateWorker const userKeyHashes: Record = {}; @@ -219,20 +219,8 @@ export function pushManagerFactory( /** Functions related to synchronization (Queues and Workers in the spec) */ pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); - pushEmitter.on(SPLIT_UPDATE, (parsedData) => { - if (parsedData.d && parsedData.c !== undefined) { - try { - const payload = parseFFUpdatePayload(parsedData.c, parsedData.d); - if (payload) { - splitsUpdateWorker.put(parsedData, payload); - return; - } - } catch (e) { - log.warn(STREAMING_PARSING_SPLIT_UPDATE, [e]); - } - } - splitsUpdateWorker.put(parsedData); - }); + pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); + pushEmitter.on(RB_SEGMENT_UPDATE, splitsUpdateWorker.put); function handleMySegmentsUpdate(parsedData: IMembershipMSUpdateData | IMembershipLSUpdateData) { switch (parsedData.u) { diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index ec80781e..fcf5048e 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -16,18 +16,19 @@ export type MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export type SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export type SPLIT_KILL = 'SPLIT_KILL'; export type SPLIT_UPDATE = 'SPLIT_UPDATE'; +export type RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export type CONTROL = 'CONTROL'; export type OCCUPANCY = 'OCCUPANCY'; -export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | ControlType.STREAMING_RESET +export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | RB_SEGMENT_UPDATE | ControlType.STREAMING_RESET type IParsedData = T extends MEMBERSHIPS_MS_UPDATE ? IMembershipMSUpdateData : T extends MEMBERSHIPS_LS_UPDATE ? IMembershipLSUpdateData : T extends SEGMENT_UPDATE ? ISegmentUpdateData : - T extends SPLIT_UPDATE ? ISplitUpdateData : + T extends SPLIT_UPDATE | RB_SEGMENT_UPDATE ? ISplitUpdateData : T extends SPLIT_KILL ? ISplitKillData : INotificationData; /** diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index aed32493..21bf81e7 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -155,14 +155,14 @@ export function syncManagerOnlineFactory( if (pushManager) { if (pollingManager.isRunning()) { // if doing polling, we must start the periodic fetch of data - if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); + if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); } else { // if not polling, we must execute the sync task for the initial fetch // of segments since `syncAll` was already executed when starting the main client mySegmentsSyncTask.execute(); } } else { - if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); + if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); } } else { if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index cd11790f..6686c68e 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -104,8 +104,13 @@ export const DISABLED = 0; export const ENABLED = 1; export const PAUSED = 2; -export const FLAG_SPEC_VERSION = '1.2'; +export const FLAG_SPEC_VERSION = '1.3'; // Matcher types export const IN_SEGMENT = 'IN_SEGMENT'; export const IN_LARGE_SEGMENT = 'IN_LARGE_SEGMENT'; +export const IN_RULE_BASED_SEGMENT = 'IN_RULE_BASED_SEGMENT'; + +export const STANDARD_SEGMENT = 'standard'; +export const LARGE_SEGMENT = 'large'; +export const RULE_BASED_SEGMENT = 'rule-based'; diff --git a/src/utils/labels/index.ts b/src/utils/labels/index.ts index b5765299..957100d7 100644 --- a/src/utils/labels/index.ts +++ b/src/utils/labels/index.ts @@ -6,3 +6,4 @@ export const EXCEPTION = 'exception'; export const SPLIT_ARCHIVED = 'archived'; export const NOT_IN_SPLIT = 'not in split'; export const UNSUPPORTED_MATCHER_TYPE = 'targeting rule type unsupported by sdk'; +export const PREREQUISITES_NOT_MET = 'prerequisites not met'; diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index b1a7e35a..3435cbb1 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -111,7 +111,7 @@ export function groupBy>(source: T[], prop: string /** * Checks if a given value is a boolean. */ -export function isBoolean(val: any): boolean { +export function isBoolean(val: any): val is boolean { return val === true || val === false; } diff --git a/src/utils/promise/__tests__/wrapper.spec.ts b/src/utils/promise/__tests__/wrapper.spec.ts index ca0d418a..ab44f9d2 100644 --- a/src/utils/promise/__tests__/wrapper.spec.ts +++ b/src/utils/promise/__tests__/wrapper.spec.ts @@ -120,7 +120,7 @@ test('Promise utils / promise wrapper', function (done) { }); -test('Promise utils / promise wrapper: async/await', async function () { +test('Promise utils / promise wrapper: async/await', async () => { expect.assertions(8); // number of passHandler, passHandlerWithThrow and passHandlerFinally diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index a2a3fb14..f850f0bf 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -1,6 +1,7 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; import { ISettings } from '../../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { FLAG_SPEC_VERSION } from '../../constants'; export const settingsWithKey = { core: { @@ -67,7 +68,7 @@ export const fullSettings: ISettings = { groupedFilters: { bySet: [], byName: [], byPrefix: [] }, }, enabled: true, - flagSpecVersion: '1.2' + flagSpecVersion: FLAG_SPEC_VERSION }, version: 'jest', runtime: { diff --git a/types/splitio.d.ts b/types/splitio.d.ts index d8a1e67d..ad8644b2 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -879,7 +879,7 @@ declare namespace SplitIO { /** * The list of treatments available for the feature flag. */ - treatments: Array; + treatments: string[]; /** * Current change number of the feature flag. */ @@ -903,6 +903,10 @@ declare namespace SplitIO { * Whether the feature flag has impressions tracking disabled or not. */ impressionsDisabled: boolean; + /** + * Prerequisites for the feature flag. + */ + prerequisites: Array<{ flagName: string, treatments: string[] }>; }; /** * A promise that resolves to a feature flag view or null if the feature flag is not found.