├── .github ├── CODEOWNERS ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── update-license-year.yml ├── .gitignore ├── CHANGES.txt ├── CONTRIBUTORS-GUIDE.md ├── LICENSE.txt ├── README.md ├── doc.go ├── go.mod ├── go.sum ├── sonar-project.properties ├── splitio ├── client │ ├── bucketingkey.go │ ├── client.go │ ├── client_test.go │ ├── factory.go │ ├── factory_test.go │ ├── factory_tracker.go │ ├── factory_tracker_test.go │ ├── input_validator.go │ ├── input_validator_test.go │ ├── manager.go │ └── manager_test.go ├── conf │ ├── defaults.go │ ├── sdkconf.go │ ├── sdkconf_test.go │ └── util.go ├── impressionListener │ ├── impression_listener.go │ └── impressions_listener_wrapper.go ├── impressions │ ├── builder.go │ └── builder_test.go └── version.go └── testdata ├── murmur3-sample-data-non-alpha-numeric-v2.csv ├── murmur3-sample-data-v2.csv ├── sample-data-non-alpha-numeric.jsonl ├── sample-data.jsonl ├── segment_mock.json ├── segments └── segment_1.json ├── split_mock.json ├── splits.json ├── splits.yaml ├── splits_mock.json ├── splits_mock_2.json └── splits_mock_3.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @splitio/sdk 2 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | reviewers: 10 | - "splitio/sdk" 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # GO SDK 2 | 3 | ## What did you accomplish? 4 | 5 | ## How do we test the changes introduced in this PR? 6 | 7 | ## Extra Notes 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches-ignore: 5 | - none 6 | pull_request: 7 | branches: 8 | - development 9 | - main 10 | 11 | jobs: 12 | test: 13 | name: Run Tests 14 | runs-on: ubuntu-latest 15 | services: 16 | redis: 17 | image: redis 18 | ports: 19 | - 6379:6379 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Set up Go version 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: '1.18.0' 30 | 31 | - name: Go mod 32 | run: go mod tidy 33 | 34 | - name: Execute tests 35 | run: go test -coverprofile=coverage.out -count=1 -race ./... 36 | 37 | - name: Set VERSION env 38 | run: echo "VERSION=$(cat splitio/version.go | grep 'Version =' | awk '{print $4}' | tr -d '"')" >> $GITHUB_ENV 39 | 40 | - name: SonarQube Scan (Push) 41 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/development') 42 | uses: SonarSource/sonarcloud-github-action@v1.9 43 | env: 44 | SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | projectBaseDir: . 48 | args: > 49 | -Dsonar.host.url=${{ secrets.SONARQUBE_HOST }} 50 | -Dsonar.projectVersion=${{ env.VERSION }} 51 | 52 | - name: SonarQube Scan (Pull Request) 53 | if: github.event_name == 'pull_request' 54 | uses: SonarSource/sonarcloud-github-action@v1.9 55 | env: 56 | SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | projectBaseDir: . 60 | args: > 61 | -Dsonar.host.url=${{ secrets.SONARQUBE_HOST }} 62 | -Dsonar.projectVersion=${{ env.VERSION }} 63 | -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} 64 | -Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} 65 | -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} 66 | -------------------------------------------------------------------------------- /.github/workflows/update-license-year.yml: -------------------------------------------------------------------------------- 1 | name: Update License Year 2 | 3 | on: 4 | schedule: 5 | - cron: "0 3 1 1 *" # 03:00 AM on January 1 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set Current year 21 | run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV" 22 | 23 | - name: Set Previous Year 24 | run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV" 25 | 26 | - name: Update LICENSE 27 | uses: jacobtomlinson/gha-find-replace@v3 28 | with: 29 | find: ${{ env.PREVIOUS }} 30 | replace: ${{ env.CURRENT }} 31 | include: "LICENSE.txt" 32 | regex: false 33 | 34 | - name: Commit files 35 | run: | 36 | git config user.name 'github-actions[bot]' 37 | git config user.email 'github-actions[bot]@users.noreply.github.com' 38 | git commit -m "Updated License Year" -a 39 | 40 | - name: Create Pull Request 41 | uses: peter-evans/create-pull-request@v6 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | title: Update License Year 45 | branch: update-license 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 3 | *.o 4 | *.a 5 | *.so 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | *.swp 28 | 29 | .DS_Store 30 | 31 | Gopkg.lock 32 | 33 | .vscode/* 34 | 35 | coverage.out 36 | .scannerwork 37 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 6.7.0 (Jan 17, 2025) 2 | - Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on SplitView type objects. Read more in our docs. 3 | 4 | 6.6.0 (May 14, 2024) 5 | - Updated go-split-commons to v6 6 | - Added support for targeting rules based on semantic versions (https://semver.org/). 7 | - Added the logic to handle correctly when the SDK receives an unsupported Matcher type. 8 | 9 | 6.5.2 (Dec 21, 2023) 10 | - Updated telemetry name methods for flagSets. 11 | 12 | 6.5.1 (Dec 6, 2023) 13 | - Fixed FlagSetsFilter naming in Advanced config. 14 | 15 | 6.5.0 (Nov 29, 2023) 16 | - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): 17 | - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. 18 | - TreatmentsByFlagSet and TreatmentsByFlagSets 19 | - TreatmentsWithConfigByFlagSet and TreatmentsWithConfigByFlagSets 20 | - Added a new optional Flag Sets Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. 21 | - Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init. 22 | - Updated the following SDK manager methods to expose flag sets on flag views. 23 | - Added `DefaultTreatment` property to the `SplitView` object returned by the `Split` and `Splits` functions of the SDK manager. 24 | 25 | 6.4.0 (Jul 18, 2023) 26 | - Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system. 27 | - Pointed to new version of go-split-commons v5.0.0. 28 | 29 | 6.3.3 (Jun 9, 2023) 30 | - Fixed buffering logic in impressions for consumer mode 31 | - Pointed to new version of go-split-commons v4.3.5: 32 | Fixed nil message in error log when trying to connect to Redis 33 | Storing the proper impressions mode for telemetry in consumer mode 34 | 35 | 6.3.2 (May 15, 2023) 36 | - Updated terminology on the SDKs codebase to be more aligned with current standard without causing a breaking change. The core change is the term split for feature flag on things like logs and godoc comments. 37 | - Pointed to new version of go-split-commons v4.3.3: 38 | - Updated default treatment to be control for json localhost. 39 | 40 | 6.3.1 (March 21, 2023) 41 | - Fixed Unique Keys dto in redis. 42 | 43 | 6.3.0 (March 13, 2023) 44 | - Added support to use JSON files in localhost mode. 45 | - Pointed to new version of go-split-commons v4.3.0 for vulnerability fixes 46 | 47 | 6.2.1 (Oct 28, 2022) 48 | - Updated Matchers logging: demoted the log message to "warning". 49 | 50 | 6.2.0 (Oct 12, 2022) 51 | - Added a new impressions mode for the SDK called NONE, to be used in factory when there is no desire to capture impressions on an SDK factory to feed Split's analytics engine. Running NONE mode, the SDK will only capture unique keys evaluated for a particular feature flag instead of full blown impressions. 52 | 53 | 6.1.8 (Sep 9, 2022) 54 | - Updated BooleanMatcher logging: demoted the log message to "warning". 55 | 56 | 6.1.7 (Sep 8, 2022) 57 | - Replaced murmur3 32 library. 58 | 59 | 6.1.6 (June 21, 2022) 60 | - Updated the synchronization flow to be more reliable in the event of an edge case generating delay in cache purge propagation, keeping the SDK cache properly synced. 61 | 62 | 6.1.5 (Jun 6, 2022) 63 | - Updated segments and feature flag storage to return -1 as default changeNumber instead of zero 64 | 65 | 6.1.4 (May 12, 2022) 66 | - Updated config telemetry redis storage 67 | - Made the synchronization for telemetry cfg as goroutine to not degrade SDK readiness. 68 | 69 | 6.1.3 (Apr 18, 2022) 70 | - Fixed panic on initialization when redis is down. 71 | 72 | 6.1.2 (April 8, 2022) 73 | - Fixed SDK library version. 74 | 75 | 6.1.1 (Jan 12, 2022) 76 | - Fixed 100% cpu issue in localhost mode. 77 | - Bumped toolkit & commons dependencies. 78 | 79 | 6.1.0 (Apr 14, 2021) 80 | - Updated SDK telemetry storage, metrics and updater to be more effective and send less often. 81 | 82 | 6.0.2 (Feb 25, 2021) 83 | - Fixed SSE race conditions on token refresh & general push revamp (implemented in commons and toolkit). 84 | 85 | 6.0.1 (Dec 22, 2020) 86 | - Point to new versions of commons & toolkit which remove unnecessary log message. 87 | 88 | 6.0.0 (Oct 6, 2020) 89 | - BREAKING CHANGE: Migrated to go modules (dep & bare-bones go-dep no longer supported). 90 | 91 | 5.3.0 (Oct 6, 2020) 92 | - Added impressions dedupe logic to avoid sending duplicated impressions: 93 | - Added `OPTIMIZED` and `DEBUG` modes in order to enabling/disabling how impressions are going to be sent into Split servers, 94 | - `OPTIMIZED`: will send unique impressions in a timeframe in order to reduce how many times impressions are posted to Split. 95 | - `DEBUG`: will send every impression generated to Split. 96 | 97 | 5.2.2 (Sep 10, 2020) 98 | - Fixed possible issue with SSE client using 100% cpu. 99 | 100 | 5.2.1 (Aug 31, 2020) 101 | - Added impression observer. 102 | - Added rates validation. 103 | - Updated with latest version in go-split-commons. 104 | 105 | 5.2.0 (Aug 3, 2020) 106 | - Added split-commons library. 107 | - Added Streaming support. 108 | 109 | 5.1.3 (Jan 27, 2020) 110 | - Removed unnecessary Feature flag copy made in memory. 111 | 112 | 5.1.2 (Nov 28, 2019) 113 | - Several fixes in tests as a result of a locking & race conditions audit/ 114 | - Fixed locking issue for .Treatments() && .TreatmentsWithConfig() methods. 115 | 116 | 5.1.1 (Oct 15, 2019) 117 | - Added logic to fetch multiple feature flags at once on getTreatments/getTreatmentsWithConfig. 118 | - Added flag `IPAddressesEnabled` into Config to enable/disable sending MachineName and MachineIP when data is posted in headers. 119 | - Fixed resource leak in `api.ValidateApikey`. 120 | 121 | 5.1.0 (Jul 19, 2019) 122 | - Added support for TLS connections to redis. 123 | - Refactored initialization process. 124 | - Fixed traffic type count issue. 125 | - Fixed possible concurrency issue with feature flag storage. 126 | 127 | 5.0.1 (Jun 19, 2019) 128 | - Added coverage for traffic type validation existence only on ready and non localhost mode. 129 | 130 | 5.0.0 (Jun 4, 2019) 131 | - Added support for optional event properties via our Track() method. 132 | - Added validation for traffic types in track call. 133 | - Added new label when the sdk is not ready. 134 | - Added multiple factory instantiation check. 135 | - Added validation when feature flag does not exist in treatments and manager calls. 136 | - Moved Impressions in-memory to single-queue approach and refactored ImpressionDTO. 137 | 138 | 4.0.1 (May 17, 2019) 139 | - Fixed bug on client.Destroy() method. 140 | 141 | 4.0.0 (April 30, 2019) 142 | - Added custom Impression Listener. 143 | - BlockUntilReady refactor. 144 | - Added getTreatmentWithConfig and getTreatmentsWithConfig methods. 145 | - Added support for YAML file in Localhost mode. 146 | 147 | 3.0.1 (March 8, 2019) 148 | - Updated Feature flags refreshing rate. 149 | 150 | 3.0.0 (Feb 19, 2019) 151 | - Updated SDK Parity. 152 | - BREAKING CHANGE: Moved Impressions to Single Queue. 153 | 154 | 2.1.1 (Dec 19, 2018) 155 | - Fixed traffic allocation issue on 1%. 156 | 157 | 2.1.0: (Oct 12, 2018) 158 | - Added Input Sanitization 159 | 160 | 2.0.0: (May 24, 2018) 161 | - Fixed bucketing key 162 | 163 | 1.1.1: (Apr 20, 2018) 164 | - Fixing http headers output 165 | 166 | 1.1.0: (Feb 9, 2018) 167 | - Split client supports .track method (events) in all falvours (inmemory-standalone, redis-standalone, redis-consumer) 168 | 169 | 1.0.0: (Dec 22, 2017) 170 | - Downgrade logging level for shared memory messages 171 | -------------------------------------------------------------------------------- /CONTRIBUTORS-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Split GO SDK 2 | 3 | Split SDK is an open source project and we welcome feedback and contribution. The information below describes how to build the project with your changes, run the tests, and send the Pull Request(PR). 4 | 5 | ## Development 6 | 7 | ### Development process 8 | 9 | 1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch. 10 | 2. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like "fix bug". 11 | 3. Make sure to add tests for both positive and negative cases. 12 | 4. Run the linter script of the project and fix any issues you find. 13 | 5. Run the build script and make sure it runs with no errors. 14 | 6. Run all tests and make sure there are no failures. 15 | 7. `git push` your changes to GitHub within your topic branch. 16 | 8. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository. 17 | 9. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project. 18 | 10. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`. 19 | 11. Keep an eye out for any feedback or comments from Split's SDK team. 20 | 21 | ### Running tests 22 | 23 | To run test you can execute the command `go test ./...` on the root folder. 24 | 25 | # Contact 26 | 27 | If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io. 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2025 Split Software, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Split GO SDK 2 | 3 | [![build workflow](https://github.com/splitio/go-client/actions/workflows/ci.yml/badge.svg)](https://github.com/splitio/go-client/actions) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/splitio/go-client/v6)](https://pkg.go.dev/github.com/splitio/go-client/v6/splitio?tab=doc) 5 | [![Documentation](https://img.shields.io/badge/go_client-documentation-informational)](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) 6 | 7 | ## Overview 8 | 9 | This SDK is designed to work with Split, the platform for controlled rollouts, which serves features to your users via feature flags to manage your complete customer experience. 10 | 11 | [![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware) 12 | 13 | ## Compatibility 14 | 15 | This SDK is compatible with Go 1.18. 16 | 17 | ## Getting started 18 | 19 | Below is a simple example that describes the instantiation and most basic usage of our SDK: 20 | 21 | Run `go get github.com/splitio/go-client/` 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "github.com/splitio/go-client/v6/splitio/client" 28 | "github.com/splitio/go-client/v6/splitio/conf" 29 | ) 30 | 31 | func main() { 32 | cfg := conf.Default() 33 | factory, err := client.NewSplitFactory("YOUR_SDK_KEY", cfg) 34 | if err != nil { 35 | // SDK initialization error 36 | } 37 | 38 | client := factory.Client() 39 | 40 | err = client.BlockUntilReady(10) 41 | if err != nil { 42 | // SDK timeout error 43 | } 44 | 45 | treatment := client.Treatment("CUSTOMER_ID", "FEATURE_FLAG_NAME", nil) 46 | if treatment == "on" { 47 | // insert code here to show on treatment 48 | } else if treatment == "off" { 49 | // insert code here to show off treatment 50 | } else { 51 | // insert your control treatment code here 52 | } 53 | } 54 | ``` 55 | 56 | Please refer to [our official docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK)) to learn about all the functionality provided by our SDK and the configuration options available for tailoring it to your current application setup. 57 | 58 | ## Submitting issues 59 | 60 | The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/go-client/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner. 61 | 62 | ## Contributing 63 | 64 | Please see [Contributors Guide](CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR). 65 | 66 | ## License 67 | 68 | Licensed under the Apache License, Version 2.0. See: [Apache License](http://www.apache.org/licenses/). 69 | 70 | ## About Split 71 | 72 | Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy feature flags as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new feature flags will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery. 73 | 74 | To learn more about Split, contact hello@split.io, or get started with feature flags for free at [Split](https://www.split.io/signup). 75 | 76 | Split has built and maintains SDKs for: 77 | 78 | * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) 79 | * JavaScript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) 80 | * JavaScript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) 81 | * Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) 82 | * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) 83 | * Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) 84 | * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) 85 | * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) 86 | * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) 87 | * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) 88 | * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) 89 | * Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) 90 | * React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) 91 | * React Native [Github](https://github.com/splitio/react-native-client) [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) 92 | * Redux [Github](https://github.com/splitio/redux-client) [Docs](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK) 93 | 94 | For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20). 95 | 96 | **Learn more about Split:** 97 | 98 | Visit [split.io/product](https://www.split.io/product) for an overview of Split, or visit our documentation at [help.split.io](http://help.split.io) for more detailed information. 99 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | package splitclient 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/splitio/go-client/v6 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/splitio/go-split-commons/v6 v6.1.0 7 | github.com/splitio/go-toolkit/v5 v5.4.0 8 | ) 9 | 10 | require ( 11 | github.com/bits-and-blooms/bitset v1.3.1 // indirect 12 | github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect 13 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/redis/go-redis/v9 v9.0.4 // indirect 18 | github.com/stretchr/objx v0.5.2 // indirect 19 | github.com/stretchr/testify v1.9.0 // indirect 20 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 21 | golang.org/x/sync v0.3.0 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bits-and-blooms/bitset v1.3.1 h1:y+qrlmq3XsWi+xZqSaueaE8ry8Y127iMxlMfqcK8p0g= 2 | github.com/bits-and-blooms/bitset v1.3.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= 3 | github.com/bits-and-blooms/bloom/v3 v3.3.1 h1:K2+A19bXT8gJR5mU7y+1yW6hsKfNCjcP2uNfLFKncjQ= 4 | github.com/bits-and-blooms/bloom/v3 v3.3.1/go.mod h1:bhUUknWd5khVbTe4UgMCSiOOVJzr3tMoijSK3WwvW90= 5 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 6 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 7 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 8 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 12 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc= 16 | github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= 17 | github.com/splitio/go-split-commons/v6 v6.1.0 h1:k3mwr12DF6gbEaV8XXU/tSAQlPkIEuzIgTEneYhGg2I= 18 | github.com/splitio/go-split-commons/v6 v6.1.0/go.mod h1:D/XIY/9Hmfk9ivWsRsJVp439kEdmHbzUi3PKzQQDOXY= 19 | github.com/splitio/go-toolkit/v5 v5.4.0 h1:g5WFpRhQomnXCmvfsNOWV4s5AuUrWIZ+amM68G8NBKM= 20 | github.com/splitio/go-toolkit/v5 v5.4.0/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko= 21 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 22 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 23 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 24 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 25 | github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= 26 | github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 27 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 28 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 29 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 30 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=go-client 2 | sonar.sources=. 3 | sonar.exclusions=testdata/* 4 | sonar.tests=. 5 | sonar.test.inclusions=**/*_test.go 6 | sonar.go.coverage.reportPaths=coverage.out 7 | sonar.coverage.exclusions=**/mocks/* 8 | sonar.links.ci=https://github.com/splitio/go-client 9 | sonar.links.scm=https://github.com/splitio/go-client/actions 10 | -------------------------------------------------------------------------------- /splitio/client/bucketingkey.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | // Key struct to be used when supplying two keys. One for matching purposes and another one 4 | // for hashing. 5 | type Key struct { 6 | MatchingKey string 7 | BucketingKey string 8 | } 9 | 10 | // NewKey instantiates a new key 11 | func NewKey(matchingKey string, bucketingKey string) *Key { 12 | return &Key{ 13 | MatchingKey: matchingKey, 14 | BucketingKey: bucketingKey, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /splitio/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime/debug" 7 | "strings" 8 | "time" 9 | 10 | "github.com/splitio/go-client/v6/splitio/conf" 11 | impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener" 12 | 13 | "github.com/splitio/go-split-commons/v6/dtos" 14 | "github.com/splitio/go-split-commons/v6/engine/evaluator" 15 | "github.com/splitio/go-split-commons/v6/engine/evaluator/impressionlabels" 16 | "github.com/splitio/go-split-commons/v6/flagsets" 17 | "github.com/splitio/go-split-commons/v6/provisional" 18 | "github.com/splitio/go-split-commons/v6/storage" 19 | "github.com/splitio/go-split-commons/v6/telemetry" 20 | "github.com/splitio/go-toolkit/v5/logging" 21 | ) 22 | 23 | const ( 24 | treatment = "Treatment" 25 | treatments = "Treatments" 26 | treatmentsByFlagSet = "TreatmentsByFlagSet" 27 | treatmentsByFlagSets = "TreatmentsByFlagSets" 28 | treatmentWithConfig = "TreatmentWithConfig" 29 | treatmentsWithConfig = "TreatmentsWithConfig" 30 | treatmentsWithConfigByFlagSet = "TreatmentsWithConfigByFlagSet" 31 | treatmentsWithConfigByFlagSets = "TreatmentsWithConfigByFlagSets" 32 | ) 33 | 34 | // SplitClient is the entry-point of the split SDK. 35 | type SplitClient struct { 36 | logger logging.LoggerInterface 37 | evaluator evaluator.Interface 38 | impressions storage.ImpressionStorageProducer 39 | events storage.EventStorageProducer 40 | validator inputValidation 41 | factory *SplitFactory 42 | impressionListener *impressionlistener.WrapperImpressionListener 43 | impressionManager provisional.ImpressionManager 44 | initTelemetry storage.TelemetryConfigProducer 45 | evaluationTelemetry storage.TelemetryEvaluationProducer 46 | runtimeTelemetry storage.TelemetryRuntimeProducer 47 | flagSetsFilter flagsets.FlagSetFilter 48 | } 49 | 50 | // TreatmentResult struct that includes the Treatment evaluation with the corresponding Config 51 | type TreatmentResult struct { 52 | Treatment string `json:"treatment"` 53 | Config *string `json:"config"` 54 | } 55 | 56 | // getEvaluationResult calls evaluation for one particular feature flag 57 | func (c *SplitClient) getEvaluationResult(matchingKey string, bucketingKey *string, featureFlag string, attributes map[string]interface{}, operation string) *evaluator.Result { 58 | if c.isReady() { 59 | return c.evaluator.EvaluateFeature(matchingKey, bucketingKey, featureFlag, attributes) 60 | } 61 | 62 | c.logger.Warning(fmt.Sprintf("%s: the SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", operation, featureFlag)) 63 | c.initTelemetry.RecordNonReadyUsage() 64 | return &evaluator.Result{ 65 | Treatment: evaluator.Control, 66 | Label: impressionlabels.ClientNotReady, 67 | Config: nil, 68 | ImpressionsDisabled: false, 69 | } 70 | } 71 | 72 | // getEvaluationsResult calls evaluation for multiple treatments at once 73 | func (c *SplitClient) getEvaluationsResult(matchingKey string, bucketingKey *string, featureFlags []string, attributes map[string]interface{}, operation string) evaluator.Results { 74 | if c.isReady() { 75 | return c.evaluator.EvaluateFeatures(matchingKey, bucketingKey, featureFlags, attributes) 76 | } 77 | featureFlagsToPrint := strings.Join(featureFlags, ", ") 78 | c.logger.Warning(fmt.Sprintf("%s: the SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", operation, featureFlagsToPrint)) 79 | c.initTelemetry.RecordNonReadyUsage() 80 | result := evaluator.Results{ 81 | EvaluationTime: 0, 82 | Evaluations: make(map[string]evaluator.Result), 83 | } 84 | for _, featureFlag := range featureFlags { 85 | result.Evaluations[featureFlag] = evaluator.Result{ 86 | Treatment: evaluator.Control, 87 | Label: impressionlabels.ClientNotReady, 88 | Config: nil, 89 | ImpressionsDisabled: false, 90 | } 91 | } 92 | return result 93 | } 94 | 95 | // createImpression creates impression to be stored and used by listener 96 | func (c *SplitClient) createImpression(featureFlag string, bucketingKey *string, evaluationLabel string, matchingKey string, treatment string, changeNumber int64, disabled bool) dtos.Impression { 97 | var label string 98 | if c.factory.cfg.LabelsEnabled { 99 | label = evaluationLabel 100 | } 101 | 102 | impressionBucketingKey := "" 103 | if bucketingKey != nil { 104 | impressionBucketingKey = *bucketingKey 105 | } 106 | 107 | return dtos.Impression{ 108 | FeatureName: featureFlag, 109 | BucketingKey: impressionBucketingKey, 110 | ChangeNumber: changeNumber, 111 | KeyName: matchingKey, 112 | Label: label, 113 | Treatment: treatment, 114 | Time: time.Now().UTC().UnixNano() / int64(time.Millisecond), // Convert standard timestamp to java's ms timestamps 115 | Disabled: disabled, 116 | } 117 | } 118 | 119 | // storeData stores impression, runs listener and stores metrics 120 | func (c *SplitClient) storeData(impressions []dtos.Impression, attributes map[string]interface{}, metricsLabel string, evaluationTime time.Duration) { 121 | // Store impression 122 | if c.impressions != nil { 123 | listenerEnabled := c.impressionListener != nil 124 | 125 | forLog, forListener := c.impressionManager.Process(impressions, listenerEnabled) 126 | c.impressions.LogImpressions(forLog) 127 | 128 | // Custom Impression Listener 129 | if listenerEnabled { 130 | c.impressionListener.SendDataToClient(forListener, attributes) 131 | } 132 | } else { 133 | c.logger.Warning("No impression storage set in client. Not sending impressions!") 134 | } 135 | 136 | // Store latency 137 | c.evaluationTelemetry.RecordLatency(metricsLabel, evaluationTime) 138 | } 139 | 140 | // doTreatmentCall retrieves treatments of an specific feature flag with configurations object if it is present for a certain key and set of attributes 141 | func (c *SplitClient) doTreatmentCall(key interface{}, featureFlag string, attributes map[string]interface{}, operation string, metricsLabel string) (t TreatmentResult) { 142 | controlTreatment := TreatmentResult{ 143 | Treatment: evaluator.Control, 144 | Config: nil, 145 | } 146 | 147 | // Set up a guard deferred function to recover if the SDK starts panicking 148 | defer func() { 149 | if r := recover(); r != nil { 150 | // At this point we'll only trust that the logger isn't panicking trust 151 | // that the logger isn't panicking 152 | c.evaluationTelemetry.RecordException(metricsLabel) 153 | c.logger.Error( 154 | "SDK is panicking with the following error", r, "\n", 155 | string(debug.Stack()), "\n", 156 | "Returning CONTROL", "\n") 157 | t = controlTreatment 158 | } 159 | }() 160 | 161 | if c.isDestroyed() { 162 | c.logger.Error("Client has already been destroyed - no calls possible") 163 | return controlTreatment 164 | } 165 | 166 | matchingKey, bucketingKey, err := c.validator.ValidateTreatmentKey(key, operation) 167 | if err != nil { 168 | c.logger.Error(err.Error()) 169 | return controlTreatment 170 | } 171 | 172 | featureFlag, err = c.validator.ValidateFeatureName(featureFlag, operation) 173 | if err != nil { 174 | c.logger.Error(err.Error()) 175 | return controlTreatment 176 | } 177 | 178 | evaluationResult := c.getEvaluationResult(matchingKey, bucketingKey, featureFlag, attributes, operation) 179 | 180 | if !c.validator.IsSplitFound(evaluationResult.Label, featureFlag, operation) { 181 | return controlTreatment 182 | } 183 | 184 | c.storeData( 185 | []dtos.Impression{c.createImpression(featureFlag, bucketingKey, evaluationResult.Label, matchingKey, evaluationResult.Treatment, evaluationResult.SplitChangeNumber, evaluationResult.ImpressionsDisabled)}, 186 | attributes, 187 | metricsLabel, 188 | evaluationResult.EvaluationTime, 189 | ) 190 | 191 | return TreatmentResult{ 192 | Treatment: evaluationResult.Treatment, 193 | Config: evaluationResult.Config, 194 | } 195 | } 196 | 197 | // Treatment implements the main functionality of split. Retrieve treatments of a specific feature flag 198 | // for a certain key and set of attributes 199 | func (c *SplitClient) Treatment(key interface{}, featureFlagName string, attributes map[string]interface{}) string { 200 | return c.doTreatmentCall(key, featureFlagName, attributes, treatment, telemetry.Treatment).Treatment 201 | } 202 | 203 | // TreatmentWithConfig implements the main functionality of split. Retrieves the treatment of a specific feature flag 204 | // with the corresponding configuration if it is present 205 | func (c *SplitClient) TreatmentWithConfig(key interface{}, featureFlagName string, attributes map[string]interface{}) TreatmentResult { 206 | return c.doTreatmentCall(key, featureFlagName, attributes, treatmentWithConfig, telemetry.TreatmentWithConfig) 207 | } 208 | 209 | // Generates control treatments 210 | func (c *SplitClient) generateControlTreatments(featureFlagNames []string, operation string) map[string]TreatmentResult { 211 | treatments := make(map[string]TreatmentResult) 212 | filtered, err := c.validator.ValidateFeatureNames(featureFlagNames, operation) 213 | if err != nil { 214 | return treatments 215 | } 216 | for _, featureFlag := range filtered { 217 | treatments[featureFlag] = TreatmentResult{ 218 | Treatment: evaluator.Control, 219 | Config: nil, 220 | } 221 | } 222 | return treatments 223 | } 224 | 225 | func (c *SplitClient) processResult(result evaluator.Results, operation string, bucketingKey *string, matchingKey string, attributes map[string]interface{}, metricsLabel string) (t map[string]TreatmentResult) { 226 | var bulkImpressions []dtos.Impression 227 | treatments := make(map[string]TreatmentResult) 228 | for feature, evaluation := range result.Evaluations { 229 | if !c.validator.IsSplitFound(evaluation.Label, feature, operation) { 230 | treatments[feature] = TreatmentResult{ 231 | Treatment: evaluator.Control, 232 | Config: nil, 233 | } 234 | } else { 235 | bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber, evaluation.ImpressionsDisabled)) 236 | 237 | treatments[feature] = TreatmentResult{ 238 | Treatment: evaluation.Treatment, 239 | Config: evaluation.Config, 240 | } 241 | } 242 | } 243 | c.storeData(bulkImpressions, attributes, metricsLabel, result.EvaluationTime) 244 | return treatments 245 | } 246 | 247 | // doTreatmentsCall retrieves treatments of an specific array of feature flag names with configurations object if it is present for a certain key and set of attributes 248 | func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []string, attributes map[string]interface{}, operation string, metricsLabel string) (t map[string]TreatmentResult) { 249 | // Set up a guard deferred function to recover if the SDK starts panicking 250 | defer func() { 251 | if r := recover(); r != nil { 252 | // At this point we'll only trust that the logger isn't panicking trust 253 | // that the logger isn't panicking 254 | c.evaluationTelemetry.RecordException(metricsLabel) 255 | c.logger.Error( 256 | "SDK is panicking with the following error", r, "\n", 257 | string(debug.Stack()), "\n") 258 | t = c.generateControlTreatments(featureFlagNames, operation) 259 | } 260 | }() 261 | 262 | if c.isDestroyed() { 263 | c.logger.Error("Client has already been destroyed - no calls possible") 264 | return c.generateControlTreatments(featureFlagNames, operation) 265 | } 266 | 267 | matchingKey, bucketingKey, err := c.validator.ValidateTreatmentKey(key, operation) 268 | if err != nil { 269 | c.logger.Error(err.Error()) 270 | return c.generateControlTreatments(featureFlagNames, operation) 271 | } 272 | 273 | filteredFeatures, err := c.validator.ValidateFeatureNames(featureFlagNames, operation) 274 | if err != nil { 275 | c.logger.Error(err.Error()) 276 | return map[string]TreatmentResult{} 277 | } 278 | 279 | evaluationsResult := c.getEvaluationsResult(matchingKey, bucketingKey, filteredFeatures, attributes, operation) 280 | 281 | return c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel) 282 | } 283 | 284 | // doTreatmentsCallByFlagSets retrieves treatments of a specific array of feature flag names, that belong to flag sets, with configurations object if it is present for a certain key and set of attributes 285 | func (c *SplitClient) doTreatmentsCallByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}, operation string, metricsLabel string) (t map[string]TreatmentResult) { 286 | treatments := make(map[string]TreatmentResult) 287 | 288 | // Set up a guard deferred function to recover if the SDK starts panicking 289 | defer func() { 290 | if r := recover(); r != nil { 291 | // At this point we'll only trust that the logger isn't panicking trust 292 | // that the logger isn't panicking 293 | c.evaluationTelemetry.RecordException(metricsLabel) 294 | c.logger.Error( 295 | "SDK is panicking with the following error", r, "\n", 296 | string(debug.Stack()), "\n") 297 | t = treatments 298 | } 299 | }() 300 | 301 | if c.isDestroyed() { 302 | return treatments 303 | } 304 | 305 | matchingKey, bucketingKey, err := c.validator.ValidateTreatmentKey(key, operation) 306 | if err != nil { 307 | c.logger.Error(err.Error()) 308 | return treatments 309 | } 310 | 311 | if c.isReady() { 312 | evaluationsResult := c.evaluator.EvaluateFeatureByFlagSets(matchingKey, bucketingKey, flagSets, attributes) 313 | treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel) 314 | } 315 | return treatments 316 | } 317 | 318 | // Treatments evaluates multiple feature flag names for a single user and set of attributes at once 319 | func (c *SplitClient) Treatments(key interface{}, featureFlagNames []string, attributes map[string]interface{}) map[string]string { 320 | treatmentsResult := map[string]string{} 321 | result := c.doTreatmentsCall(key, featureFlagNames, attributes, treatments, telemetry.Treatments) 322 | for feature, treatmentResult := range result { 323 | treatmentsResult[feature] = treatmentResult.Treatment 324 | } 325 | return treatmentsResult 326 | } 327 | 328 | func (c *SplitClient) validateSets(flagSets []string) []string { 329 | if len(flagSets) == 0 { 330 | c.logger.Warning("sets must be a non-empty array") 331 | return nil 332 | } 333 | flagSets, errs := flagsets.SanitizeMany(flagSets) 334 | if len(errs) != 0 { 335 | for _, err := range errs { 336 | if errType, ok := err.(*dtos.FlagSetValidatonError); ok { 337 | c.logger.Warning(errType.Message) 338 | } 339 | } 340 | } 341 | flagSets = c.filterSetsAreInConfig(flagSets) 342 | if len(flagSets) == 0 { 343 | return nil 344 | } 345 | return flagSets 346 | } 347 | 348 | // Treatments evaluate multiple feature flag names belonging to a flag set for a single user and a set of attributes at once 349 | func (c *SplitClient) TreatmentsByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}) map[string]string { 350 | treatmentsResult := map[string]string{} 351 | sets := c.validateSets([]string{flagSet}) 352 | if sets == nil { 353 | return treatmentsResult 354 | } 355 | result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSet, telemetry.TreatmentsByFlagSet) 356 | for feature, treatmentResult := range result { 357 | treatmentsResult[feature] = treatmentResult.Treatment 358 | } 359 | return treatmentsResult 360 | } 361 | 362 | // Treatments evaluate multiple feature flag names belonging to flag sets for a single user and a set of attributes at once 363 | func (c *SplitClient) TreatmentsByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}) map[string]string { 364 | treatmentsResult := map[string]string{} 365 | flagSets = c.validateSets(flagSets) 366 | if flagSets == nil { 367 | return treatmentsResult 368 | } 369 | result := c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsByFlagSets, telemetry.TreatmentsByFlagSets) 370 | for feature, treatmentResult := range result { 371 | treatmentsResult[feature] = treatmentResult.Treatment 372 | } 373 | return treatmentsResult 374 | } 375 | 376 | func (c *SplitClient) filterSetsAreInConfig(flagSets []string) []string { 377 | toReturn := []string{} 378 | for _, flagSet := range flagSets { 379 | if !c.flagSetsFilter.IsPresent(flagSet) { 380 | c.logger.Warning(fmt.Sprintf("you passed %s which is not part of the configured FlagSetsFilter, ignoring Flag Set.", flagSet)) 381 | continue 382 | } 383 | toReturn = append(toReturn, flagSet) 384 | } 385 | return toReturn 386 | } 387 | 388 | // TreatmentsWithConfig evaluates multiple feature flag names for a single user and set of attributes at once and returns configurations 389 | func (c *SplitClient) TreatmentsWithConfig(key interface{}, featureFlagNames []string, attributes map[string]interface{}) map[string]TreatmentResult { 390 | return c.doTreatmentsCall(key, featureFlagNames, attributes, treatmentsWithConfig, telemetry.TreatmentsWithConfig) 391 | } 392 | 393 | // TreatmentsWithConfigByFlagSet evaluates multiple feature flag names belonging to a flag set for a single user and set of attributes at once and returns configurations 394 | func (c *SplitClient) TreatmentsWithConfigByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}) map[string]TreatmentResult { 395 | treatmentsResult := make(map[string]TreatmentResult) 396 | sets := c.validateSets([]string{flagSet}) 397 | if sets == nil { 398 | return treatmentsResult 399 | } 400 | return c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsWithConfigByFlagSet, telemetry.TreatmentsWithConfigByFlagSet) 401 | } 402 | 403 | // TreatmentsWithConfigByFlagSet evaluates multiple feature flag names belonging to a flag sets for a single user and set of attributes at once and returns configurations 404 | func (c *SplitClient) TreatmentsWithConfigByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}) map[string]TreatmentResult { 405 | treatmentsResult := make(map[string]TreatmentResult) 406 | flagSets = c.validateSets(flagSets) 407 | if flagSets == nil { 408 | return treatmentsResult 409 | } 410 | return c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsWithConfigByFlagSets, telemetry.TreatmentsWithConfigByFlagSets) 411 | } 412 | 413 | // isDestroyed returns true if the client has been destroyed 414 | func (c *SplitClient) isDestroyed() bool { 415 | return c.factory.IsDestroyed() 416 | } 417 | 418 | // isReady returns true if the client is ready 419 | func (c *SplitClient) isReady() bool { 420 | return c.factory.IsReady() 421 | } 422 | 423 | // Destroy the client and the underlying factory. 424 | func (c *SplitClient) Destroy() { 425 | if !c.isDestroyed() { 426 | c.factory.Destroy() 427 | } 428 | } 429 | 430 | // Track an event and its custom value 431 | func (c *SplitClient) Track(key string, trafficType string, eventType string, value interface{}, properties map[string]interface{}) (ret error) { 432 | defer func() { 433 | if r := recover(); r != nil { 434 | // At this point we'll only trust that the logger isn't panicking 435 | c.evaluationTelemetry.RecordException(telemetry.Track) 436 | c.logger.Error( 437 | "SDK is panicking with the following error", r, "\n", 438 | string(debug.Stack()), "\n", 439 | ) 440 | ret = errors.New("Track is panicking. Please check logs") 441 | } 442 | }() 443 | 444 | if c.isDestroyed() { 445 | c.logger.Error("Client has already been destroyed - no calls possible") 446 | return errors.New("Client has already been destroyed - no calls possible") 447 | } 448 | 449 | if !c.isReady() { 450 | c.logger.Warning("Track: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method") 451 | c.initTelemetry.RecordNonReadyUsage() 452 | } 453 | 454 | key, trafficType, eventType, value, err := c.validator.ValidateTrackInputs( 455 | key, 456 | trafficType, 457 | eventType, 458 | value, 459 | c.isReady() && c.factory.apikey != conf.Localhost, 460 | ) 461 | if err != nil { 462 | c.logger.Error(err.Error()) 463 | return err 464 | } 465 | 466 | properties, size, err := c.validator.validateTrackProperties(properties) 467 | if err != nil { 468 | return err 469 | } 470 | 471 | err = c.events.Push(dtos.EventDTO{ 472 | Key: key, 473 | TrafficTypeName: trafficType, 474 | EventTypeID: eventType, 475 | Value: value, 476 | Timestamp: time.Now().UTC().UnixNano() / int64(time.Millisecond), // Convert standard timestamp to java's ms timestamps 477 | Properties: properties, 478 | }, size) 479 | 480 | if err != nil { 481 | c.logger.Error("Error tracking event", err.Error()) 482 | return err 483 | } 484 | 485 | return nil 486 | } 487 | 488 | // BlockUntilReady Calls BlockUntilReady on factory to block client on readiness 489 | func (c *SplitClient) BlockUntilReady(timer int) error { 490 | return c.factory.BlockUntilReady(timer) 491 | } 492 | -------------------------------------------------------------------------------- /splitio/client/factory.go: -------------------------------------------------------------------------------- 1 | // Package client contains implementations of the Split SDK client and the factory used 2 | // to instantiate it. 3 | package client 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/splitio/go-client/v6/splitio" 14 | "github.com/splitio/go-client/v6/splitio/conf" 15 | impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener" 16 | "github.com/splitio/go-client/v6/splitio/impressions" 17 | 18 | config "github.com/splitio/go-split-commons/v6/conf" 19 | "github.com/splitio/go-split-commons/v6/dtos" 20 | "github.com/splitio/go-split-commons/v6/engine" 21 | "github.com/splitio/go-split-commons/v6/engine/evaluator" 22 | "github.com/splitio/go-split-commons/v6/flagsets" 23 | "github.com/splitio/go-split-commons/v6/healthcheck/application" 24 | "github.com/splitio/go-split-commons/v6/provisional" 25 | "github.com/splitio/go-split-commons/v6/provisional/strategy" 26 | "github.com/splitio/go-split-commons/v6/service/api" 27 | "github.com/splitio/go-split-commons/v6/service/api/specs" 28 | "github.com/splitio/go-split-commons/v6/service/local" 29 | "github.com/splitio/go-split-commons/v6/storage" 30 | "github.com/splitio/go-split-commons/v6/storage/inmemory" 31 | "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexmap" 32 | "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexqueue" 33 | "github.com/splitio/go-split-commons/v6/storage/mocks" 34 | "github.com/splitio/go-split-commons/v6/storage/redis" 35 | "github.com/splitio/go-split-commons/v6/synchronizer" 36 | "github.com/splitio/go-split-commons/v6/synchronizer/worker/event" 37 | "github.com/splitio/go-split-commons/v6/synchronizer/worker/segment" 38 | "github.com/splitio/go-split-commons/v6/synchronizer/worker/split" 39 | "github.com/splitio/go-split-commons/v6/tasks" 40 | "github.com/splitio/go-split-commons/v6/telemetry" 41 | "github.com/splitio/go-toolkit/v5/logging" 42 | ) 43 | 44 | const ( 45 | sdkStatusDestroyed = iota 46 | sdkStatusInitializing 47 | sdkStatusReady 48 | 49 | sdkInitializationFailed = -1 50 | ) 51 | 52 | type sdkStorages struct { 53 | splits storage.SplitStorageConsumer 54 | segments storage.SegmentStorageConsumer 55 | impressionsConsumer storage.ImpressionStorageConsumer 56 | impressions storage.ImpressionStorageProducer 57 | events storage.EventStorageProducer 58 | initTelemetry storage.TelemetryConfigProducer 59 | runtimeTelemetry storage.TelemetryRuntimeProducer 60 | evaluationTelemetry storage.TelemetryEvaluationProducer 61 | impressionsCount storage.ImpressionsCountProducer 62 | } 63 | 64 | // SplitFactory struct is responsible for instantiating and storing instances of client and manager. 65 | type SplitFactory struct { 66 | startTime time.Time // Tracking startTime 67 | metadata dtos.Metadata 68 | storages sdkStorages 69 | apikey string 70 | status atomic.Value 71 | readinessSubscriptors map[int]chan int 72 | operationMode string 73 | mutex sync.Mutex 74 | cfg *conf.SplitSdkConfig 75 | impressionListener *impressionlistener.WrapperImpressionListener 76 | logger logging.LoggerInterface 77 | syncManager synchronizer.Manager 78 | telemetrySync telemetry.TelemetrySynchronizer // To execute SynchronizeInit 79 | impressionManager provisional.ImpressionManager 80 | } 81 | 82 | // Client returns the split client instantiated by the factory 83 | func (f *SplitFactory) Client() *SplitClient { 84 | return &SplitClient{ 85 | logger: f.logger, 86 | evaluator: evaluator.NewEvaluator(f.storages.splits, f.storages.segments, engine.NewEngine(f.logger), f.logger), 87 | impressions: f.storages.impressions, 88 | events: f.storages.events, 89 | validator: inputValidation{ 90 | logger: f.logger, 91 | splitStorage: f.storages.splits, 92 | }, 93 | factory: f, 94 | impressionListener: f.impressionListener, 95 | impressionManager: f.impressionManager, 96 | initTelemetry: f.storages.initTelemetry, // For capturing NonReadyUsages 97 | runtimeTelemetry: f.storages.runtimeTelemetry, // For capturing runtime stats 98 | evaluationTelemetry: f.storages.evaluationTelemetry, // For capturing treatment stats 99 | } 100 | } 101 | 102 | // Manager returns the split manager instantiated by the factory 103 | func (f *SplitFactory) Manager() *SplitManager { 104 | return &SplitManager{ 105 | splitStorage: f.storages.splits, 106 | validator: inputValidation{logger: f.logger}, 107 | logger: f.logger, 108 | factory: f, 109 | initTelemetry: f.storages.initTelemetry, // For capturing NonReadyUsages 110 | } 111 | } 112 | 113 | // IsDestroyed returns true if the client has been destroyed 114 | func (f *SplitFactory) IsDestroyed() bool { 115 | return f.status.Load() == sdkStatusDestroyed 116 | } 117 | 118 | // IsReady returns true if the factory is ready 119 | func (f *SplitFactory) IsReady() bool { 120 | return f.status.Load() == sdkStatusReady 121 | } 122 | 123 | // initializates tasks for in-memory mode 124 | func (f *SplitFactory) initializationManager(readyChannel chan int, flagSetsInvalid int64) { 125 | go f.syncManager.Start() 126 | msg := <-readyChannel 127 | switch msg { 128 | case synchronizer.Ready: 129 | // Broadcast ready status for SDK 130 | f.broadcastReadiness(sdkStatusReady, make([]string, 0), flagSetsInvalid) 131 | default: 132 | f.broadcastReadiness(sdkInitializationFailed, make([]string, 0), flagSetsInvalid) 133 | } 134 | } 135 | 136 | func (f *SplitFactory) initializationRedis() { 137 | go f.syncManager.Start() 138 | f.broadcastReadiness(sdkStatusReady, make([]string, 0), 0) 139 | } 140 | 141 | // recordInitTelemetry In charge of recording init stats from redis and memory 142 | func (f *SplitFactory) recordInitTelemetry(tags []string, currentFactories map[string]int64, flagSetsInvalid int64) { 143 | f.logger.Debug("Sending init telemetry") 144 | f.telemetrySync.SynchronizeConfig( 145 | telemetry.InitConfig{ 146 | AdvancedConfig: config.AdvancedConfig{ 147 | HTTPTimeout: f.cfg.Advanced.HTTPTimeout, 148 | SegmentQueueSize: f.cfg.Advanced.SegmentQueueSize, 149 | SegmentWorkers: f.cfg.Advanced.SegmentWorkers, 150 | SdkURL: f.cfg.Advanced.SdkURL, 151 | EventsURL: f.cfg.Advanced.EventsURL, 152 | TelemetryServiceURL: f.cfg.Advanced.TelemetryServiceURL, 153 | EventsBulkSize: f.cfg.Advanced.EventsBulkSize, 154 | EventsQueueSize: f.cfg.Advanced.EventsQueueSize, 155 | ImpressionsQueueSize: f.cfg.Advanced.ImpressionsQueueSize, 156 | ImpressionsBulkSize: f.cfg.Advanced.ImpressionsBulkSize, 157 | StreamingEnabled: f.cfg.Advanced.StreamingEnabled, 158 | AuthServiceURL: f.cfg.Advanced.AuthServiceURL, 159 | StreamingServiceURL: f.cfg.Advanced.StreamingServiceURL, 160 | }, 161 | TaskPeriods: config.TaskPeriods(f.cfg.TaskPeriods), 162 | ImpressionsMode: f.cfg.ImpressionsMode, 163 | ListenerEnabled: f.cfg.Advanced.ImpressionListener != nil, 164 | FlagSetsTotal: int64(len(f.cfg.Advanced.FlagSetsFilter)), 165 | FlagSetsInvalid: flagSetsInvalid, 166 | }, 167 | time.Now().UTC().Sub(f.startTime).Milliseconds(), 168 | currentFactories, 169 | tags, 170 | ) 171 | } 172 | 173 | // broadcastReadiness broadcasts message to all the subscriptors 174 | func (f *SplitFactory) broadcastReadiness(status int, tags []string, flagSetsInvalid int64) { 175 | f.mutex.Lock() 176 | defer f.mutex.Unlock() 177 | if f.status.Load() == sdkStatusInitializing && status == sdkStatusReady { 178 | f.status.Store(sdkStatusReady) 179 | } 180 | for _, subscriptor := range f.readinessSubscriptors { 181 | subscriptor <- status 182 | } 183 | // At this point the SDK is ready for sending telemetry 184 | go f.recordInitTelemetry(tags, getFactories(), flagSetsInvalid) 185 | } 186 | 187 | // subscribes listener 188 | func (f *SplitFactory) subscribe(name int, subscriptor chan int) { 189 | f.mutex.Lock() 190 | defer f.mutex.Unlock() 191 | f.readinessSubscriptors[name] = subscriptor 192 | } 193 | 194 | // removes a particular subscriptor from the list 195 | func (f *SplitFactory) unsubscribe(name int) { 196 | f.mutex.Lock() 197 | defer f.mutex.Unlock() 198 | _, ok := f.readinessSubscriptors[name] 199 | if ok { 200 | delete(f.readinessSubscriptors, name) 201 | } 202 | } 203 | 204 | // BlockUntilReady blocks client or manager until the SDK is ready, error occurs or times out 205 | func (f *SplitFactory) BlockUntilReady(timer int) error { 206 | if f.IsReady() { 207 | return nil 208 | } 209 | if timer <= 0 { 210 | return errors.New("SDK Initialization: timer must be positive number") 211 | } 212 | if f.IsDestroyed() { 213 | return errors.New("SDK Initialization: Client is destroyed") 214 | } 215 | block := make(chan int, 1) 216 | 217 | f.mutex.Lock() 218 | subscriptorName := len(f.readinessSubscriptors) 219 | f.mutex.Unlock() 220 | 221 | defer func() { 222 | // Unsubscription will happen only if a block channel has been created 223 | if block != nil { 224 | f.unsubscribe(subscriptorName) 225 | close(block) 226 | } 227 | }() 228 | 229 | f.subscribe(subscriptorName, block) 230 | 231 | select { 232 | case status := <-block: 233 | switch status { 234 | case sdkStatusReady: 235 | break 236 | case sdkInitializationFailed: 237 | return errors.New("SDK Initialization failed") 238 | } 239 | case <-time.After(time.Second * time.Duration(timer)): 240 | f.storages.initTelemetry.RecordBURTimeout() // Records BURTimeout 241 | return fmt.Errorf("SDK Initialization: time of %d exceeded", timer) 242 | } 243 | 244 | return nil 245 | } 246 | 247 | // Destroy stops all async tasks and clears all storages 248 | func (f *SplitFactory) Destroy() { 249 | if !f.IsDestroyed() { 250 | removeInstanceFromTracker(f.apikey) 251 | } 252 | f.status.Store(sdkStatusDestroyed) 253 | if f.storages.runtimeTelemetry != nil { 254 | f.storages.runtimeTelemetry.RecordSessionLength(int64(time.Since(f.startTime) * time.Millisecond)) 255 | } 256 | f.syncManager.Stop() 257 | } 258 | 259 | // setupLogger sets up the logger according to the parameters submitted by the sdk user 260 | func setupLogger(cfg *conf.SplitSdkConfig) logging.LoggerInterface { 261 | var logger logging.LoggerInterface 262 | if cfg.Logger != nil { 263 | // If a custom logger is supplied, use it. 264 | logger = cfg.Logger 265 | } else { 266 | logger = logging.NewLogger(&cfg.LoggerConfig) 267 | } 268 | return logger 269 | } 270 | 271 | func setupInMemoryFactory( 272 | apikey string, 273 | cfg *conf.SplitSdkConfig, 274 | logger logging.LoggerInterface, 275 | metadata dtos.Metadata, 276 | ) (*SplitFactory, error) { 277 | advanced, warnings := conf.NormalizeSDKConf(cfg.Advanced) 278 | advanced.AuthSpecVersion = specs.FLAG_V1_1 279 | advanced.FlagsSpecVersion = specs.FLAG_V1_1 280 | printWarnings(logger, warnings) 281 | flagSetsInvalid := int64(len(cfg.Advanced.FlagSetsFilter) - len(advanced.FlagSetsFilter)) 282 | if strings.TrimSpace(cfg.SplitSyncProxyURL) != "" { 283 | advanced.StreamingEnabled = false 284 | } 285 | 286 | inMememoryFullQueue := make(chan string, 2) // Size 2: So that it's able to accept one event from each resource simultaneously. 287 | 288 | flagSetFilter := flagsets.NewFlagSetFilter(advanced.FlagSetsFilter) 289 | splitsStorage := mutexmap.NewMMSplitStorage(flagSetFilter) 290 | segmentsStorage := mutexmap.NewMMSegmentStorage() 291 | telemetryStorage, err := inmemory.NewTelemetryStorage() 292 | impressionsStorage := mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, inMememoryFullQueue, logger, telemetryStorage) 293 | eventsStorage := mutexqueue.NewMQEventsStorage(cfg.Advanced.EventsQueueSize, inMememoryFullQueue, logger, telemetryStorage) 294 | if err != nil { 295 | return nil, err 296 | } 297 | 298 | var dummyHC = &application.Dummy{} 299 | 300 | splitAPI := api.NewSplitAPI(apikey, advanced, logger, metadata) 301 | workers := synchronizer.Workers{ 302 | SplitUpdater: split.NewSplitUpdater(splitsStorage, splitAPI.SplitFetcher, logger, telemetryStorage, dummyHC, flagSetFilter), 303 | SegmentUpdater: segment.NewSegmentUpdater(splitsStorage, segmentsStorage, splitAPI.SegmentFetcher, logger, telemetryStorage, dummyHC), 304 | EventRecorder: event.NewEventRecorderSingle(eventsStorage, splitAPI.EventRecorder, logger, metadata, telemetryStorage), 305 | TelemetryRecorder: telemetry.NewTelemetrySynchronizer(telemetryStorage, splitAPI.TelemetryRecorder, splitsStorage, segmentsStorage, logger, metadata, telemetryStorage), 306 | } 307 | splitTasks := synchronizer.SplitTasks{ 308 | SplitSyncTask: tasks.NewFetchSplitsTask(workers.SplitUpdater, cfg.TaskPeriods.SplitSync, logger), 309 | SegmentSyncTask: tasks.NewFetchSegmentsTask(workers.SegmentUpdater, cfg.TaskPeriods.SegmentSync, advanced.SegmentWorkers, advanced.SegmentQueueSize, logger, dummyHC), 310 | EventSyncTask: tasks.NewRecordEventsTask(workers.EventRecorder, advanced.EventsBulkSize, cfg.TaskPeriods.EventsSync, logger), 311 | TelemetrySyncTask: tasks.NewRecordTelemetryTask(workers.TelemetryRecorder, cfg.TaskPeriods.TelemetrySync, logger), 312 | } 313 | 314 | storages := sdkStorages{ 315 | splits: splitsStorage, 316 | events: eventsStorage, 317 | impressionsConsumer: impressionsStorage, 318 | impressions: impressionsStorage, 319 | segments: segmentsStorage, 320 | initTelemetry: telemetryStorage, 321 | evaluationTelemetry: telemetryStorage, 322 | runtimeTelemetry: telemetryStorage, 323 | } 324 | 325 | if cfg.ImpressionsMode == "" { 326 | cfg.ImpressionsMode = config.ImpressionsModeOptimized 327 | } 328 | 329 | impressionManager, err := impressions.BuildInMemoryManager(cfg, advanced, logger, &splitTasks, &workers, metadata, splitAPI, storages.runtimeTelemetry, storages.impressionsConsumer) 330 | if err != nil { 331 | return nil, err 332 | } 333 | 334 | syncImpl := synchronizer.NewSynchronizer( 335 | advanced, 336 | splitTasks, 337 | workers, 338 | logger, 339 | inMememoryFullQueue, 340 | ) 341 | 342 | readyChannel := make(chan int, 1) 343 | clientKey := apikey[len(apikey)-4:] 344 | syncManager, err := synchronizer.NewSynchronizerManager( 345 | syncImpl, 346 | logger, 347 | advanced, 348 | splitAPI.AuthClient, 349 | splitsStorage, 350 | readyChannel, 351 | telemetryStorage, 352 | metadata, 353 | &clientKey, 354 | dummyHC, 355 | ) 356 | if err != nil { 357 | return nil, err 358 | } 359 | 360 | splitFactory := SplitFactory{ 361 | startTime: time.Now().UTC(), 362 | apikey: apikey, 363 | cfg: cfg, 364 | metadata: metadata, 365 | logger: logger, 366 | operationMode: conf.InMemoryStandAlone, 367 | storages: storages, 368 | readinessSubscriptors: make(map[int]chan int), 369 | syncManager: syncManager, 370 | telemetrySync: workers.TelemetryRecorder, 371 | impressionManager: impressionManager, 372 | } 373 | splitFactory.status.Store(sdkStatusInitializing) 374 | setFactory(splitFactory.apikey, splitFactory.logger) 375 | 376 | go splitFactory.initializationManager(readyChannel, flagSetsInvalid) 377 | 378 | return &splitFactory, nil 379 | } 380 | 381 | func setupRedisFactory(apikey string, cfg *conf.SplitSdkConfig, logger logging.LoggerInterface, metadata dtos.Metadata) (*SplitFactory, error) { 382 | redisClient, err := redis.NewRedisClient(&cfg.Redis, logger) 383 | if err != nil { 384 | logger.Error("Failed to instantiate redis client.") 385 | return nil, err 386 | } 387 | 388 | telemetryStorage := redis.NewTelemetryStorage(redisClient, logger, metadata) 389 | runtimeTelemetry := mocks.MockTelemetryStorage{ 390 | RecordSyncLatencyCall: func(resource int, latency time.Duration) {}, 391 | RecordImpressionsStatsCall: func(dataType int, count int64) {}, 392 | RecordSessionLengthCall: func(session int64) {}, 393 | } 394 | inMememoryFullQueue := make(chan string, 2) // Size 2: So that it's able to accept one event from each resource simultaneously. 395 | impressionStorage := redis.NewImpressionStorage(redisClient, metadata, logger) 396 | 397 | if len(cfg.Advanced.FlagSetsFilter) != 0 { 398 | cfg.Advanced.FlagSetsFilter = []string{} 399 | logger.Warning("FlagSets filter is not applicable for Consumer modes where the SDK does not keep rollout data in sync. FlagSet filter was discarded") 400 | } 401 | flagSetFilter := flagsets.NewFlagSetFilter([]string{}) 402 | 403 | storages := sdkStorages{ 404 | splits: redis.NewSplitStorage(redisClient, logger, flagSetFilter), 405 | segments: redis.NewSegmentStorage(redisClient, logger), 406 | impressionsConsumer: impressionStorage, 407 | impressions: impressionStorage, 408 | events: redis.NewEventsStorage(redisClient, metadata, logger), 409 | initTelemetry: telemetryStorage, 410 | evaluationTelemetry: telemetryStorage, 411 | impressionsCount: redis.NewImpressionsCountStorage(redisClient, logger), 412 | runtimeTelemetry: runtimeTelemetry, 413 | } 414 | 415 | splitTasks := synchronizer.SplitTasks{} 416 | workers := synchronizer.Workers{} 417 | advanced := config.AdvancedConfig{} 418 | 419 | if cfg.ImpressionsMode == "" { 420 | cfg.ImpressionsMode = config.ImpressionsModeDebug 421 | } 422 | 423 | impressionManager, err := impressions.BuildRedisManager(cfg, logger, &splitTasks, storages.initTelemetry, storages.impressionsCount, storages.runtimeTelemetry) 424 | if err != nil { 425 | return nil, err 426 | } 427 | 428 | syncImpl := synchronizer.NewSynchronizer( 429 | advanced, 430 | splitTasks, 431 | workers, 432 | logger, 433 | inMememoryFullQueue, 434 | ) 435 | 436 | syncManager := synchronizer.NewSynchronizerManagerRedis(syncImpl, logger) 437 | 438 | factory := &SplitFactory{ 439 | startTime: time.Now().UTC(), 440 | apikey: apikey, 441 | cfg: cfg, 442 | metadata: metadata, 443 | logger: logger, 444 | operationMode: conf.RedisConsumer, 445 | storages: storages, 446 | readinessSubscriptors: make(map[int]chan int), 447 | telemetrySync: telemetry.NewSynchronizerRedis(telemetryStorage, logger), 448 | impressionManager: impressionManager, 449 | syncManager: syncManager, 450 | } 451 | factory.status.Store(sdkStatusInitializing) 452 | setFactory(factory.apikey, factory.logger) 453 | 454 | factory.initializationRedis() 455 | 456 | return factory, nil 457 | } 458 | 459 | func setupLocalhostFactory( 460 | apikey string, 461 | cfg *conf.SplitSdkConfig, 462 | logger logging.LoggerInterface, 463 | metadata dtos.Metadata, 464 | ) (*SplitFactory, error) { 465 | flagSets, errs := flagsets.SanitizeMany(cfg.Advanced.FlagSetsFilter) 466 | flagSetsInvalid := int64(len(cfg.Advanced.FlagSetsFilter) - len(flagSets)) 467 | printWarnings(logger, errs) 468 | flagSetFilter := flagsets.NewFlagSetFilter(flagSets) 469 | splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) 470 | segmentStorage := mutexmap.NewMMSegmentStorage() 471 | telemetryStorage, err := inmemory.NewTelemetryStorage() 472 | if err != nil { 473 | return nil, err 474 | } 475 | readyChannel := make(chan int, 1) 476 | fileFormat := local.DefineFormat(cfg.SplitFile, logger) 477 | splitAPI := &api.SplitAPI{SplitFetcher: local.NewFileSplitFetcher(cfg.SplitFile, logger, fileFormat)} 478 | 479 | if cfg.SegmentDirectory != "" { 480 | splitAPI.SegmentFetcher = local.NewFileSegmentFetcher(cfg.SegmentDirectory, logger) 481 | } 482 | 483 | var dummyHC = &application.Dummy{} 484 | 485 | localConfig := &synchronizer.LocalConfig{ 486 | SplitPeriod: cfg.TaskPeriods.SplitSync, 487 | SegmentPeriod: cfg.TaskPeriods.SegmentSync, 488 | SegmentWorkers: cfg.Advanced.SegmentWorkers, 489 | QueueSize: cfg.Advanced.SegmentQueueSize, 490 | SegmentDirectory: cfg.SegmentDirectory, 491 | RefreshEnabled: cfg.LocalhostRefreshEnabled, 492 | } 493 | 494 | syncManager, err := synchronizer.NewSynchronizerManager( 495 | synchronizer.NewLocal(localConfig, splitAPI, splitStorage, segmentStorage, logger, telemetryStorage, dummyHC), 496 | logger, 497 | config.AdvancedConfig{StreamingEnabled: false}, 498 | nil, 499 | splitStorage, 500 | readyChannel, 501 | telemetryStorage, 502 | metadata, 503 | nil, 504 | dummyHC, 505 | ) 506 | 507 | if err != nil { 508 | return nil, err 509 | } 510 | 511 | splitFactory := &SplitFactory{ 512 | startTime: time.Now().UTC(), 513 | apikey: apikey, 514 | cfg: cfg, 515 | metadata: metadata, 516 | logger: logger, 517 | storages: sdkStorages{ 518 | splits: splitStorage, 519 | impressions: mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 1), logger, telemetryStorage), 520 | events: mutexqueue.NewMQEventsStorage(cfg.Advanced.EventsQueueSize, make(chan string, 1), logger, telemetryStorage), 521 | segments: segmentStorage, 522 | initTelemetry: telemetryStorage, 523 | evaluationTelemetry: telemetryStorage, 524 | runtimeTelemetry: telemetryStorage, 525 | }, 526 | readinessSubscriptors: make(map[int]chan int), 527 | syncManager: syncManager, 528 | telemetrySync: &telemetry.NoOp{}, 529 | } 530 | splitFactory.status.Store(sdkStatusInitializing) 531 | 532 | impressionObserver, err := strategy.NewImpressionObserver(500) 533 | if err != nil { 534 | return nil, err 535 | } 536 | impressionsStrategy := strategy.NewDebugImpl(impressionObserver, cfg.Advanced.ImpressionListener != nil) 537 | splitFactory.impressionManager = provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl) 538 | setFactory(splitFactory.apikey, splitFactory.logger) 539 | 540 | // Call fetching tasks as goroutine 541 | go splitFactory.initializationManager(readyChannel, flagSetsInvalid) 542 | 543 | return splitFactory, nil 544 | } 545 | 546 | // newFactory instantiates a new SplitFactory object. Accepts a SplitSdkConfig struct as an argument, 547 | // which will be used to instantiate both the client and the manager 548 | func newFactory(apikey string, cfg conf.SplitSdkConfig, logger logging.LoggerInterface) (*SplitFactory, error) { 549 | metadata := dtos.Metadata{ 550 | SDKVersion: "go-" + splitio.Version, 551 | MachineIP: cfg.IPAddress, 552 | MachineName: cfg.InstanceName, 553 | } 554 | 555 | var splitFactory *SplitFactory 556 | var err error 557 | 558 | switch cfg.OperationMode { 559 | case conf.InMemoryStandAlone: 560 | splitFactory, err = setupInMemoryFactory(apikey, &cfg, logger, metadata) 561 | case conf.RedisConsumer: 562 | splitFactory, err = setupRedisFactory(apikey, &cfg, logger, metadata) 563 | case conf.Localhost: 564 | splitFactory, err = setupLocalhostFactory(apikey, &cfg, logger, metadata) 565 | default: 566 | err = fmt.Errorf("Invalid operation mode \"%s\"", cfg.OperationMode) 567 | } 568 | 569 | if err != nil { 570 | return nil, err 571 | } 572 | 573 | if cfg.Advanced.ImpressionListener != nil { 574 | splitFactory.impressionListener = impressionlistener.NewImpressionListenerWrapper( 575 | cfg.Advanced.ImpressionListener, 576 | metadata, 577 | ) 578 | } 579 | 580 | return splitFactory, nil 581 | } 582 | 583 | func printWarnings(logger logging.LoggerInterface, errs []error) { 584 | if len(errs) != 0 { 585 | for _, err := range errs { 586 | if errType, ok := err.(dtos.FlagSetValidatonError); ok { 587 | logger.Warning(errType.Message) 588 | } 589 | } 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /splitio/client/factory_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-split-commons/v6/flagsets" 7 | ) 8 | 9 | func TestPrintWarnings(t *testing.T) { 10 | 11 | flagSets, warnings := flagsets.SanitizeMany([]string{"set1", " set2"}) 12 | if len(flagSets) != 2 { 13 | t.Error("flag set size should be 2") 14 | } 15 | printWarnings(getMockedLogger(), warnings) 16 | if !mW.Matches("Flag Set name set2 has extra whitespace, trimming") { 17 | t.Error("Wrong message") 18 | } 19 | flagSets, warnings = flagsets.SanitizeMany([]string{"set1", "Set2"}) 20 | if len(flagSets) != 2 { 21 | t.Error("flag set size should be 2") 22 | } 23 | printWarnings(getMockedLogger(), warnings) 24 | if !mW.Matches("Flag Set name Set2 should be all lowercase - converting string to lowercase") { 25 | t.Error("Wrong message") 26 | } 27 | flagSets, warnings = flagsets.SanitizeMany([]string{"set1", "@set4"}) 28 | if len(flagSets) != 1 { 29 | t.Error("flag set size should be 1") 30 | } 31 | printWarnings(getMockedLogger(), warnings) 32 | if !mW.Matches("you passed @set4, Flag Set must adhere to the regular expressions ^[a-z0-9][_a-z0-9]{0,49}$. This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. @set4 was discarded.") { 33 | t.Error("Wrong message") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /splitio/client/factory_tracker.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/splitio/go-client/v6/splitio/conf" 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | ) 10 | 11 | // factoryInstances factory tracker instantiations 12 | var factoryInstances = make(map[string]int64) 13 | var mutex = &sync.RWMutex{} 14 | 15 | func setFactory(apikey string, logger logging.LoggerInterface) { 16 | mutex.Lock() 17 | defer mutex.Unlock() 18 | 19 | counter, exists := factoryInstances[apikey] 20 | if !exists { 21 | if len(factoryInstances) > 0 { 22 | logger.Warning("Factory Instantiation: You already have an instance of the Split factory. Make sure you definitely want " + 23 | "this additional instance. We recommend keeping only one instance of the factory at all times (Singleton pattern) and " + 24 | "reusing it throughout your application.") 25 | } 26 | factoryInstances[apikey] = 1 27 | } else { 28 | if counter == 1 { 29 | logger.Warning("Factory Instantiation: You already have 1 factory with this SDK Key. We recommend keeping only one instance of the factory " + 30 | "at all times (Singleton pattern) and reusing it throughout your application.") 31 | } else { 32 | logger.Warning(fmt.Sprintf("Factory Instantiation: You already have %d factories with this SDK Key.", counter) + 33 | " We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application.") 34 | } 35 | factoryInstances[apikey]++ 36 | } 37 | } 38 | 39 | // removeInstanceFromTracker decrease the instance of factory track 40 | func removeInstanceFromTracker(apikey string) { 41 | mutex.Lock() 42 | defer mutex.Unlock() 43 | 44 | counter, exists := factoryInstances[apikey] 45 | if exists { 46 | if counter == 1 { 47 | delete(factoryInstances, apikey) 48 | } else { 49 | factoryInstances[apikey]-- 50 | } 51 | } 52 | } 53 | 54 | // NewSplitFactory instantiates a new SplitFactory object. Accepts a SplitSdkConfig struct as an argument, 55 | // which will be used to instantiate both the client and the manager 56 | func NewSplitFactory(apikey string, cfg *conf.SplitSdkConfig) (*SplitFactory, error) { 57 | if cfg == nil { 58 | cfg = conf.Default() 59 | } 60 | 61 | logger := setupLogger(cfg) 62 | 63 | err := conf.Normalize(apikey, cfg) 64 | if err != nil { 65 | logger.Error(err.Error()) 66 | return nil, err 67 | } 68 | 69 | splitFactory, err := newFactory(apikey, *cfg, logger) 70 | return splitFactory, err 71 | } 72 | 73 | func getFactories() map[string]int64 { 74 | toReturn := make(map[string]int64) 75 | mutex.RLock() 76 | defer mutex.RUnlock() 77 | for k, v := range factoryInstances { 78 | toReturn[k] = v 79 | } 80 | return toReturn 81 | } 82 | -------------------------------------------------------------------------------- /splitio/client/factory_tracker_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-client/v6/splitio/conf" 7 | "github.com/splitio/go-toolkit/v5/logging" 8 | ) 9 | 10 | func TestFactoryTrackerMultipleInstantiation(t *testing.T) { 11 | var mockWriter MockWriter 12 | sdkConf := conf.Default() 13 | sdkConf.Logger = logging.NewLogger(&logging.LoggerOptions{ 14 | LogLevel: logging.LevelAll, 15 | ErrorWriter: &mockWriter, 16 | WarningWriter: &mockWriter, 17 | InfoWriter: &mockWriter, 18 | DebugWriter: &mockWriter, 19 | VerboseWriter: &mockWriter, 20 | }) 21 | sdkConf.SplitFile = "../../testdata/splits.yaml" 22 | 23 | removeInstanceFromTracker(conf.Localhost) 24 | removeInstanceFromTracker("something") 25 | 26 | factory, _ := NewSplitFactory(conf.Localhost, sdkConf) 27 | client := factory.Client() 28 | 29 | if factoryInstances[conf.Localhost] != 1 { 30 | t.Error("It should be 1") 31 | } 32 | 33 | factory2, _ := NewSplitFactory(conf.Localhost, sdkConf) 34 | _ = factory2.Client() 35 | 36 | if factoryInstances[conf.Localhost] != 2 { 37 | t.Error("It should be 2") 38 | } 39 | expected := "Factory Instantiation: You already have 1 factory with this SDK Key. We recommend keeping only one " + 40 | "instance of the factory at all times (Singleton pattern) and reusing it throughout your application." 41 | if !mockWriter.Matches(expected) { 42 | t.Error("Error is distinct from the expected one") 43 | } 44 | 45 | factory4, _ := NewSplitFactory("asdadd", sdkConf) 46 | client2 := factory4.Client() 47 | expected = "Factory Instantiation: You already have an instance of the Split factory. Make sure you definitely want " + 48 | "this additional instance. We recommend keeping only one instance of the factory at all times (Singleton pattern) and " + 49 | "reusing it throughout your application." 50 | if !mockWriter.Matches(expected) { 51 | t.Error("Error is distinct from the expected one") 52 | } 53 | 54 | client.Destroy() 55 | 56 | if factoryInstances[conf.Localhost] != 1 { 57 | t.Error("It should be 1") 58 | } 59 | 60 | if factoryInstances["asdadd"] != 1 { 61 | t.Error("It should be 1") 62 | } 63 | 64 | client.Destroy() 65 | 66 | if factoryInstances[conf.Localhost] != 1 { 67 | t.Error("It should be 1") 68 | } 69 | 70 | client2.Destroy() 71 | 72 | _, exist := factoryInstances["asdadd"] 73 | if exist { 74 | t.Error("It should not exist") 75 | } 76 | 77 | factory3, _ := NewSplitFactory(conf.Localhost, sdkConf) 78 | _ = factory3.Client() 79 | expected = "Factory Instantiation: You already have 1 factory with this SDK Key. We recommend keeping only one " + 80 | "instance of the factory at all times (Singleton pattern) and reusing it throughout your application." 81 | if !mockWriter.Matches(expected) { 82 | t.Error("Error is distinct from the expected one") 83 | } 84 | 85 | if factoryInstances[conf.Localhost] != 2 { 86 | t.Error("It should be 2") 87 | } 88 | 89 | factory5, _ := NewSplitFactory(conf.Localhost, sdkConf) 90 | _ = factory5.Client() 91 | expected = "Factory Instantiation: You already have 2 factories with this SDK Key. We recommend keeping only one " + 92 | "instance of the factory at all times (Singleton pattern) and reusing it throughout your application." 93 | if !mockWriter.Matches(expected) { 94 | t.Error("Error is distinct from the expected one") 95 | } 96 | if factoryInstances[conf.Localhost] != 3 { 97 | t.Error("It should be 3") 98 | } 99 | 100 | removeInstanceFromTracker(conf.Localhost) 101 | removeInstanceFromTracker("asdadd") 102 | } 103 | -------------------------------------------------------------------------------- /splitio/client/input_validator.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/splitio/go-split-commons/v6/engine/evaluator/impressionlabels" 12 | "github.com/splitio/go-split-commons/v6/storage" 13 | "github.com/splitio/go-toolkit/v5/datastructures/set" 14 | "github.com/splitio/go-toolkit/v5/logging" 15 | ) 16 | 17 | // InputValidation struct is responsible for cheking any input of treatment and 18 | // track methods. 19 | 20 | // MaxLength constant to check the length of the feature flags 21 | const MaxLength = 250 22 | 23 | // MaxEventLength constant to limit the event size 24 | const MaxEventLength = 32768 25 | 26 | // RegExpEventType constant that EventType must match 27 | const RegExpEventType = "^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$" 28 | 29 | type inputValidation struct { 30 | logger logging.LoggerInterface 31 | splitStorage storage.SplitStorageConsumer 32 | } 33 | 34 | func parseIfNumeric(value interface{}, operation string) (string, error) { 35 | f, float := value.(float64) 36 | i, integer := value.(int) 37 | i32, integer32 := value.(int32) 38 | i64, integer64 := value.(int64) 39 | 40 | if float { 41 | if math.IsNaN(f) || math.IsInf(f, -1) || math.IsInf(f, 1) || math.IsInf(f, 0) { 42 | return "", errors.New(operation + ": you passed an invalid key, key must be a non-empty string") 43 | } 44 | return strconv.FormatFloat(f, 'f', -1, 64), nil 45 | } 46 | if integer { 47 | return strconv.Itoa(i), nil 48 | } 49 | if integer32 { 50 | return strconv.FormatInt(int64(i32), 10), nil 51 | } 52 | if integer64 { 53 | return strconv.FormatInt(i64, 10), nil 54 | } 55 | return "", errors.New(operation + ": you passed an invalid key, key must be a non-empty string") 56 | } 57 | 58 | func (i *inputValidation) checkWhitespaces(value string, operation string) string { 59 | trimmed := strings.TrimSpace(value) 60 | if strings.TrimSpace(value) != value { 61 | i.logger.Warning(fmt.Sprintf(operation+": featureFlagName '%s' has extra whitespace, trimming", value)) 62 | } 63 | return trimmed 64 | } 65 | 66 | func checkIsEmptyString(value string, name string, typeName string, operation string) error { 67 | if strings.TrimSpace(value) == "" { 68 | return errors.New(operation + ": you passed an empty " + name + ", " + typeName + " must be a non-empty string") 69 | } 70 | return nil 71 | } 72 | 73 | func checkIsNotValidLength(value string, name string, operation string) error { 74 | if len(value) > MaxLength { 75 | return errors.New(operation + ": " + name + " too long - must be " + strconv.Itoa(MaxLength) + " characters or less") 76 | } 77 | return nil 78 | } 79 | 80 | func checkIsValidString(value string, name string, typeName string, operation string) error { 81 | err := checkIsEmptyString(value, name, typeName, operation) 82 | if err != nil { 83 | return err 84 | } 85 | return checkIsNotValidLength(value, name, operation) 86 | } 87 | 88 | func checkValidKeyObject(matchingKey string, bucketingKey *string, operation string) (string, *string, error) { 89 | if bucketingKey == nil { 90 | return "", nil, errors.New(operation + ": you passed a nil bucketingKey, bucketingKey must be a non-empty string") 91 | } 92 | 93 | err := checkIsValidString(matchingKey, "matchingKey", "matchingKey", operation) 94 | if err != nil { 95 | return "", nil, err 96 | } 97 | 98 | err = checkIsValidString(*bucketingKey, "bucketingKey", "bucketingKey", operation) 99 | if err != nil { 100 | return "", nil, err 101 | } 102 | 103 | return matchingKey, bucketingKey, nil 104 | } 105 | 106 | // ValidateTreatmentKey implements the validation for Treatment call 107 | func (i *inputValidation) ValidateTreatmentKey(key interface{}, operation string) (string, *string, error) { 108 | if key == nil { 109 | return "", nil, errors.New(operation + ": you passed a nil key, key must be a non-empty string") 110 | } 111 | okey, ok := key.(*Key) 112 | if ok { 113 | return checkValidKeyObject(okey.MatchingKey, &okey.BucketingKey, operation) 114 | } 115 | var sMatchingKey string 116 | var err error 117 | sMatchingKey, ok = key.(string) 118 | if !ok { 119 | sMatchingKey, err = parseIfNumeric(key, operation) 120 | if err != nil { 121 | return "", nil, err 122 | } 123 | i.logger.Warning(fmt.Sprintf(operation+": key %s is not of type string, converting", key)) 124 | } 125 | err = checkIsValidString(sMatchingKey, "key", "key", operation) 126 | if err != nil { 127 | return "", nil, err 128 | } 129 | 130 | return sMatchingKey, nil, nil 131 | } 132 | 133 | // ValidateFeatureName implements the validation for FeatureFlagName 134 | func (i *inputValidation) ValidateFeatureName(featureFlagName string, operation string) (string, error) { 135 | err := checkIsEmptyString(featureFlagName, "featureFlagName", "flag name", operation) 136 | if err != nil { 137 | return "", err 138 | } 139 | return i.checkWhitespaces(featureFlagName, operation), nil 140 | } 141 | 142 | func checkEventType(eventType string) error { 143 | err := checkIsEmptyString(eventType, "event type", "event type", "Track") 144 | if err != nil { 145 | return err 146 | } 147 | var r = regexp.MustCompile(RegExpEventType) 148 | if !r.MatchString(eventType) { 149 | return errors.New("Track: you passed " + eventType + ", event name must adhere to " + 150 | "the regular expression " + RegExpEventType + ". This means an event " + 151 | "name must be alphanumeric, cannot be more than 80 characters long, and can " + 152 | "only include a dash, underscore, period, or colon as separators of " + 153 | "alphanumeric characters") 154 | } 155 | return nil 156 | } 157 | 158 | func (i *inputValidation) checkTrafficType(trafficType string, shouldValidateExistence bool) (string, error) { 159 | err := checkIsEmptyString(trafficType, "traffic type", "traffic type", "Track") 160 | if err != nil { 161 | return "", err 162 | } 163 | toLower := strings.ToLower(trafficType) 164 | if toLower != trafficType { 165 | i.logger.Warning("Track: traffic type should be all lowercase - converting string to lowercase") 166 | } 167 | if shouldValidateExistence && !i.splitStorage.TrafficTypeExists(toLower) { 168 | i.logger.Warning("Track: traffic type " + toLower + " does not have any corresponding feature flags in this environment, " + 169 | "make sure you’re tracking your events to a valid traffic type defined in the Split user interface") 170 | } 171 | return toLower, nil 172 | } 173 | 174 | func checkValue(value interface{}) error { 175 | if value == nil { 176 | return nil 177 | } 178 | 179 | _, float := value.(float64) 180 | _, integer := value.(int) 181 | _, integer32 := value.(int32) 182 | _, integer64 := value.(int64) 183 | 184 | if float || integer || integer32 || integer64 { 185 | return nil 186 | } 187 | return errors.New("Track: value must be a number") 188 | } 189 | 190 | // ValidateTrackInputs implements the validation for Track call 191 | func (i *inputValidation) ValidateTrackInputs( 192 | key string, 193 | trafficType string, 194 | eventType string, 195 | value interface{}, 196 | shouldValidateExistence bool, 197 | ) (string, string, string, interface{}, error) { 198 | err := checkIsValidString(key, "key", "key", "Track") 199 | if err != nil { 200 | return "", trafficType, eventType, value, err 201 | } 202 | 203 | err = checkEventType(eventType) 204 | if err != nil { 205 | return key, trafficType, "", value, err 206 | } 207 | 208 | trafficType, err = i.checkTrafficType(trafficType, shouldValidateExistence) 209 | if err != nil { 210 | return key, "", eventType, value, err 211 | } 212 | 213 | err = checkValue(value) 214 | if err != nil { 215 | return key, trafficType, eventType, nil, err 216 | } 217 | 218 | return key, trafficType, eventType, value, nil 219 | } 220 | 221 | // ValidateManagerInputs implements the validation for Track call 222 | func (i *inputValidation) ValidateManagerInputs(featureFlag string) error { 223 | return checkIsEmptyString(featureFlag, "featureFlagName", "flag name", "Split") 224 | } 225 | 226 | // ValidateFeatureNames implements the validation for Treatments call 227 | func (i *inputValidation) ValidateFeatureNames(featureFlags []string, operation string) ([]string, error) { 228 | var featureFlagsSet = set.NewSet() 229 | if len(featureFlags) == 0 { 230 | return []string{}, errors.New(operation + ": featureFlagNames must be a non-empty array") 231 | } 232 | for _, featureFlag := range featureFlags { 233 | f, err := i.ValidateFeatureName(featureFlag, operation) 234 | if err != nil { 235 | i.logger.Error(err.Error()) 236 | } else { 237 | featureFlagsSet.Add(f) 238 | } 239 | } 240 | if featureFlagsSet.IsEmpty() { 241 | return []string{}, errors.New(operation + ": featureFlagNames must be a non-empty array") 242 | } 243 | f := make([]string, featureFlagsSet.Size()) 244 | for i, v := range featureFlagsSet.List() { 245 | s, ok := v.(string) 246 | if ok { 247 | f[i] = s 248 | } 249 | } 250 | return f, nil 251 | } 252 | 253 | func (i *inputValidation) validateTrackProperties(properties map[string]interface{}) (map[string]interface{}, int, error) { 254 | if len(properties) == 0 { 255 | return nil, 0, nil 256 | } 257 | 258 | if len(properties) > 300 { 259 | i.logger.Warning("Track: Event has more than 300 properties. Some of them will be trimmed when processed") 260 | } 261 | 262 | processed := make(map[string]interface{}) 263 | size := 1024 // Average event size is ~750 bytes. Using 1kbyte as a starting point. 264 | for name, value := range properties { 265 | size += len(name) 266 | switch value := value.(type) { 267 | case int, int32, int64, uint, uint32, uint64, float32, float64, bool, nil: 268 | processed[name] = value 269 | case string: 270 | size += len(value) 271 | processed[name] = value 272 | default: 273 | i.logger.Warning("Property %s is of invalid type. Setting value to nil") 274 | processed[name] = nil 275 | } 276 | 277 | if size > MaxEventLength { 278 | i.logger.Error( 279 | "The maximum size allowed for the properties is 32kb. Event not queued", 280 | ) 281 | return nil, size, errors.New("The maximum size allowed for the properties is 32kb. Event not queued") 282 | } 283 | } 284 | return processed, size, nil 285 | } 286 | 287 | func (i *inputValidation) IsSplitFound(label string, featureFlag string, operation string) bool { 288 | if label == impressionlabels.SplitNotFound { 289 | i.logger.Warning(fmt.Sprintf(operation+": you passed %s that does not exist in this environment, please double check what feature flags exist in the Split user interface.", featureFlag)) 290 | return false 291 | } 292 | return true 293 | } 294 | -------------------------------------------------------------------------------- /splitio/client/input_validator_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "math" 9 | "math/rand" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "sync" 14 | "testing" 15 | "time" 16 | 17 | "github.com/splitio/go-client/v6/splitio/conf" 18 | commonsCfg "github.com/splitio/go-split-commons/v6/conf" 19 | "github.com/splitio/go-split-commons/v6/dtos" 20 | "github.com/splitio/go-split-commons/v6/flagsets" 21 | "github.com/splitio/go-split-commons/v6/healthcheck/application" 22 | "github.com/splitio/go-split-commons/v6/provisional" 23 | "github.com/splitio/go-split-commons/v6/provisional/strategy" 24 | "github.com/splitio/go-split-commons/v6/service/api" 25 | authMocks "github.com/splitio/go-split-commons/v6/service/mocks" 26 | "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexmap" 27 | "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexqueue" 28 | "github.com/splitio/go-split-commons/v6/storage/mocks" 29 | "github.com/splitio/go-split-commons/v6/storage/redis" 30 | "github.com/splitio/go-split-commons/v6/synchronizer" 31 | "github.com/splitio/go-toolkit/v5/logging" 32 | ) 33 | 34 | type MockWriter struct { 35 | mutex sync.RWMutex 36 | messages []string 37 | } 38 | 39 | func (m *MockWriter) Write(p []byte) (n int, err error) { 40 | m.mutex.Lock() 41 | defer m.mutex.Unlock() 42 | m.messages = append(m.messages, string(p[:])) 43 | return len(p), nil 44 | } 45 | 46 | func (m *MockWriter) Reset() { 47 | m.mutex.Lock() 48 | defer m.mutex.Unlock() 49 | m.messages = nil 50 | } 51 | 52 | func (m *MockWriter) Length() int { 53 | m.mutex.Lock() 54 | defer m.mutex.Unlock() 55 | return len(m.messages) 56 | } 57 | 58 | func (m *MockWriter) Matches(expected string) bool { 59 | m.mutex.Lock() 60 | defer m.mutex.Unlock() 61 | for _, msg := range m.messages { 62 | if strings.Contains(msg, expected) { 63 | m.messages = nil 64 | return true 65 | } 66 | } 67 | m.messages = nil 68 | return false 69 | } 70 | 71 | var mW MockWriter 72 | 73 | func getMockedLogger() logging.LoggerInterface { 74 | return logging.NewLogger(&logging.LoggerOptions{ 75 | LogLevel: logging.LevelInfo, 76 | ErrorWriter: &mW, 77 | WarningWriter: &mW, 78 | InfoWriter: &mW, 79 | DebugWriter: nil, 80 | VerboseWriter: nil, 81 | }) 82 | } 83 | 84 | func getClient() SplitClient { 85 | logger := getMockedLogger() 86 | cfg := conf.Default() 87 | telemetryMockedStorage := mocks.MockTelemetryStorage{ 88 | RecordImpressionsStatsCall: func(dataType int, count int64) {}, 89 | RecordLatencyCall: func(method string, latency time.Duration) {}, 90 | } 91 | 92 | impressionObserver, _ := strategy.NewImpressionObserver(500) 93 | impressionsCounter := strategy.NewImpressionsCounter() 94 | impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryMockedStorage, true) 95 | impressionManager := provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl) 96 | 97 | factory := &SplitFactory{cfg: cfg, impressionManager: impressionManager, 98 | storages: sdkStorages{ 99 | runtimeTelemetry: telemetryMockedStorage, 100 | initTelemetry: telemetryMockedStorage, 101 | evaluationTelemetry: telemetryMockedStorage, 102 | }} 103 | 104 | client := SplitClient{ 105 | evaluator: &mockEvaluator{}, 106 | impressions: mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 1), logger, telemetryMockedStorage), 107 | logger: logger, 108 | validator: inputValidation{ 109 | logger: logger, 110 | splitStorage: mocks.MockSplitStorage{ 111 | TrafficTypeExistsCall: func(trafficType string) bool { 112 | switch trafficType { 113 | case "trafictype": 114 | return true 115 | default: 116 | return false 117 | } 118 | }, 119 | }, 120 | }, 121 | events: mocks.MockEventStorage{ 122 | PushCall: func(event dtos.EventDTO, size int) error { return nil }, 123 | }, 124 | factory: factory, 125 | impressionManager: impressionManager, 126 | initTelemetry: telemetryMockedStorage, 127 | evaluationTelemetry: telemetryMockedStorage, 128 | runtimeTelemetry: telemetryMockedStorage, 129 | } 130 | factory.status.Store(sdkStatusReady) 131 | return client 132 | } 133 | 134 | func TestFactoryWithNilApiKey(t *testing.T) { 135 | cfg := conf.Default() 136 | cfg.Logger = getMockedLogger() 137 | _, err := NewSplitFactory("", cfg) 138 | 139 | if err == nil { 140 | t.Error("Should be error") 141 | } 142 | 143 | expected := "factory instantiation: you passed an empty SDK key, SDK key must be a non-empty string" 144 | if !mW.Matches(expected) { 145 | t.Error("Error is distinct from the expected one") 146 | } 147 | } 148 | 149 | func getLongKey() string { 150 | m := "" 151 | for n := 0; n <= 256; n++ { 152 | m += "m" 153 | } 154 | return m 155 | } 156 | 157 | func TestValidationEmpty(t *testing.T) { 158 | client := getClient() 159 | mW.Reset() 160 | expectedTreatment(client.Treatment("key", "feature", nil), "TreatmentA", t) 161 | if mW.Length() > 0 { 162 | t.Error("Wrong message") 163 | } 164 | mW.Reset() 165 | } 166 | 167 | func TestTreatmentValidatorOnKeys(t *testing.T) { 168 | client := getClient() 169 | // Nil 170 | expectedTreatment(client.Treatment(nil, "feature", nil), "control", t) 171 | if !mW.Matches("Treatment: you passed a nil key, key must be a non-empty string") { 172 | t.Error("Wrong message") 173 | } 174 | 175 | // Boolean 176 | expectedTreatment(client.Treatment(true, "feature", nil), "control", t) 177 | if !mW.Matches("Treatment: you passed an invalid key, key must be a non-empty string") { 178 | t.Error("Wrong message") 179 | } 180 | 181 | // Trimmed 182 | expectedTreatment(client.Treatment(" ", "feature", nil), "control", t) 183 | if !mW.Matches("Treatment: you passed an empty key, key must be a non-empty string") { 184 | t.Error("Wrong message") 185 | } 186 | 187 | // Long 188 | expectedTreatment(client.Treatment(getLongKey(), "feature", nil), "control", t) 189 | if !mW.Matches("Treatment: key too long - must be 250 characters or less") { 190 | t.Error("Wrong message") 191 | } 192 | 193 | // Int 194 | expectedTreatment(client.Treatment(123, "feature", nil), "TreatmentA", t) 195 | if !mW.Matches("Treatment: key %!s(int=123) is not of type string, converting") { 196 | t.Error("Wrong message") 197 | } 198 | 199 | // Int32 200 | expectedTreatment(client.Treatment(int32(123), "feature", nil), "TreatmentA", t) 201 | if !mW.Matches("Treatment: key %!s(int32=123) is not of type string, converting") { 202 | t.Error("Wrong message") 203 | } 204 | 205 | // Int 64 206 | expectedTreatment(client.Treatment(int64(123), "feature", nil), "TreatmentA", t) 207 | if !mW.Matches("Treatment: key %!s(int64=123) is not of type string, converting") { 208 | t.Error("Wrong message") 209 | } 210 | 211 | // Float 212 | expectedTreatment(client.Treatment(1.3, "feature", nil), "TreatmentA", t) 213 | if !mW.Matches("Treatment: key %!s(float64=1.3) is not of type string, converting") { 214 | t.Error("Wrong message") 215 | } 216 | 217 | // NaN 218 | expectedTreatment(client.Treatment(math.NaN, "feature", nil), "control", t) 219 | if !mW.Matches("Treatment: you passed an invalid key, key must be a non-empty string") { 220 | t.Error("Wrong message") 221 | } 222 | 223 | // Inf 224 | expectedTreatment(client.Treatment(math.Inf, "feature", nil), "control", t) 225 | if !mW.Matches("Treatment: you passed an invalid key, key must be a non-empty string") { 226 | t.Error("Wrong message") 227 | } 228 | } 229 | 230 | func getKey(matchingKey string, bucketingKey string) *Key { 231 | return &Key{ 232 | MatchingKey: matchingKey, 233 | BucketingKey: bucketingKey, 234 | } 235 | } 236 | 237 | func TestTreatmentValidatorWithKeyObject(t *testing.T) { 238 | client := getClient() 239 | // Empty 240 | expectedTreatment(client.Treatment(getKey("", "bucketing"), "feature", nil), "control", t) 241 | if !mW.Matches("Treatment: you passed an empty matchingKey, matchingKey must be a non-empty string") { 242 | t.Error("Wrong message") 243 | } 244 | 245 | // Long 246 | expectedTreatment(client.Treatment(getKey(getLongKey(), "bucketing"), "feature", nil), "control", t) 247 | if !mW.Matches("Treatment: matchingKey too long - must be 250 characters or less") { 248 | t.Error("Wrong message") 249 | } 250 | 251 | // Empty Bucketing 252 | expectedTreatment(client.Treatment(getKey("matching", ""), "feature", nil), "control", t) 253 | if !mW.Matches("Treatment: you passed an empty bucketingKey, bucketingKey must be a non-empty string") { 254 | t.Error("Wrong message") 255 | } 256 | 257 | // Long Bucketing 258 | expectedTreatment(client.Treatment(getKey("matching", getLongKey()), "feature", nil), "control", t) 259 | if !mW.Matches("Treatment: bucketingKey too long - must be 250 characters or less") { 260 | t.Error("Wrong message") 261 | } 262 | 263 | // Ok 264 | mW.Reset() 265 | expectedTreatment(client.Treatment(getKey("matching", "bucketing"), "feature", nil), "TreatmentA", t) 266 | if mW.Length() > 0 { 267 | t.Error("Wrong message") 268 | } 269 | mW.Reset() 270 | } 271 | 272 | func TestTreatmentValidatorOnFeatureName(t *testing.T) { 273 | client := getClient() 274 | // Empty 275 | expectedTreatment(client.Treatment("key", "", nil), "control", t) 276 | if !mW.Matches("Treatment: you passed an empty featureFlagName, flag name must be a non-empty string") { 277 | t.Error("Wrong message") 278 | } 279 | 280 | // Trimmed 281 | expectedTreatment(client.Treatment("key", " feature ", nil), "TreatmentA", t) 282 | if !mW.Matches("Treatment: featureFlagName ' feature ' has extra whitespace, trimming") { 283 | t.Error("Wrong message") 284 | } 285 | 286 | // Non Existent 287 | expectedTreatment(client.Treatment("key", "feature_non_existent", nil), "control", t) 288 | if !mW.Matches("Treatment: you passed feature_non_existent that does not exist in this environment, please double check what feature flags exist in the Split user interface") { 289 | t.Error("Wrong message") 290 | } 291 | 292 | // Non Existent 293 | expectedTreatmentAndConfig(client.TreatmentWithConfig("key", "feature_non_existent", nil), "control", "", t) 294 | if !mW.Matches("TreatmentWithConfig: you passed feature_non_existent that does not exist in this environment, please double check what feature flags exist in the Split user interface") { 295 | t.Error("Wrong message") 296 | } 297 | } 298 | 299 | func expectedTreatments(key interface{}, features []string, length int, t *testing.T) map[string]string { 300 | client := getClient() 301 | result := client.Treatments(key, features, nil) 302 | if len(result) != length { 303 | t.Error("Wrong len of elements") 304 | } 305 | return result 306 | } 307 | 308 | func TestTreatmentsValidator(t *testing.T) { 309 | client := getClient() 310 | // Empty features 311 | expectedTreatments("key", []string{""}, 0, t) 312 | if !mW.Matches("Treatments: featureFlagNames must be a non-empty array") { 313 | t.Error("Wrong message") 314 | } 315 | 316 | // Inf 317 | result := expectedTreatments(math.Inf, []string{"feature"}, 1, t) 318 | expectedTreatment(result["feature"], "control", t) 319 | if !mW.Matches("Treatments: you passed an invalid key, key must be a non-empty string") { 320 | t.Error("Wrong message") 321 | } 322 | 323 | // Float 324 | result = expectedTreatments(1.3, []string{"feature"}, 1, t) 325 | expectedTreatment(result["feature"], "TreatmentA", t) 326 | if !mW.Matches("Treatments: key %!s(float64=1.3) is not of type string, converting") { 327 | t.Error("Wrong message") 328 | } 329 | 330 | // Trimmed 331 | result = expectedTreatments("key", []string{" some_feature "}, 1, t) 332 | expectedTreatment(result["some_feature"], "control", t) 333 | if !mW.Matches("Treatments: featureFlagName ' some_feature ' has extra whitespace, trimming") { 334 | t.Error("Wrong message") 335 | } 336 | 337 | // Non Existent 338 | result = expectedTreatments("key", []string{"feature_non_existent"}, 1, t) 339 | expectedTreatment(result["feature_non_existent"], "control", t) 340 | if !mW.Matches("Treatments: you passed feature_non_existent that does not exist in this environment, please double check what feature flags exist in the Split user interface") { 341 | t.Error("Wrong message") 342 | } 343 | 344 | // Non Existent Config 345 | resultWithConfig := client.TreatmentsWithConfig("key", []string{"feature_non_existent"}, nil) 346 | expectedTreatmentAndConfig(resultWithConfig["feature_non_existent"], "control", "", t) 347 | if !mW.Matches("TreatmentsWithConfig: you passed feature_non_existent that does not exist in this environment, please double check what feature flags exist in the Split user interface") { 348 | t.Error("Wrong message") 349 | } 350 | } 351 | 352 | func TestValidatorOnDestroy(t *testing.T) { 353 | telemetryMockedStorage := mocks.MockTelemetryStorage{ 354 | RecordSessionLengthCall: func(session int64) {}, 355 | } 356 | logger := getMockedLogger() 357 | localConfig := &synchronizer.LocalConfig{RefreshEnabled: false} 358 | sync, _ := synchronizer.NewSynchronizerManager( 359 | synchronizer.NewLocal(localConfig, &api.SplitAPI{}, mocks.MockSplitStorage{}, mocks.MockSegmentStorage{}, logger, telemetryMockedStorage, &application.Dummy{}), 360 | logger, 361 | commonsCfg.AdvancedConfig{}, 362 | authMocks.MockAuthClient{}, 363 | mocks.MockSplitStorage{}, 364 | make(chan int, 1), 365 | telemetryMockedStorage, 366 | dtos.Metadata{}, 367 | nil, 368 | &application.Dummy{}, 369 | ) 370 | factory := &SplitFactory{ 371 | cfg: conf.Default(), 372 | syncManager: sync, 373 | storages: sdkStorages{ 374 | initTelemetry: telemetryMockedStorage, 375 | runtimeTelemetry: telemetryMockedStorage, 376 | evaluationTelemetry: telemetryMockedStorage, 377 | }, 378 | } 379 | factory.status.Store(sdkStatusReady) 380 | var client2 = SplitClient{ 381 | evaluator: &mockEvaluator{}, 382 | impressions: mutexqueue.NewMQImpressionsStorage(5000, make(chan string, 1), logger, telemetryMockedStorage), 383 | logger: logger, 384 | validator: inputValidation{logger: logger}, 385 | factory: factory, 386 | initTelemetry: telemetryMockedStorage, 387 | evaluationTelemetry: telemetryMockedStorage, 388 | runtimeTelemetry: telemetryMockedStorage, 389 | } 390 | 391 | var manager = SplitManager{ 392 | logger: logger, 393 | validator: inputValidation{logger: logger}, 394 | factory: factory, 395 | initTelemetry: telemetryMockedStorage, 396 | } 397 | 398 | client2.Destroy() 399 | 400 | expectedTreatment(client2.Treatment("key", " feature ", nil), "control", t) 401 | if !mW.Matches("Client has already been destroyed - no calls possible") { 402 | t.Error("Wrong message") 403 | } 404 | 405 | result := client2.Treatments("key", []string{"some_feature"}, nil) 406 | expectedTreatment(result["some_feature"], "control", t) 407 | if !mW.Matches("Client has already been destroyed - no calls possible") { 408 | t.Error("Wrong message") 409 | } 410 | 411 | expectedTrack(client2.Track("key", "trafficType", "eventType", 0, nil), "Client has already been destroyed - no calls possible", t) 412 | 413 | manager.Split("feature") 414 | if !mW.Matches("Client has already been destroyed - no calls possible") { 415 | t.Error("Wrong message") 416 | } 417 | } 418 | 419 | func expectedTrack(err error, expected string, t *testing.T) { 420 | if err != nil && err.Error() != expected { 421 | t.Error("Wrong error", err.Error()) 422 | } 423 | if !mW.Matches(expected) { 424 | t.Error("Wrong message") 425 | } 426 | } 427 | 428 | func makeBigString(length int) string { 429 | letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 430 | asRuneSlice := make([]rune, length) 431 | for index := range asRuneSlice { 432 | asRuneSlice[index] = letterRunes[rand.Intn(len(letterRunes))] 433 | } 434 | return string(asRuneSlice) 435 | } 436 | 437 | func TestTrackValidators(t *testing.T) { 438 | client := getClient() 439 | // Empty key 440 | expectedTrack(client.Track("", "trafficType", "eventType", nil, nil), "Track: you passed an empty key, key must be a non-empty string", t) 441 | 442 | // Long key 443 | expectedTrack(client.Track(getLongKey(), "trafficType", "eventType", nil, nil), "Track: key too long - must be 250 characters or less", t) 444 | 445 | // Empty event type 446 | expectedTrack(client.Track("key", "trafficType", "", nil, nil), "Track: you passed an empty event type, event type must be a non-empty string", t) 447 | 448 | // Not match regex 449 | expected := "Track: you passed //, event name must adhere to " + 450 | "the regular expression ^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$. This means an event " + 451 | "name must be alphanumeric, cannot be more than 80 characters long, and can " + 452 | "only include a dash, underscore, period, or colon as separators of " + 453 | "alphanumeric characters" 454 | expectedTrack(client.Track("key", "trafficType", "//", nil, nil), expected, t) 455 | 456 | // Empty traffic type 457 | expectedTrack(client.Track("key", "", "eventType", nil, nil), "Track: you passed an empty traffic type, traffic type must be a non-empty string", t) 458 | 459 | // Not matching traffic type 460 | expected = "Track: traffic type traffic does not have any corresponding feature flags in this environment, make sure you’re tracking your events to a valid traffic type defined in the Split user interface" 461 | expectedTrack(client.Track("key", "traffic", "eventType", nil, nil), expected, t) 462 | 463 | // Uppercase traffic type 464 | expectedTrack(client.Track("key", "traficTYPE", "eventType", nil, nil), "Track: traffic type should be all lowercase - converting string to lowercase", t) 465 | 466 | // Traffic Type No Ocurrences 467 | err := client.Track("key", "trafficTypeNoOcurrences", "eventType", nil, nil) 468 | if !mW.Matches("Track: traffic type traffictypenoocurrences does not have any corresponding feature flags in this environment, make sure you’re tracking your events to a valid traffic type defined in the Split user interface") { 469 | t.Error("Wrong message") 470 | } 471 | if err != nil { 472 | t.Error("Should not be error") 473 | } 474 | 475 | // Value 476 | expectedTrack(client.Track("key", "traffic", "eventType", true, nil), "Track: value must be a number", t) 477 | 478 | // Properties 479 | props := make(map[string]interface{}) 480 | for i := 0; i < 301; i++ { 481 | props[fmt.Sprintf("prop-%d", i)] = "asd" 482 | } 483 | expectedTrack(client.Track("key", "traffic", "eventType", 1, props), "Track: Event has more than 300 properties. Some of them will be trimmed when processed", t) 484 | 485 | // Properties > 32kb 486 | props2 := make(map[string]interface{}) 487 | for i := 0; i < 299; i++ { 488 | props2[fmt.Sprintf("%s%d", makeBigString(255), i)] = makeBigString(255) 489 | } 490 | expectedTrack(client.Track("key", "traffic", "eventType", nil, props2), "The maximum size allowed for the properties is 32kb. Event not queued", t) 491 | 492 | // Ok 493 | err = client.Track("key", "traffic", "eventType", 1, nil) 494 | 495 | if err != nil { 496 | t.Error("Should not return error") 497 | } 498 | } 499 | 500 | func TestLocalhostTrafficType(t *testing.T) { 501 | sdkConf := conf.Default() 502 | sdkConf.SplitFile = "../../testdata/splits.yaml" 503 | factory, _ := NewSplitFactory(conf.Localhost, sdkConf) 504 | client := factory.Client() 505 | 506 | _ = client.BlockUntilReady(1) 507 | 508 | factory.status.Store(sdkStatusInitializing) 509 | 510 | if client.isReady() { 511 | t.Error("Localhost should not be ready") 512 | } 513 | 514 | err := client.Track("key", "traffic", "eventType", nil, nil) 515 | 516 | if err != nil { 517 | t.Error("It should not inform any err") 518 | } 519 | 520 | mW.Reset() 521 | if mW.Length() > 0 { 522 | t.Error("Wrong message") 523 | } 524 | mW.Reset() 525 | } 526 | 527 | func TestInMemoryFactoryFlagSets(t *testing.T) { 528 | var splitsMock, _ = ioutil.ReadFile("../../testdata/splits_mock.json") 529 | var splitMock, _ = ioutil.ReadFile("../../testdata/split_mock.json") 530 | 531 | postChannel := make(chan string, 1) 532 | 533 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 534 | switch r.URL.Path { 535 | case "/splitChanges": 536 | if r.RequestURI != "/splitChanges?s=1.1&since=-1&sets=a%2Cc%2Cd" { 537 | t.Error("wrong RequestURI for flag sets") 538 | } 539 | fmt.Fprintln(w, fmt.Sprintf(string(splitsMock), splitMock)) 540 | return 541 | case "/segmentChanges/___TEST___": 542 | w.Header().Add("Content-Encoding", "gzip") 543 | gzw := gzip.NewWriter(w) 544 | defer gzw.Close() 545 | fmt.Fprintln(gzw, "Hello, client") 546 | return 547 | case "/testImpressions/bulk": 548 | case "/events/bulk": 549 | for header := range r.Header { 550 | if (header == "SplitSDKMachineIP") || (header == "SplitSDKMachineName") { 551 | t.Error("Should not insert one of SplitSDKMachineIP, SplitSDKMachineName") 552 | } 553 | } 554 | 555 | rBody, _ := ioutil.ReadAll(r.Body) 556 | var dataInPost []map[string]interface{} 557 | err := json.Unmarshal(rBody, &dataInPost) 558 | if err != nil { 559 | t.Error(err) 560 | return 561 | } 562 | 563 | if len(dataInPost) < 1 { 564 | t.Error("It should send data") 565 | } 566 | fmt.Fprintln(w, "ok") 567 | postChannel <- "finished" 568 | case "/segmentChanges": 569 | case "/metrics/config": 570 | rBody, _ := ioutil.ReadAll(r.Body) 571 | var dataInPost dtos.Config 572 | err := json.Unmarshal(rBody, &dataInPost) 573 | if err != nil { 574 | t.Error(err) 575 | return 576 | } 577 | if dataInPost.FlagSetsInvalid != 4 { 578 | t.Error("invalid flag sets should be 4") 579 | } 580 | if dataInPost.FlagSetsTotal != 7 { 581 | t.Error("total flag sets should be 7") 582 | } 583 | default: 584 | fmt.Fprintln(w, "ok") 585 | return 586 | } 587 | })) 588 | defer ts.Close() 589 | cfg := conf.Default() 590 | cfg.LabelsEnabled = true 591 | cfg.IPAddressesEnabled = true 592 | cfg.Advanced.EventsURL = ts.URL 593 | cfg.Advanced.SdkURL = ts.URL 594 | cfg.Advanced.TelemetryServiceURL = ts.URL 595 | cfg.Advanced.AuthServiceURL = ts.URL 596 | cfg.Advanced.ImpressionListener = &ImpressionListenerTest{} 597 | cfg.TaskPeriods.ImpressionSync = 60 598 | cfg.TaskPeriods.EventsSync = 60 599 | cfg.Advanced.StreamingEnabled = false 600 | cfg.Advanced.FlagSetsFilter = []string{"a", "_b", "a", "a", "c", "d", "_d"} 601 | 602 | factory, _ := NewSplitFactory("test", cfg) 603 | client := factory.Client() 604 | errBlock := client.BlockUntilReady(15) 605 | 606 | if errBlock != nil { 607 | t.Error("client should be ready") 608 | } 609 | 610 | if !client.isReady() { 611 | t.Error("InMemory should be ready") 612 | } 613 | 614 | mW.Reset() 615 | if mW.Length() > 0 { 616 | t.Error("Wrong message") 617 | } 618 | mW.Reset() 619 | 620 | client.Destroy() 621 | } 622 | 623 | func TestConsumerFactoryFlagSets(t *testing.T) { 624 | logger := getMockedLogger() 625 | sdkConf := conf.Default() 626 | sdkConf.OperationMode = conf.RedisConsumer 627 | sdkConf.Advanced.FlagSetsFilter = []string{"a", "b"} 628 | sdkConf.Logger = logger 629 | 630 | factory, _ := NewSplitFactory("something", sdkConf) 631 | if !mW.Matches("FlagSets filter is not applicable for Consumer modes where the SDK does not keep rollout data in sync. FlagSet filter was discarded") { 632 | t.Error("Wrong message") 633 | } 634 | if !factory.IsReady() { 635 | t.Error("Factory should be ready immediately") 636 | } 637 | client := factory.Client() 638 | if !client.factory.IsReady() { 639 | t.Error("Client should be ready immediately") 640 | } 641 | 642 | err := client.BlockUntilReady(1) 643 | if err != nil { 644 | t.Error("Error was not expected") 645 | } 646 | 647 | manager := factory.Manager() 648 | if !manager.factory.IsReady() { 649 | t.Error("Manager should be ready immediately") 650 | } 651 | err = manager.BlockUntilReady(1) 652 | if err != nil { 653 | t.Error("Error was not expected") 654 | } 655 | 656 | prefixedClient, _ := redis.NewRedisClient(&commonsCfg.RedisConfig{ 657 | Host: "localhost", 658 | Port: 6379, 659 | Password: "", 660 | Prefix: "", 661 | }, logging.NewLogger(&logging.LoggerOptions{})) 662 | deleteDataGenerated(prefixedClient) 663 | 664 | client.Destroy() 665 | } 666 | 667 | func TestNotReadyYet(t *testing.T) { 668 | nonReadyUsages := 0 669 | logger := getMockedLogger() 670 | telemetryStorage := mocks.MockTelemetryStorage{ 671 | RecordNonReadyUsageCall: func() { 672 | nonReadyUsages++ 673 | }, 674 | RecordExceptionCall: func(method string) {}, 675 | } 676 | factoryNotReady := &SplitFactory{} 677 | clientNotReady := SplitClient{ 678 | evaluator: &mockEvaluator{}, 679 | impressions: mutexqueue.NewMQImpressionsStorage(5000, make(chan string, 1), logger, mocks.MockTelemetryStorage{}), 680 | logger: logger, 681 | validator: inputValidation{ 682 | logger: logger, 683 | splitStorage: mocks.MockSplitStorage{}, 684 | }, 685 | events: mocks.MockEventStorage{ 686 | PushCall: func(event dtos.EventDTO, size int) error { return nil }, 687 | }, 688 | factory: factoryNotReady, 689 | initTelemetry: telemetryStorage, 690 | evaluationTelemetry: telemetryStorage, 691 | } 692 | flagSetFilter := flagsets.NewFlagSetFilter([]string{}) 693 | maganerNotReady := SplitManager{ 694 | initTelemetry: telemetryStorage, 695 | factory: factoryNotReady, 696 | logger: logger, 697 | splitStorage: mutexmap.NewMMSplitStorage(flagSetFilter), 698 | } 699 | 700 | factoryNotReady.status.Store(sdkStatusInitializing) 701 | 702 | expectedMessage := "{operation}: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method" 703 | expectedMessage1 := "{operation}: the SDK is not ready, results may be incorrect for feature flag feature. Make sure to wait for SDK readiness before using this method" 704 | expectedMessage2 := "{operation}: the SDK is not ready, results may be incorrect for feature flags feature, feature_2. Make sure to wait for SDK readiness before using this method" 705 | 706 | clientNotReady.Treatment("test", "feature", nil) 707 | if !mW.Matches(strings.Replace(expectedMessage1, "{operation}", "Treatment", 1)) { 708 | t.Error("Wrong message") 709 | } 710 | 711 | clientNotReady.Treatments("test", []string{"feature", "feature_2"}, nil) 712 | if !mW.Matches(strings.Replace(expectedMessage2, "{operation}", "Treatments", 1)) { 713 | t.Error("Wrong message") 714 | } 715 | 716 | clientNotReady.TreatmentWithConfig("test", "feature", nil) 717 | if !mW.Matches(strings.Replace(expectedMessage1, "{operation}", "TreatmentWithConfig", 1)) { 718 | t.Error("Wrong message") 719 | } 720 | 721 | clientNotReady.TreatmentsWithConfig("test", []string{"feature", "feature_2"}, nil) 722 | if !mW.Matches(strings.Replace(expectedMessage2, "{operation}", "TreatmentsWithConfig", 1)) { 723 | t.Error("Wrong message", mW.messages) 724 | } 725 | expected := "Track: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method" 726 | expectedTrack(clientNotReady.Track("key", "traffic", "eventType", nil, nil), expected, t) 727 | 728 | maganerNotReady.Split("feature") 729 | if !mW.Matches(strings.Replace(expectedMessage, "{operation}", "Split", 1)) { 730 | t.Error("Wrong message") 731 | } 732 | 733 | maganerNotReady.Splits() 734 | if !mW.Matches(strings.Replace(expectedMessage, "{operation}", "Splits", 1)) { 735 | t.Error("Wrong message") 736 | } 737 | 738 | maganerNotReady.SplitNames() 739 | if !mW.Matches(strings.Replace(expectedMessage, "{operation}", "SplitNames", 1)) { 740 | t.Error("Wrong message") 741 | } 742 | 743 | if nonReadyUsages != 8 { 744 | t.Error("It should track a non ready usage") 745 | } 746 | } 747 | 748 | func TestManagerWithEmptySplit(t *testing.T) { 749 | flagSetFilter := flagsets.NewFlagSetFilter([]string{}) 750 | splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) 751 | factory := SplitFactory{} 752 | manager := SplitManager{ 753 | splitStorage: splitStorage, 754 | logger: getMockedLogger(), 755 | } 756 | 757 | factory.status.Store(sdkStatusReady) 758 | manager.factory = &factory 759 | 760 | manager.Split("") 761 | if !mW.Matches("Split: you passed an empty featureFlagName, flag name must be a non-empty string") { 762 | t.Error("Wrong message") 763 | } 764 | 765 | manager.Split("non_existent") 766 | if !mW.Matches("Split: you passed non_existent that does not exist in this environment, please double check what feature flags exist in the Split user interface.") { 767 | t.Error("Wrong message") 768 | } 769 | } 770 | -------------------------------------------------------------------------------- /splitio/client/manager.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/splitio/go-split-commons/v6/dtos" 7 | "github.com/splitio/go-split-commons/v6/storage" 8 | "github.com/splitio/go-toolkit/v5/logging" 9 | ) 10 | 11 | // SplitManager provides information of the currently stored splits 12 | type SplitManager struct { 13 | splitStorage storage.SplitStorageConsumer 14 | validator inputValidation 15 | logger logging.LoggerInterface 16 | factory *SplitFactory 17 | initTelemetry storage.TelemetryConfigProducer 18 | } 19 | 20 | // SplitView is a partial representation of a currently stored split 21 | type SplitView struct { 22 | Name string `json:"name"` 23 | TrafficType string `json:"trafficType"` 24 | Killed bool `json:"killed"` 25 | Treatments []string `json:"treatments"` 26 | ChangeNumber int64 `json:"changeNumber"` 27 | Configs map[string]string `json:"configs"` 28 | DefaultTreatment string `json:"defaultTreatment"` 29 | Sets []string `json:"sets"` 30 | ImpressionsDisabled bool `json:"impressionsDisabled"` 31 | } 32 | 33 | func newSplitView(splitDto *dtos.SplitDTO) *SplitView { 34 | treatments := make([]string, 0) 35 | for _, condition := range splitDto.Conditions { 36 | for _, partition := range condition.Partitions { 37 | treatments = append(treatments, partition.Treatment) 38 | } 39 | } 40 | sets := []string{} 41 | if splitDto.Sets != nil { 42 | sets = splitDto.Sets 43 | } 44 | return &SplitView{ 45 | ChangeNumber: splitDto.ChangeNumber, 46 | Killed: splitDto.Killed, 47 | Name: splitDto.Name, 48 | TrafficType: splitDto.TrafficTypeName, 49 | Treatments: treatments, 50 | Configs: splitDto.Configurations, 51 | DefaultTreatment: splitDto.DefaultTreatment, 52 | Sets: sets, 53 | ImpressionsDisabled: splitDto.ImpressionsDisabled, 54 | } 55 | } 56 | 57 | // SplitNames returns a list with the name of all the currently stored feature flags 58 | func (m *SplitManager) SplitNames() []string { 59 | if m.isDestroyed() { 60 | m.logger.Error("Client has already been destroyed - no calls possible") 61 | return []string{} 62 | } 63 | 64 | if !m.isReady() { 65 | m.logger.Warning("SplitNames: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method") 66 | m.initTelemetry.RecordNonReadyUsage() 67 | } 68 | 69 | return m.splitStorage.SplitNames() 70 | } 71 | 72 | // Splits returns a list of a partial view of every currently stored feature flag 73 | func (m *SplitManager) Splits() []SplitView { 74 | if m.isDestroyed() { 75 | m.logger.Error("Client has already been destroyed - no calls possible") 76 | return []SplitView{} 77 | } 78 | 79 | if !m.isReady() { 80 | m.logger.Warning("Splits: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method") 81 | m.initTelemetry.RecordNonReadyUsage() 82 | } 83 | 84 | splitViews := make([]SplitView, 0) 85 | splits := m.splitStorage.All() 86 | for _, split := range splits { 87 | splitViews = append(splitViews, *newSplitView(&split)) 88 | } 89 | return splitViews 90 | } 91 | 92 | // Split returns a partial view of a particular feature flag 93 | func (m *SplitManager) Split(featureFlagName string) *SplitView { 94 | if m.isDestroyed() { 95 | m.logger.Error("Client has already been destroyed - no calls possible") 96 | return nil 97 | } 98 | 99 | if !m.isReady() { 100 | m.logger.Warning("Split: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method") 101 | m.initTelemetry.RecordNonReadyUsage() 102 | } 103 | 104 | err := m.validator.ValidateManagerInputs(featureFlagName) 105 | if err != nil { 106 | m.logger.Error(err.Error()) 107 | return nil 108 | } 109 | 110 | split := m.splitStorage.Split(featureFlagName) 111 | if split != nil { 112 | return newSplitView(split) 113 | } 114 | m.logger.Error(fmt.Sprintf("Split: you passed %s that does not exist in this environment, please double check what feature flags exist in the Split user interface.", featureFlagName)) 115 | return nil 116 | } 117 | 118 | // BlockUntilReady Calls BlockUntilReady on factory to block manager on readiness 119 | func (m *SplitManager) BlockUntilReady(timer int) error { 120 | return m.factory.BlockUntilReady(timer) 121 | } 122 | 123 | func (m *SplitManager) isDestroyed() bool { 124 | return m.factory.IsDestroyed() 125 | } 126 | 127 | func (m *SplitManager) isReady() bool { 128 | return m.factory.IsReady() 129 | } 130 | -------------------------------------------------------------------------------- /splitio/client/manager_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-split-commons/v6/dtos" 7 | "github.com/splitio/go-split-commons/v6/flagsets" 8 | "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexmap" 9 | "github.com/splitio/go-toolkit/v5/datastructures/set" 10 | "github.com/splitio/go-toolkit/v5/logging" 11 | ) 12 | 13 | func TestSplitManager(t *testing.T) { 14 | flagSetFilter := flagsets.NewFlagSetFilter([]string{}) 15 | splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) 16 | splitStorage.Update([]dtos.SplitDTO{ 17 | { 18 | ChangeNumber: 123, 19 | Name: "split1", 20 | Killed: false, 21 | TrafficTypeName: "tt1", 22 | Sets: []string{"set1", "set2"}, 23 | DefaultTreatment: "s1p1", 24 | Conditions: []dtos.ConditionDTO{ 25 | { 26 | Partitions: []dtos.PartitionDTO{ 27 | {Treatment: "s1p1"}, 28 | {Treatment: "s1p2"}, 29 | {Treatment: "s1p3"}, 30 | }, 31 | }, 32 | }, 33 | }, 34 | { 35 | ChangeNumber: 123, 36 | Name: "split2", 37 | Killed: true, 38 | TrafficTypeName: "tt2", 39 | Conditions: []dtos.ConditionDTO{ 40 | { 41 | Partitions: []dtos.PartitionDTO{ 42 | {Treatment: "s2p1"}, 43 | {Treatment: "s2p2"}, 44 | {Treatment: "s2p3"}, 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, nil, 123) 50 | 51 | logger := logging.NewLogger(nil) 52 | factory := SplitFactory{} 53 | manager := SplitManager{ 54 | splitStorage: splitStorage, 55 | validator: inputValidation{logger: logger}, 56 | logger: logger, 57 | factory: &factory, 58 | } 59 | 60 | factory.status.Store(sdkStatusReady) 61 | 62 | splitNames := manager.SplitNames() 63 | splitNameSet := set.NewSet(splitNames[0], splitNames[1]) 64 | if !splitNameSet.IsEqual(set.NewSet("split1", "split2")) { 65 | t.Error("Incorrect split names returned") 66 | } 67 | 68 | s1 := manager.Split("split1") 69 | if s1.Name != "split1" || s1.Killed || s1.TrafficType != "tt1" || s1.ChangeNumber != 123 { 70 | t.Error("Split 1 stored incorrectly") 71 | } 72 | if s1.Treatments[0] != "s1p1" && s1.Treatments[1] != "s1p2" && s1.Treatments[2] != "s1p3" { 73 | t.Error("Incorrect treatments for split 1") 74 | } 75 | 76 | if len(s1.Sets) != 2 { 77 | t.Error("split1 should have 2 sets") 78 | } 79 | 80 | if s1.DefaultTreatment != "s1p1" { 81 | t.Error("the default treatment for split1 should be s1p1") 82 | } 83 | 84 | if s1.ImpressionsDisabled { 85 | t.Error("track impressions for split1 should be false") 86 | } 87 | 88 | s2 := manager.Split("split2") 89 | if s2.Name != "split2" || !s2.Killed || s2.TrafficType != "tt2" || s2.ChangeNumber != 123 { 90 | t.Error("Split 2 stored incorrectly") 91 | } 92 | if s2.Treatments[0] != "s1p2" && s2.Treatments[1] != "s2p2" && s2.Treatments[2] != "s2p3" { 93 | t.Error("Incorrect treatments for split 2") 94 | } 95 | 96 | if s2.Sets == nil && len(s2.Sets) != 0 { 97 | t.Error("split2 sets should be empty array") 98 | } 99 | 100 | if s2.ImpressionsDisabled { 101 | t.Error("track impressions for split2 should be false") 102 | } 103 | 104 | all := manager.Splits() 105 | if len(all) != 2 { 106 | t.Error("Incorrect number of splits returned") 107 | } 108 | 109 | sx := manager.Split("split3492042") 110 | if sx != nil { 111 | t.Error("Nonexistent split should return nil") 112 | } 113 | } 114 | 115 | func TestSplitManagerWithConfigs(t *testing.T) { 116 | flagSetFilter := flagsets.NewFlagSetFilter([]string{}) 117 | splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) 118 | splitStorage.Update([]dtos.SplitDTO{*valid, *killed, *noConfig}, nil, 123) 119 | 120 | logger := logging.NewLogger(nil) 121 | factory := SplitFactory{} 122 | manager := SplitManager{ 123 | splitStorage: splitStorage, 124 | logger: logger, 125 | validator: inputValidation{logger: logger}, 126 | factory: &factory, 127 | } 128 | 129 | factory.status.Store(sdkStatusReady) 130 | manager.factory = &factory 131 | 132 | splitNames := manager.SplitNames() 133 | splitNameSet := set.NewSet(splitNames[0], splitNames[1], splitNames[2]) 134 | if !splitNameSet.IsEqual(set.NewSet("valid", "killed", "noConfig")) { 135 | t.Error("Incorrect split names returned") 136 | } 137 | 138 | s1 := manager.Split("valid") 139 | if s1.Name != "valid" || s1.Killed || s1.TrafficType != "user" || s1.ChangeNumber != 1494593336752 { 140 | t.Error("Split 1 stored incorrectly") 141 | } 142 | if s1.Treatments[0] != "on" { 143 | t.Error("Incorrect treatments for split 1") 144 | } 145 | if s1.Configs == nil { 146 | t.Error("It should have configs") 147 | } 148 | if s1.Configs["on"] != "{\"color\": \"blue\",\"size\": 13}" { 149 | t.Error("It should have configs") 150 | } 151 | if s1.DefaultTreatment != "off" { 152 | t.Error("the default treatment for valid should be off") 153 | } 154 | if s1.ImpressionsDisabled { 155 | t.Error("ImpressionsDisabled for valid should be false") 156 | } 157 | 158 | s2 := manager.Split("killed") 159 | if s2.Name != "killed" || !s2.Killed || s2.TrafficType != "user" || s2.ChangeNumber != 1494593336752 { 160 | t.Error("Split 2 stored incorrectly") 161 | } 162 | if s2.Treatments[0] != "off" { 163 | t.Error("Incorrect treatments for split 2") 164 | } 165 | if s2.Configs == nil { 166 | t.Error("It should have configs") 167 | } 168 | if s2.Configs["defTreatment"] != "{\"color\": \"orange\",\"size\": 15}" { 169 | t.Error("It should have configs") 170 | } 171 | if s2.DefaultTreatment != "defTreatment" { 172 | t.Error("the default treatment for killed should be defTreatment") 173 | } 174 | if s2.ImpressionsDisabled { 175 | t.Error("track impressions for killed should be false") 176 | } 177 | 178 | s3 := manager.Split("noConfig") 179 | if s3.Name != "noConfig" || s3.Killed || s3.TrafficType != "user" || s3.ChangeNumber != 1494593336752 { 180 | t.Error("Split 3 stored incorrectly") 181 | } 182 | if s3.Treatments[0] != "off" { 183 | t.Error("Incorrect treatments for split 3") 184 | } 185 | if s3.Configs != nil { 186 | t.Error("It should not have configs") 187 | } 188 | if s3.DefaultTreatment != "defTreatment" { 189 | t.Error("the default treatment for killed should be defTreatment") 190 | } 191 | if s3.ImpressionsDisabled { 192 | t.Error("track impressions for noConfig should be false") 193 | } 194 | 195 | all := manager.Splits() 196 | if len(all) != 3 { 197 | t.Error("Incorrect number of splits returned") 198 | } 199 | 200 | sx := manager.Split("split3492042") 201 | if sx != nil { 202 | t.Error("Nonexistent split should return nil") 203 | } 204 | } 205 | 206 | func TestSplitManagerTrackImpressions(t *testing.T) { 207 | flagSetFilter := flagsets.NewFlagSetFilter([]string{}) 208 | splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) 209 | valid.ImpressionsDisabled = true 210 | noConfig.ImpressionsDisabled = true 211 | splitStorage.Update([]dtos.SplitDTO{*valid, *killed, *noConfig}, nil, 123) 212 | 213 | logger := logging.NewLogger(nil) 214 | factory := SplitFactory{} 215 | manager := SplitManager{ 216 | splitStorage: splitStorage, 217 | logger: logger, 218 | validator: inputValidation{logger: logger}, 219 | factory: &factory, 220 | } 221 | 222 | factory.status.Store(sdkStatusReady) 223 | manager.factory = &factory 224 | 225 | s1 := manager.Split("valid") 226 | if !s1.ImpressionsDisabled { 227 | t.Error("track impressions for valid should be true") 228 | } 229 | 230 | s2 := manager.Split("killed") 231 | if s2.ImpressionsDisabled { 232 | t.Error("track impressions for killed should be false") 233 | } 234 | 235 | s3 := manager.Split("noConfig") 236 | if !s3.ImpressionsDisabled { 237 | t.Error("track impressions for noConfig should be true") 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /splitio/conf/defaults.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | const ( 4 | defaultHTTPTimeout = 30 5 | defaultTaskPeriod = 60 6 | defaultTelemetrySync = 3600 7 | defaultRedisHost = "localhost" 8 | defaultRedisPort = 6379 9 | defaultRedisDb = 0 10 | defaultSegmentQueueSize = 500 11 | defaultSegmentWorkers = 10 12 | defaultImpressionSyncOptimized = 300 13 | defaultImpressionSyncDebug = 60 14 | ) 15 | 16 | const ( 17 | minSplitSync = 5 18 | minSegmentSync = 30 19 | minImpressionSync = 1 20 | minImpressionSyncOptimized = 60 21 | minEventSync = 1 22 | minTelemetrySync = 30 23 | ) 24 | -------------------------------------------------------------------------------- /splitio/conf/sdkconf.go: -------------------------------------------------------------------------------- 1 | // Package conf contains configuration structures used to setup the SDK 2 | package conf 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "math" 8 | "os/user" 9 | "path" 10 | "strings" 11 | 12 | impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener" 13 | "github.com/splitio/go-split-commons/v6/conf" 14 | "github.com/splitio/go-toolkit/v5/datastructures/set" 15 | "github.com/splitio/go-toolkit/v5/logging" 16 | "github.com/splitio/go-toolkit/v5/nethelpers" 17 | ) 18 | 19 | const ( 20 | // RedisConsumer mode 21 | RedisConsumer = "redis-consumer" 22 | // Localhost mode 23 | Localhost = "localhost" 24 | // InMemoryStandAlone mode 25 | InMemoryStandAlone = "inmemory-standalone" 26 | ) 27 | 28 | // SplitSdkConfig struct ... 29 | // struct used to setup a Split.io SDK client. 30 | // 31 | // Parameters: 32 | // - OperationMode (Required) Must be one of ["inmemory-standalone", "redis-consumer"] 33 | // - InstanceName (Optional) Name to be used when submitting metrics & impressions to split servers 34 | // - IPAddress (Optional) Address to be used when submitting metrics & impressions to split servers 35 | // - BlockUntilReady (Optional) How much to wait until the sdk is ready 36 | // - SplitFile (Optional) File with splits to use when running in localhost mode 37 | // - SegmentDirectory (Optional) Path where all the segment files are located to use when running in json localhost mode 38 | // - LabelsEnabled (Optional) Can be used to disable labels if the user does not want to send that info to split servers. 39 | // - Logger: (Optional) Custom logger complying with logging.LoggerInterface 40 | // - LoggerConfig: (Optional) Options to setup the sdk's own logger 41 | // - TaskPeriods: (Optional) How often should each task run 42 | // - Redis: (Required for "redis-consumer". Sets up Redis config 43 | // - Advanced: (Optional) Sets up various advanced options for the sdk 44 | // - ImpressionsMode (Optional) Flag for enabling local impressions dedupe - Possible values <'optimized'|'debug'> 45 | // - LocalhostRefreshEnabled: (Optional) Flag to run synchronization refresh for Splits and Segments in localhost mode. 46 | type SplitSdkConfig struct { 47 | OperationMode string 48 | InstanceName string 49 | IPAddress string 50 | IPAddressesEnabled bool 51 | BlockUntilReady int 52 | SplitFile string 53 | SegmentDirectory string 54 | LabelsEnabled bool 55 | SplitSyncProxyURL string 56 | Logger logging.LoggerInterface 57 | LoggerConfig logging.LoggerOptions 58 | TaskPeriods TaskPeriods 59 | Advanced AdvancedConfig 60 | Redis conf.RedisConfig 61 | ImpressionsMode string 62 | LocalhostRefreshEnabled bool 63 | } 64 | 65 | // TaskPeriods struct is used to configure the period for each synchronization task 66 | type TaskPeriods struct { 67 | SplitSync int 68 | SegmentSync int 69 | ImpressionSync int 70 | GaugeSync int 71 | CounterSync int 72 | LatencySync int 73 | EventsSync int 74 | TelemetrySync int 75 | } 76 | 77 | // AdvancedConfig exposes more configurable parameters that can be used to further tailor the sdk to the user's needs 78 | // - ImpressionListener - struct that will be notified each time an impression bulk is ready 79 | // - HTTPTimeout - Timeout for HTTP requests when doing synchronization 80 | // - SegmentQueueSize - How many segments can be queued for updating (should be >= # segments the user has) 81 | // - SegmentWorkers - How many workers will be used when performing segments sync. 82 | type AdvancedConfig struct { 83 | ImpressionListener impressionlistener.ImpressionListener 84 | HTTPTimeout int 85 | SegmentQueueSize int 86 | SegmentWorkers int 87 | AuthServiceURL string 88 | SdkURL string 89 | EventsURL string 90 | StreamingServiceURL string 91 | TelemetryServiceURL string 92 | EventsBulkSize int64 93 | EventsQueueSize int 94 | ImpressionsQueueSize int 95 | ImpressionsBulkSize int64 96 | StreamingEnabled bool 97 | FlagSetsFilter []string 98 | } 99 | 100 | // Default returns a config struct with all the default values 101 | func Default() *SplitSdkConfig { 102 | instanceName := "unknown" 103 | ipAddress, err := nethelpers.ExternalIP() 104 | if err != nil { 105 | ipAddress = "unknown" 106 | } else { 107 | instanceName = fmt.Sprintf("ip-%s", strings.Replace(ipAddress, ".", "-", -1)) 108 | } 109 | 110 | var splitFile string 111 | usr, err := user.Current() 112 | if err != nil { 113 | splitFile = "splits" 114 | } else { 115 | splitFile = path.Join(usr.HomeDir, ".splits") 116 | } 117 | 118 | return &SplitSdkConfig{ 119 | OperationMode: InMemoryStandAlone, 120 | LabelsEnabled: true, 121 | IPAddress: ipAddress, 122 | IPAddressesEnabled: true, 123 | InstanceName: instanceName, 124 | Logger: nil, 125 | LoggerConfig: logging.LoggerOptions{}, 126 | SplitFile: splitFile, 127 | ImpressionsMode: conf.ImpressionsModeOptimized, 128 | LocalhostRefreshEnabled: false, 129 | Redis: conf.RedisConfig{ 130 | Database: 0, 131 | Host: "localhost", 132 | Password: "", 133 | Port: 6379, 134 | Prefix: "", 135 | }, 136 | TaskPeriods: TaskPeriods{ 137 | GaugeSync: defaultTelemetrySync, 138 | CounterSync: defaultTelemetrySync, 139 | LatencySync: defaultTelemetrySync, 140 | TelemetrySync: defaultTelemetrySync, 141 | ImpressionSync: defaultImpressionSyncOptimized, 142 | SegmentSync: defaultTaskPeriod, 143 | SplitSync: defaultTaskPeriod, 144 | EventsSync: defaultTaskPeriod, 145 | }, 146 | Advanced: AdvancedConfig{ 147 | AuthServiceURL: "", 148 | EventsURL: "", 149 | SdkURL: "", 150 | StreamingServiceURL: "", 151 | TelemetryServiceURL: "", 152 | HTTPTimeout: defaultHTTPTimeout, 153 | ImpressionListener: nil, 154 | SegmentQueueSize: 500, 155 | SegmentWorkers: 10, 156 | EventsBulkSize: 5000, 157 | EventsQueueSize: 10000, 158 | ImpressionsQueueSize: 10000, 159 | ImpressionsBulkSize: 5000, 160 | StreamingEnabled: true, 161 | }, 162 | } 163 | } 164 | 165 | func checkImpressionSync(cfg *SplitSdkConfig) error { 166 | if cfg.TaskPeriods.ImpressionSync == 0 { 167 | cfg.TaskPeriods.ImpressionSync = defaultImpressionSyncOptimized 168 | } else { 169 | if cfg.TaskPeriods.ImpressionSync < minImpressionSyncOptimized { 170 | return fmt.Errorf("ImpressionSync must be >= %d. Actual is: %d", minImpressionSyncOptimized, cfg.TaskPeriods.ImpressionSync) 171 | } 172 | cfg.TaskPeriods.ImpressionSync = int(math.Max(float64(minImpressionSyncOptimized), float64(cfg.TaskPeriods.ImpressionSync))) 173 | } 174 | return nil 175 | } 176 | 177 | func validConfigRates(cfg *SplitSdkConfig) error { 178 | if cfg.OperationMode == RedisConsumer { 179 | return nil 180 | } 181 | 182 | if cfg.TaskPeriods.SplitSync < minSplitSync { 183 | return fmt.Errorf("SplitSync must be >= %d. Actual is: %d", minSplitSync, cfg.TaskPeriods.SplitSync) 184 | } 185 | if cfg.TaskPeriods.SegmentSync < minSegmentSync { 186 | return fmt.Errorf("SegmentSync must be >= %d. Actual is: %d", minSegmentSync, cfg.TaskPeriods.SegmentSync) 187 | } 188 | 189 | cfg.ImpressionsMode = strings.ToLower(cfg.ImpressionsMode) 190 | switch cfg.ImpressionsMode { 191 | case conf.ImpressionsModeOptimized: 192 | err := checkImpressionSync(cfg) 193 | if err != nil { 194 | return err 195 | } 196 | case conf.ImpressionsModeDebug: 197 | if cfg.TaskPeriods.ImpressionSync == 0 { 198 | cfg.TaskPeriods.ImpressionSync = defaultImpressionSyncDebug 199 | } else { 200 | if cfg.TaskPeriods.ImpressionSync < minImpressionSync { 201 | return fmt.Errorf("ImpressionSync must be >= %d. Actual is: %d", minImpressionSync, cfg.TaskPeriods.ImpressionSync) 202 | } 203 | } 204 | case conf.ImpressionsModeNone: 205 | return nil 206 | default: 207 | fmt.Println(`You passed an invalid impressionsMode, impressionsMode should be one of the following values: 'debug', 'optimized' or 'none'. Defaulting to 'optimized' mode.`) 208 | cfg.ImpressionsMode = conf.ImpressionsModeOptimized 209 | err := checkImpressionSync(cfg) 210 | if err != nil { 211 | return err 212 | } 213 | } 214 | 215 | if cfg.TaskPeriods.EventsSync < minEventSync { 216 | return fmt.Errorf("EventsSync must be >= %d. Actual is: %d", minEventSync, cfg.TaskPeriods.EventsSync) 217 | } 218 | if cfg.TaskPeriods.TelemetrySync < minTelemetrySync { 219 | return fmt.Errorf("TelemetrySync must be >= %d. Actual is: %d", minTelemetrySync, cfg.TaskPeriods.TelemetrySync) 220 | } 221 | if cfg.Advanced.SegmentWorkers <= 0 { 222 | return errors.New("number of workers for fetching segments MUST be greater than zero") 223 | } 224 | return nil 225 | } 226 | 227 | // Normalize checks that the parameters passed by the user are correct and updates parameters if necessary. 228 | // returns an error if something is wrong 229 | func Normalize(apikey string, cfg *SplitSdkConfig) error { 230 | // Fail if no apikey is provided 231 | if apikey == "" && cfg.OperationMode != Localhost { 232 | return errors.New("factory instantiation: you passed an empty SDK key, SDK key must be a non-empty string") 233 | } 234 | 235 | // To keep the interface consistent with other sdks we accept "localhost" as an apikey, 236 | // which sets the operation mode to localhost 237 | if apikey == Localhost { 238 | cfg.OperationMode = Localhost 239 | } 240 | 241 | // Fail if an invalid operation-mode is provided 242 | operationModes := set.NewSet( 243 | Localhost, 244 | InMemoryStandAlone, 245 | RedisConsumer, 246 | ) 247 | 248 | if !operationModes.Has(cfg.OperationMode) { 249 | return fmt.Errorf("OperationMode parameter must be one of: %v", operationModes.List()) 250 | } 251 | 252 | if cfg.SplitSyncProxyURL != "" { 253 | cfg.Advanced.AuthServiceURL = cfg.SplitSyncProxyURL 254 | cfg.Advanced.SdkURL = cfg.SplitSyncProxyURL 255 | cfg.Advanced.EventsURL = cfg.SplitSyncProxyURL 256 | cfg.Advanced.StreamingServiceURL = cfg.SplitSyncProxyURL 257 | cfg.Advanced.TelemetryServiceURL = cfg.SplitSyncProxyURL 258 | } 259 | 260 | if !cfg.IPAddressesEnabled { 261 | cfg.IPAddress = "NA" 262 | cfg.InstanceName = "NA" 263 | } 264 | 265 | return validConfigRates(cfg) 266 | } 267 | -------------------------------------------------------------------------------- /splitio/conf/sdkconf_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-split-commons/v6/conf" 7 | ) 8 | 9 | func TestSdkConfNormalization(t *testing.T) { 10 | cfg := Default() 11 | cfg.OperationMode = "invalid_mode" 12 | err := Normalize("asd", cfg) 13 | 14 | if err == nil { 15 | t.Error("Should throw an error when setting an invalid operation mode") 16 | } 17 | 18 | cfg = Default() 19 | err = Normalize("", cfg) 20 | if err == nil { 21 | t.Error("Should throw an error if no apikey is passed and operation mode != \"localhost\"") 22 | } 23 | 24 | cfg.SplitSyncProxyURL = "http://some-proxy" 25 | err = Normalize("asd", cfg) 26 | if err != nil { 27 | t.Error("Should not return an error with proper parameters") 28 | } 29 | 30 | if cfg.Advanced.SdkURL != cfg.SplitSyncProxyURL || cfg.Advanced.EventsURL != cfg.SplitSyncProxyURL { 31 | t.Error("Sdk & Events URL should be updated when SplitSyncProxyURL is not empty") 32 | } 33 | 34 | cfg = Default() 35 | cfg.IPAddressesEnabled = false 36 | err = Normalize("asd", cfg) 37 | if err != nil || cfg.IPAddress != "NA" || cfg.InstanceName != "NA" { 38 | t.Error("Should be NA") 39 | } 40 | 41 | cfg = Default() 42 | err = Normalize("asd", cfg) 43 | if err != nil || cfg.IPAddress == "NA" || cfg.InstanceName == "NA" { 44 | t.Error("Should not be NA") 45 | } 46 | } 47 | 48 | func TestValidRates(t *testing.T) { 49 | cfg := Default() 50 | err := Normalize("asd", cfg) 51 | if err != nil { 52 | t.Error("It should not return err") 53 | } 54 | 55 | cfg.TaskPeriods.TelemetrySync = 0 56 | err = Normalize("asd", cfg) 57 | if err == nil || err.Error() != "TelemetrySync must be >= 30. Actual is: 0" { 58 | t.Error("It should return err") 59 | } 60 | 61 | cfg = Default() 62 | cfg.TaskPeriods.SplitSync = 4 63 | err = Normalize("asd", cfg) 64 | if err == nil || err.Error() != "SplitSync must be >= 5. Actual is: 4" { 65 | t.Error("It should return err") 66 | } 67 | 68 | cfg = Default() 69 | cfg.TaskPeriods.SegmentSync = 29 70 | err = Normalize("asd", cfg) 71 | if err == nil || err.Error() != "SegmentSync must be >= 30. Actual is: 29" { 72 | t.Error("It should return err") 73 | } 74 | 75 | cfg = Default() // Optimized by Default 76 | cfg.TaskPeriods.ImpressionSync = 59 77 | err = Normalize("asd", cfg) 78 | if err == nil || err.Error() != "ImpressionSync must be >= 60. Actual is: 59" { 79 | t.Error("It should return err") 80 | } 81 | 82 | cfg = Default() // Optimized by Default 83 | cfg.TaskPeriods.ImpressionSync = 75 84 | err = Normalize("asd", cfg) 85 | if err != nil || cfg.TaskPeriods.ImpressionSync != 75 { 86 | t.Error("It should match") 87 | } 88 | 89 | cfg = Default() // Debug 90 | cfg.TaskPeriods.ImpressionSync = -1 91 | cfg.ImpressionsMode = conf.ImpressionsModeDebug 92 | err = Normalize("asd", cfg) 93 | if err == nil || err.Error() != "ImpressionSync must be >= 1. Actual is: -1" { 94 | t.Error("It should return err") 95 | } 96 | 97 | cfg = Default() 98 | cfg.TaskPeriods.EventsSync = 0 99 | err = Normalize("asd", cfg) 100 | if err == nil || err.Error() != "EventsSync must be >= 1. Actual is: 0" { 101 | t.Error("It should return err") 102 | } 103 | 104 | cfg = Default() 105 | cfg.Advanced.SegmentWorkers = 0 106 | err = Normalize("asd", cfg) 107 | if err == nil || err.Error() != "number of workers for fetching segments MUST be greater than zero" { 108 | t.Error("It should return err") 109 | } 110 | 111 | cfg = Default() 112 | cfg.ImpressionsMode = "some" 113 | err = Normalize("asd", cfg) 114 | if err != nil || cfg.ImpressionsMode != conf.ImpressionsModeOptimized { 115 | t.Error("It should not return err") 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /splitio/conf/util.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/splitio/go-split-commons/v6/conf" 7 | "github.com/splitio/go-split-commons/v6/flagsets" 8 | ) 9 | 10 | // NormalizeSDKConf compares against SDK Config to set defaults 11 | func NormalizeSDKConf(sdkConfig AdvancedConfig) (conf.AdvancedConfig, []error) { 12 | config := conf.GetDefaultAdvancedConfig() 13 | if sdkConfig.HTTPTimeout > 0 { 14 | config.HTTPTimeout = sdkConfig.HTTPTimeout 15 | } 16 | if sdkConfig.EventsBulkSize > 0 { 17 | config.EventsBulkSize = sdkConfig.EventsBulkSize 18 | } 19 | if sdkConfig.EventsQueueSize > 0 { 20 | config.EventsQueueSize = sdkConfig.EventsQueueSize 21 | } 22 | if sdkConfig.ImpressionsBulkSize > 0 { 23 | config.ImpressionsBulkSize = sdkConfig.ImpressionsBulkSize 24 | } 25 | if sdkConfig.ImpressionsQueueSize > 0 { 26 | config.ImpressionsQueueSize = sdkConfig.ImpressionsQueueSize 27 | } 28 | if sdkConfig.SegmentQueueSize > 0 { 29 | config.SegmentQueueSize = sdkConfig.SegmentQueueSize 30 | } 31 | if sdkConfig.SegmentWorkers > 0 { 32 | config.SegmentWorkers = sdkConfig.SegmentWorkers 33 | } 34 | if strings.TrimSpace(sdkConfig.EventsURL) != "" { 35 | config.EventsURL = sdkConfig.EventsURL 36 | } 37 | if strings.TrimSpace(sdkConfig.SdkURL) != "" { 38 | config.SdkURL = sdkConfig.SdkURL 39 | } 40 | if strings.TrimSpace(sdkConfig.AuthServiceURL) != "" { 41 | config.AuthServiceURL = sdkConfig.AuthServiceURL 42 | } 43 | if strings.TrimSpace(sdkConfig.StreamingServiceURL) != "" { 44 | config.StreamingServiceURL = sdkConfig.StreamingServiceURL 45 | } 46 | if strings.TrimSpace(sdkConfig.TelemetryServiceURL) != "" { 47 | config.TelemetryServiceURL = sdkConfig.TelemetryServiceURL 48 | } 49 | config.StreamingEnabled = sdkConfig.StreamingEnabled 50 | 51 | flagSets, errs := flagsets.SanitizeMany(sdkConfig.FlagSetsFilter) 52 | config.FlagSetsFilter = flagSets 53 | return config, errs 54 | } 55 | -------------------------------------------------------------------------------- /splitio/impressionListener/impression_listener.go: -------------------------------------------------------------------------------- 1 | package impressionlistener 2 | 3 | // ImpressionListener declaration of ImpressionListener interface 4 | type ImpressionListener interface { 5 | LogImpression(data ILObject) 6 | } 7 | -------------------------------------------------------------------------------- /splitio/impressionListener/impressions_listener_wrapper.go: -------------------------------------------------------------------------------- 1 | package impressionlistener 2 | 3 | import ( 4 | "github.com/splitio/go-split-commons/v6/dtos" 5 | ) 6 | 7 | // ILObject struct to map entire data for listener 8 | type ILObject struct { 9 | Impression dtos.Impression 10 | Attributes map[string]interface{} 11 | InstanceID string 12 | SDKLanguageVersion string 13 | } 14 | 15 | // WrapperImpressionListener struct 16 | type WrapperImpressionListener struct { 17 | ImpressionListener ImpressionListener 18 | metadata dtos.Metadata 19 | } 20 | 21 | // NewImpressionListenerWrapper instantiates a new ImpressionListenerWrapper 22 | func NewImpressionListenerWrapper(impressionListener ImpressionListener, metadata dtos.Metadata) *WrapperImpressionListener { 23 | return &WrapperImpressionListener{ 24 | ImpressionListener: impressionListener, 25 | metadata: metadata, 26 | } 27 | } 28 | 29 | // SendDataToClient sends the data to client 30 | func (i *WrapperImpressionListener) SendDataToClient(impressions []dtos.Impression, attributes map[string]interface{}) { 31 | for _, impression := range impressions { 32 | datToSend := ILObject{ 33 | Impression: impression, 34 | Attributes: attributes, 35 | InstanceID: i.metadata.MachineName, 36 | SDKLanguageVersion: i.metadata.SDKVersion, 37 | } 38 | 39 | i.ImpressionListener.LogImpression(datToSend) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /splitio/impressions/builder.go: -------------------------------------------------------------------------------- 1 | package impressions 2 | 3 | import ( 4 | "github.com/splitio/go-client/v6/splitio/conf" 5 | config "github.com/splitio/go-split-commons/v6/conf" 6 | "github.com/splitio/go-split-commons/v6/dtos" 7 | "github.com/splitio/go-split-commons/v6/provisional" 8 | "github.com/splitio/go-split-commons/v6/provisional/strategy" 9 | "github.com/splitio/go-split-commons/v6/service/api" 10 | "github.com/splitio/go-split-commons/v6/storage" 11 | "github.com/splitio/go-split-commons/v6/storage/filter" 12 | "github.com/splitio/go-split-commons/v6/synchronizer" 13 | "github.com/splitio/go-split-commons/v6/synchronizer/worker/impression" 14 | "github.com/splitio/go-split-commons/v6/synchronizer/worker/impressionscount" 15 | "github.com/splitio/go-split-commons/v6/tasks" 16 | "github.com/splitio/go-split-commons/v6/telemetry" 17 | "github.com/splitio/go-toolkit/v5/logging" 18 | ) 19 | 20 | const ( 21 | bfExpectedElemenets = 10000000 22 | bfFalsePositiveProbability = 0.01 23 | bfCleaningPeriod = 86400 // 24 hours 24 | uniqueKeysPeriodTaskInMemory = 900 // 15 min 25 | uniqueKeysPeriodTaskRedis = 300 // 5 min 26 | impressionsCountPeriodTaskInMemory = 1800 // 30 min 27 | impressionsCountPeriodTaskRedis = 300 // 5 min 28 | impressionsBulkSizeRedis = 100 29 | ) 30 | 31 | func BuildInMemoryManager( 32 | cfg *conf.SplitSdkConfig, 33 | advanced config.AdvancedConfig, 34 | logger logging.LoggerInterface, 35 | splitTasks *synchronizer.SplitTasks, 36 | workers *synchronizer.Workers, 37 | metadata dtos.Metadata, 38 | splitAPI *api.SplitAPI, 39 | telemetryStorage storage.TelemetryRuntimeProducer, 40 | impressionStorage storage.ImpressionStorageConsumer, 41 | ) (provisional.ImpressionManager, error) { 42 | listenerEnabled := cfg.Advanced.ImpressionListener != nil 43 | impressionsCounter := strategy.NewImpressionsCounter() 44 | filter := filter.NewBloomFilter(bfExpectedElemenets, bfFalsePositiveProbability) 45 | uniqueKeysTracker := strategy.NewUniqueKeysTracker(filter) 46 | 47 | workers.ImpressionsCountRecorder = impressionscount.NewRecorderSingle(impressionsCounter, splitAPI.ImpressionRecorder, metadata, logger, telemetryStorage) 48 | 49 | splitTasks.ImpressionsCountSyncTask = tasks.NewRecordImpressionsCountTask(workers.ImpressionsCountRecorder, logger, impressionsCountPeriodTaskInMemory) 50 | splitTasks.UniqueKeysTask = tasks.NewRecordUniqueKeysTask(workers.TelemetryRecorder, uniqueKeysTracker, uniqueKeysPeriodTaskInMemory, logger) 51 | splitTasks.CleanFilterTask = tasks.NewCleanFilterTask(filter, logger, bfCleaningPeriod) 52 | 53 | noneStrategy := strategy.NewNoneImpl(impressionsCounter, uniqueKeysTracker, listenerEnabled) 54 | 55 | if cfg.ImpressionsMode == config.ImpressionsModeNone { 56 | impManager := provisional.NewImpressionManagerImp(noneStrategy, noneStrategy) 57 | return impManager, nil 58 | } 59 | 60 | workers.ImpressionRecorder = impression.NewRecorderSingle(impressionStorage, splitAPI.ImpressionRecorder, logger, metadata, cfg.ImpressionsMode, telemetryStorage) 61 | splitTasks.ImpressionSyncTask = tasks.NewRecordImpressionsTask(workers.ImpressionRecorder, cfg.TaskPeriods.ImpressionSync, logger, advanced.ImpressionsBulkSize) 62 | 63 | impressionObserver, err := strategy.NewImpressionObserver(500) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | var impressionsStrategy strategy.ProcessStrategyInterface 69 | switch cfg.ImpressionsMode { 70 | case config.ImpressionsModeDebug: 71 | impressionsStrategy = strategy.NewDebugImpl(impressionObserver, listenerEnabled) 72 | default: 73 | impressionsStrategy = strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryStorage, listenerEnabled) 74 | } 75 | 76 | manager := provisional.NewImpressionManagerImp(noneStrategy, impressionsStrategy) 77 | 78 | return manager, nil 79 | } 80 | 81 | func BuildRedisManager( 82 | cfg *conf.SplitSdkConfig, 83 | logger logging.LoggerInterface, 84 | splitTasks *synchronizer.SplitTasks, 85 | telemetryConfigStorage storage.TelemetryConfigProducer, 86 | impressionsCountStorage storage.ImpressionsCountProducer, 87 | telemetryRuntimeStorage storage.TelemetryRuntimeProducer, 88 | ) (provisional.ImpressionManager, error) { 89 | listenerEnabled := cfg.Advanced.ImpressionListener != nil 90 | 91 | impressionsCounter := strategy.NewImpressionsCounter() 92 | filter := filter.NewBloomFilter(bfExpectedElemenets, bfFalsePositiveProbability) 93 | uniqueKeysTracker := strategy.NewUniqueKeysTracker(filter) 94 | 95 | telemetryRecorder := telemetry.NewSynchronizerRedis(telemetryConfigStorage, logger) 96 | impressionsCountRecorder := impressionscount.NewRecorderRedis(impressionsCounter, impressionsCountStorage, logger) 97 | 98 | splitTasks.ImpressionsCountSyncTask = tasks.NewRecordImpressionsCountTask(impressionsCountRecorder, logger, impressionsCountPeriodTaskRedis) 99 | splitTasks.UniqueKeysTask = tasks.NewRecordUniqueKeysTask(telemetryRecorder, uniqueKeysTracker, uniqueKeysPeriodTaskRedis, logger) 100 | splitTasks.CleanFilterTask = tasks.NewCleanFilterTask(filter, logger, bfCleaningPeriod) 101 | 102 | noneStrategy := strategy.NewNoneImpl(impressionsCounter, uniqueKeysTracker, listenerEnabled) 103 | 104 | if cfg.ImpressionsMode == config.ImpressionsModeNone { 105 | impManager := provisional.NewImpressionManagerImp(noneStrategy, noneStrategy) 106 | return impManager, nil 107 | } 108 | 109 | impressionObserver, err := strategy.NewImpressionObserver(500) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | var impressionsStrategy strategy.ProcessStrategyInterface 115 | switch cfg.ImpressionsMode { 116 | case config.ImpressionsModeDebug: 117 | impressionsStrategy = strategy.NewDebugImpl(impressionObserver, listenerEnabled) 118 | default: 119 | impressionsStrategy = strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryRuntimeStorage, listenerEnabled) 120 | } 121 | 122 | manager := provisional.NewImpressionManagerImp(noneStrategy, impressionsStrategy) 123 | 124 | return manager, nil 125 | } 126 | -------------------------------------------------------------------------------- /splitio/impressions/builder_test.go: -------------------------------------------------------------------------------- 1 | package impressions 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/splitio/go-client/v6/splitio/conf" 7 | config "github.com/splitio/go-split-commons/v6/conf" 8 | "github.com/splitio/go-split-commons/v6/dtos" 9 | "github.com/splitio/go-split-commons/v6/service/api" 10 | "github.com/splitio/go-split-commons/v6/storage/inmemory" 11 | "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexqueue" 12 | "github.com/splitio/go-split-commons/v6/storage/mocks" 13 | "github.com/splitio/go-split-commons/v6/synchronizer" 14 | "github.com/splitio/go-toolkit/v5/logging" 15 | ) 16 | 17 | func TestBuildInMemoryWithNone(t *testing.T) { 18 | cfg := conf.SplitSdkConfig{ 19 | ImpressionsMode: config.ImpressionsModeNone, 20 | } 21 | advanced, _ := conf.NormalizeSDKConf(cfg.Advanced) 22 | logger := logging.NewLogger(&logging.LoggerOptions{}) 23 | splitTasks := synchronizer.SplitTasks{} 24 | workers := synchronizer.Workers{} 25 | metadata := dtos.Metadata{} 26 | splitAPI := api.NewSplitAPI("apikey", advanced, logger, metadata) 27 | telemetryStorage, _ := inmemory.NewTelemetryStorage() 28 | impressionsStorage := mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 2), logger, telemetryStorage) 29 | 30 | impManager, err := BuildInMemoryManager(&cfg, advanced, logger, &splitTasks, &workers, metadata, splitAPI, telemetryStorage, impressionsStorage) 31 | if err != nil { 32 | t.Error("err should be nil. ", err.Error()) 33 | } 34 | if impManager == nil { 35 | t.Error("impManager should not be nil. ") 36 | } 37 | if workers.ImpressionsCountRecorder == nil { 38 | t.Error("ImpressionRecorder should not be nil. ") 39 | } 40 | if splitTasks.ImpressionsCountSyncTask == nil { 41 | t.Error("ImpressionsCountSyncTask should not be nil. ") 42 | } 43 | if splitTasks.UniqueKeysTask == nil { 44 | t.Error("UniqueKeysTask should not be nil. ") 45 | } 46 | if splitTasks.CleanFilterTask == nil { 47 | t.Error("CleanFilterTask should not be nil. ") 48 | } 49 | 50 | if workers.ImpressionRecorder != nil { 51 | t.Error("ImpressionRecorder should be nil. ") 52 | } 53 | if splitTasks.ImpressionSyncTask != nil { 54 | t.Error("ImpressionSyncTask should be nil. ") 55 | } 56 | } 57 | 58 | func TestBuildInMemoryWithDebug(t *testing.T) { 59 | cfg := conf.SplitSdkConfig{ 60 | ImpressionsMode: config.ImpressionsModeDebug, 61 | } 62 | advanced, _ := conf.NormalizeSDKConf(cfg.Advanced) 63 | logger := logging.NewLogger(&logging.LoggerOptions{}) 64 | splitTasks := synchronizer.SplitTasks{} 65 | workers := synchronizer.Workers{} 66 | metadata := dtos.Metadata{} 67 | splitAPI := api.NewSplitAPI("apikey", advanced, logger, metadata) 68 | telemetryStorage, _ := inmemory.NewTelemetryStorage() 69 | impressionsStorage := mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 2), logger, telemetryStorage) 70 | 71 | impManager, err := BuildInMemoryManager(&cfg, advanced, logger, &splitTasks, &workers, metadata, splitAPI, telemetryStorage, impressionsStorage) 72 | if err != nil { 73 | t.Error("err should be nil. ", err.Error()) 74 | } 75 | if impManager == nil { 76 | t.Error("impManager should not be nil. ") 77 | } 78 | if workers.ImpressionsCountRecorder == nil { 79 | t.Error("ImpressionRecorder should not be nil. ") 80 | } 81 | if splitTasks.ImpressionsCountSyncTask == nil { 82 | t.Error("ImpressionsCountSyncTask should not be nil. ") 83 | } 84 | if splitTasks.UniqueKeysTask == nil { 85 | t.Error("UniqueKeysTask should not be nil. ") 86 | } 87 | if splitTasks.CleanFilterTask == nil { 88 | t.Error("CleanFilterTask should not be nil. ") 89 | } 90 | if workers.ImpressionRecorder == nil { 91 | t.Error("ImpressionRecorder should be nil. ") 92 | } 93 | if splitTasks.ImpressionSyncTask == nil { 94 | t.Error("ImpressionSyncTask should be nil. ") 95 | } 96 | } 97 | 98 | func TestBuildInMemoryWithOptimized(t *testing.T) { 99 | cfg := conf.SplitSdkConfig{ 100 | ImpressionsMode: config.ImpressionsModeOptimized, 101 | } 102 | advanced, _ := conf.NormalizeSDKConf(cfg.Advanced) 103 | logger := logging.NewLogger(&logging.LoggerOptions{}) 104 | splitTasks := synchronizer.SplitTasks{} 105 | workers := synchronizer.Workers{} 106 | metadata := dtos.Metadata{} 107 | splitAPI := api.NewSplitAPI("apikey", advanced, logger, metadata) 108 | telemetryStorage, _ := inmemory.NewTelemetryStorage() 109 | impressionsStorage := mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 2), logger, telemetryStorage) 110 | 111 | impManager, err := BuildInMemoryManager(&cfg, advanced, logger, &splitTasks, &workers, metadata, splitAPI, telemetryStorage, impressionsStorage) 112 | if err != nil { 113 | t.Error("err should be nil. ", err.Error()) 114 | } 115 | if impManager == nil { 116 | t.Error("impManager should not be nil. ") 117 | } 118 | if workers.ImpressionsCountRecorder == nil { 119 | t.Error("ImpressionRecorder should not be nil. ") 120 | } 121 | if splitTasks.ImpressionsCountSyncTask == nil { 122 | t.Error("ImpressionsCountSyncTask should not be nil. ") 123 | } 124 | if splitTasks.UniqueKeysTask == nil { 125 | t.Error("UniqueKeysTask should not be nil. ") 126 | } 127 | if splitTasks.CleanFilterTask == nil { 128 | t.Error("CleanFilterTask should not be nil. ") 129 | } 130 | if workers.ImpressionRecorder == nil { 131 | t.Error("ImpressionRecorder should be nil. ") 132 | } 133 | if splitTasks.ImpressionSyncTask == nil { 134 | t.Error("ImpressionSyncTask should be nil. ") 135 | } 136 | } 137 | 138 | func TestBuildRedisWithNone(t *testing.T) { 139 | cfg := conf.SplitSdkConfig{ 140 | ImpressionsMode: config.ImpressionsModeNone, 141 | } 142 | logger := logging.NewLogger(&logging.LoggerOptions{}) 143 | splitTasks := synchronizer.SplitTasks{} 144 | runtimeTelemetry := mocks.MockTelemetryStorage{} 145 | impressionCountStorage := mocks.MockImpressionsCountStorage{} 146 | 147 | impManager, err := BuildRedisManager(&cfg, logger, &splitTasks, runtimeTelemetry, impressionCountStorage, runtimeTelemetry) 148 | if err != nil { 149 | t.Error("err should be nil. ", err.Error()) 150 | } 151 | if impManager == nil { 152 | t.Error("impManager should not be nil. ") 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /splitio/version.go: -------------------------------------------------------------------------------- 1 | package splitio 2 | 3 | // Version contains a string with the split sdk version 4 | const Version = "6.7.0" 5 | -------------------------------------------------------------------------------- /testdata/segment_mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "employees", 3 | "added": [ 4 | "user_for_testing_do_no_erase" 5 | ], 6 | "removed": [], 7 | "since": -1, 8 | "till": 1489542661161 9 | } 10 | -------------------------------------------------------------------------------- /testdata/segments/segment_1.json: -------------------------------------------------------------------------------- 1 | {"name":"segment_1","added":["example1","example2"],"removed":[],"since":-1,"till":1585948850110} -------------------------------------------------------------------------------- /testdata/split_mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "trafficTypeName": "user", 3 | "name": "DEMO_MURMUR2", 4 | "trafficAllocation": 100, 5 | "trafficAllocationSeed": 1314112417, 6 | "seed": -2059033614, 7 | "status": "ACTIVE", 8 | "killed": false, 9 | "defaultTreatment": "of", 10 | "changeNumber": 1491244291288, 11 | "algo": 2, 12 | "configurations": { 13 | "on": "{\"color\": \"blue\",\"size\": 13}" 14 | }, 15 | "conditions": [ 16 | { 17 | "conditionType": "ROLLOUT", 18 | "matcherGroup": { 19 | "combiner": "AND", 20 | "matchers": [ 21 | { 22 | "keySelector": { 23 | "trafficType": "user", 24 | "attribute": null 25 | }, 26 | "matcherType": "ALL_KEYS", 27 | "negate": false, 28 | "userDefinedSegmentMatcherData": null, 29 | "whitelistMatcherData": null, 30 | "unaryNumericMatcherData": null, 31 | "betweenMatcherData": null 32 | } 33 | ] 34 | }, 35 | "partitions": [ 36 | { 37 | "treatment": "on", 38 | "size": 0 39 | }, 40 | { 41 | "treatment": "of", 42 | "size": 100 43 | } 44 | ], 45 | "label": "in segment all" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /testdata/splits.json: -------------------------------------------------------------------------------- 1 | {"till":300,"since":-1,"splits":[{"changeNumber":1660326991072,"trafficTypeName":"user","name":"feature_flag_1","trafficAllocation":100,"trafficAllocationSeed":-1364119282,"seed":-605938843,"status":"ACTIVE","killed":false,"defaultTreatment":"off","algo":2,"conditions":[{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"dependencyMatcherData":null,"booleanMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"default rule"}],"configurations":{}},{"changeNumber":1683928900842,"trafficTypeName":"user","name":"feature_flag_2","trafficAllocation":100,"trafficAllocationSeed":-29637986,"seed":651776645,"status":"ACTIVE","killed":false,"defaultTreatment":"off","algo":2,"conditions":[{"conditionType":"WHITELIST","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":null,"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"segment_1"},"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"dependencyMatcherData":null,"booleanMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"some_treatment","size":100}],"label":"whitelisted segment"},{"conditionType":"ROLLOUT","matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":null},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":null,"whitelistMatcherData":null,"unaryNumericMatcherData":null,"betweenMatcherData":null,"dependencyMatcherData":null,"booleanMatcherData":null,"stringMatcherData":null}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0},{"treatment":"some_treatment","size":0}],"label":"default rule"}],"configurations":{"off":"{\"color\":\"blue\"}","on":"{\"color\":\"red\"}","some_treatment":"{\"color\":\"white\"}"}},{"changeNumber":1637270255857,"trafficTypeName":"user","name":"feature_flag_3","trafficAllocation":100,"trafficAllocationSeed":-1680947654,"seed":-109151416,"status":"ACTIVE","killed":false,"algo":2},{"changeNumber":1660326991079,"trafficTypeName":"user","name":"feature_flag_4","trafficAllocation":100,"trafficAllocationSeed":-1680947654,"seed":-109151416,"status":"ACTIVE","killed":true,"algo":2}]} -------------------------------------------------------------------------------- /testdata/splits.yaml: -------------------------------------------------------------------------------- 1 | - my_feature: 2 | treatment: "on" 3 | keys: "key" 4 | config: "{\"desc\" : \"this applies only to ON treatment\"}" 5 | - other_feature_3: 6 | treatment: "off" 7 | - my_feature: 8 | treatment: "off" 9 | keys: "only_key" 10 | config: "{\"desc\" : \"this applies only to OFF and only for only_key. The rest will receive ON\"}" 11 | - other_feature_3: 12 | treatment: "on" 13 | keys: "key_whitelist" 14 | - other_feature: 15 | treatment: "on" 16 | keys: ["key2","key3"] 17 | - other_feature_2: 18 | treatment: "on" 19 | -------------------------------------------------------------------------------- /testdata/splits_mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [%s], 3 | "since": 1491244291288, 4 | "till": 1491244291288 5 | } 6 | -------------------------------------------------------------------------------- /testdata/splits_mock_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "trafficTypeName": "user", 5 | "name": "DEMO_MURMUR2", 6 | "trafficAllocation": 100, 7 | "trafficAllocationSeed": 1314112417, 8 | "seed": -2059033614, 9 | "status": "ACTIVE", 10 | "killed": false, 11 | "defaultTreatment": "of", 12 | "changeNumber": 1491244291288, 13 | "algo": 2, 14 | "configurations": { 15 | "on": "{\"color\": \"blue\",\"size\": 13}" 16 | }, 17 | "conditions": [ 18 | { 19 | "conditionType": "ROLLOUT", 20 | "matcherGroup": { 21 | "combiner": "AND", 22 | "matchers": [ 23 | { 24 | "keySelector": { 25 | "trafficType": "user", 26 | "attribute": null 27 | }, 28 | "matcherType": "ALL_KEYS", 29 | "negate": false, 30 | "userDefinedSegmentMatcherData": null, 31 | "whitelistMatcherData": null, 32 | "unaryNumericMatcherData": null, 33 | "betweenMatcherData": null 34 | } 35 | ] 36 | }, 37 | "partitions": [ 38 | { 39 | "treatment": "on", 40 | "size": 0 41 | }, 42 | { 43 | "treatment": "of", 44 | "size": 100 45 | } 46 | ], 47 | "label": "in segment all" 48 | } 49 | ] 50 | }, 51 | { 52 | "trafficTypeName": "user", 53 | "name": "DEMO_MURMUR", 54 | "trafficAllocation": 100, 55 | "trafficAllocationSeed": 1314112417, 56 | "seed": -2059033614, 57 | "status": "ACTIVE", 58 | "killed": false, 59 | "defaultTreatment": "of", 60 | "changeNumber": 1491244291288, 61 | "algo": 2, 62 | "configurations": { 63 | "on": "{\"color\": \"blue\",\"size\": 13}" 64 | }, 65 | "conditions": [ 66 | { 67 | "conditionType": "ROLLOUT", 68 | "matcherGroup": { 69 | "combiner": "AND", 70 | "matchers": [ 71 | { 72 | "keySelector": { 73 | "trafficType": "user", 74 | "attribute": null 75 | }, 76 | "matcherType": "ALL_KEYS", 77 | "negate": false, 78 | "userDefinedSegmentMatcherData": null, 79 | "whitelistMatcherData": null, 80 | "unaryNumericMatcherData": null, 81 | "betweenMatcherData": null 82 | } 83 | ] 84 | }, 85 | "partitions": [ 86 | { 87 | "treatment": "on", 88 | "size": 0 89 | }, 90 | { 91 | "treatment": "of", 92 | "size": 100 93 | } 94 | ], 95 | "label": "in segment all" 96 | } 97 | ] 98 | }, 99 | { 100 | "trafficTypeName": "user", 101 | "name": "IMPRESSION_TOGGLE_FLAG", 102 | "trafficAllocation": 100, 103 | "trafficAllocationSeed": 1314112417, 104 | "seed": -2059033614, 105 | "status": "ACTIVE", 106 | "killed": false, 107 | "defaultTreatment": "of", 108 | "changeNumber": 1491244291288, 109 | "algo": 2, 110 | "impressionsDisabled": true, 111 | "conditions": [ 112 | { 113 | "conditionType": "ROLLOUT", 114 | "matcherGroup": { 115 | "combiner": "AND", 116 | "matchers": [ 117 | { 118 | "keySelector": { 119 | "trafficType": "user", 120 | "attribute": null 121 | }, 122 | "matcherType": "ALL_KEYS", 123 | "negate": false, 124 | "userDefinedSegmentMatcherData": null, 125 | "whitelistMatcherData": null, 126 | "unaryNumericMatcherData": null, 127 | "betweenMatcherData": null 128 | } 129 | ] 130 | }, 131 | "partitions": [ 132 | { 133 | "treatment": "on", 134 | "size": 0 135 | }, 136 | { 137 | "treatment": "of", 138 | "size": 100 139 | } 140 | ], 141 | "label": "in segment all" 142 | } 143 | ] 144 | } 145 | ], 146 | "since": 1491244291288, 147 | "till": 1491244291288 148 | } -------------------------------------------------------------------------------- /testdata/splits_mock_3.json: -------------------------------------------------------------------------------- 1 | { 2 | "splits": [ 3 | { 4 | "trafficTypeName": "user", 5 | "name": "semver", 6 | "trafficAllocation": 100, 7 | "trafficAllocationSeed": 1314112417, 8 | "seed": -2059033614, 9 | "status": "ACTIVE", 10 | "killed": false, 11 | "defaultTreatment": "of", 12 | "changeNumber": 1491244291288, 13 | "algo": 2, 14 | "configurations": { 15 | "on": "{\"color\": \"blue\",\"size\": 13}" 16 | }, 17 | "conditions": [ 18 | { 19 | "conditionType": "ROLLOUT", 20 | "matcherGroup": { 21 | "combiner": "AND", 22 | "matchers": [ 23 | { 24 | "keySelector": { 25 | "trafficType": "user", 26 | "attribute": "version" 27 | }, 28 | "matcherType":"EQUAL_TO_SEMVER", 29 | "negate":false, 30 | "userDefinedSegmentMatcherData":null, 31 | "whitelistMatcherData":null, 32 | "unaryNumericMatcherData":null, 33 | "betweenMatcherData":null, 34 | "dependencyMatcherData":null, 35 | "booleanMatcherData":null, 36 | "stringMatcherData":"1.22.9" 37 | } 38 | ] 39 | }, 40 | "partitions": [ 41 | { 42 | "treatment": "on", 43 | "size": 100 44 | }, 45 | { 46 | "treatment": "of", 47 | "size": 0 48 | } 49 | ], 50 | "label": "in segment all" 51 | } 52 | ] 53 | }, 54 | { 55 | "trafficTypeName": "user", 56 | "name": "semver1", 57 | "trafficAllocation": 100, 58 | "trafficAllocationSeed": 1314112417, 59 | "seed": -2059033614, 60 | "status": "ACTIVE", 61 | "killed": false, 62 | "defaultTreatment": "of", 63 | "changeNumber": 1491244291288, 64 | "algo": 2, 65 | "configurations": { 66 | "on": "{\"color\": \"blue\",\"size\": 13}" 67 | }, 68 | "conditions": [ 69 | { 70 | "conditionType": "ROLLOUT", 71 | "matcherGroup": { 72 | "combiner": "AND", 73 | "matchers": [ 74 | { 75 | "keySelector": { 76 | "trafficType": "user", 77 | "attribute": "version" 78 | }, 79 | "matcherType":"BETWEEN_SEMVER", 80 | "negate":false, 81 | "userDefinedSegmentMatcherData":null, 82 | "whitelistMatcherData":null, 83 | "unaryNumericMatcherData":null, 84 | "betweenMatcherData":null, 85 | "dependencyMatcherData":null, 86 | "booleanMatcherData":null, 87 | "stringMatcherData":null, 88 | "betweenStringMatcherData":{ 89 | "start":"1.22.9", 90 | "end":"2.1.0" 91 | } 92 | } 93 | ] 94 | }, 95 | "partitions": [ 96 | { 97 | "treatment": "on", 98 | "size": 100 99 | }, 100 | { 101 | "treatment": "of", 102 | "size": 0 103 | } 104 | ], 105 | "label": "in segment all" 106 | } 107 | ] 108 | }, 109 | { 110 | "trafficTypeName": "user", 111 | "name": "semver2", 112 | "trafficAllocation": 100, 113 | "trafficAllocationSeed": 1314112417, 114 | "seed": -2059033614, 115 | "status": "ACTIVE", 116 | "killed": false, 117 | "defaultTreatment": "of", 118 | "changeNumber": 1491244291288, 119 | "algo": 2, 120 | "configurations": { 121 | "on": "{\"color\": \"blue\",\"size\": 13}" 122 | }, 123 | "conditions": [ 124 | { 125 | "conditionType": "ROLLOUT", 126 | "matcherGroup": { 127 | "combiner": "AND", 128 | "matchers": [ 129 | { 130 | "keySelector": { 131 | "trafficType": "user", 132 | "attribute": "version" 133 | }, 134 | "matcherType":"GREATER_THAN_OR_EQUAL_TO_SEMVER", 135 | "negate":false, 136 | "userDefinedSegmentMatcherData":null, 137 | "whitelistMatcherData":null, 138 | "unaryNumericMatcherData":null, 139 | "betweenMatcherData":null, 140 | "dependencyMatcherData":null, 141 | "booleanMatcherData":null, 142 | "stringMatcherData":"1.22.9" 143 | } 144 | ] 145 | }, 146 | "partitions": [ 147 | { 148 | "treatment": "on", 149 | "size": 100 150 | }, 151 | { 152 | "treatment": "of", 153 | "size": 0 154 | } 155 | ], 156 | "label": "in segment all" 157 | } 158 | ] 159 | }, 160 | { 161 | "trafficTypeName": "user", 162 | "name": "semver3", 163 | "trafficAllocation": 100, 164 | "trafficAllocationSeed": 1314112417, 165 | "seed": -2059033614, 166 | "status": "ACTIVE", 167 | "killed": false, 168 | "defaultTreatment": "of", 169 | "changeNumber": 1491244291288, 170 | "algo": 2, 171 | "configurations": { 172 | "on": "{\"color\": \"blue\",\"size\": 13}" 173 | }, 174 | "conditions": [ 175 | { 176 | "conditionType": "ROLLOUT", 177 | "matcherGroup": { 178 | "combiner": "AND", 179 | "matchers": [ 180 | { 181 | "keySelector": { 182 | "trafficType": "user", 183 | "attribute": "version" 184 | }, 185 | "matcherType":"LESS_THAN_OR_EQUAL_TO_SEMVER", 186 | "negate":false, 187 | "userDefinedSegmentMatcherData":null, 188 | "whitelistMatcherData":null, 189 | "unaryNumericMatcherData":null, 190 | "betweenMatcherData":null, 191 | "dependencyMatcherData":null, 192 | "booleanMatcherData":null, 193 | "stringMatcherData":"1.22.9" 194 | } 195 | ] 196 | }, 197 | "partitions": [ 198 | { 199 | "treatment": "on", 200 | "size": 100 201 | }, 202 | { 203 | "treatment": "of", 204 | "size": 0 205 | } 206 | ], 207 | "label": "in segment all" 208 | } 209 | ] 210 | }, 211 | { 212 | "trafficTypeName": "user", 213 | "name": "semver4", 214 | "trafficAllocation": 100, 215 | "trafficAllocationSeed": 1314112417, 216 | "seed": -2059033614, 217 | "status": "ACTIVE", 218 | "killed": false, 219 | "defaultTreatment": "of", 220 | "changeNumber": 1491244291288, 221 | "algo": 2, 222 | "configurations": { 223 | "on": "{\"color\": \"blue\",\"size\": 13}" 224 | }, 225 | "conditions": [ 226 | { 227 | "conditionType": "ROLLOUT", 228 | "matcherGroup": { 229 | "combiner": "AND", 230 | "matchers": [ 231 | { 232 | "keySelector": { 233 | "trafficType": "user", 234 | "attribute": "version" 235 | }, 236 | "matcherType":"IN_LIST_SEMVER", 237 | "negate":false, 238 | "userDefinedSegmentMatcherData":null, 239 | "whitelistMatcherData":{ 240 | "whitelist":[ 241 | "1.22.9", 242 | "2.1.0" 243 | ] 244 | }, 245 | "unaryNumericMatcherData":null, 246 | "betweenMatcherData":null, 247 | "dependencyMatcherData":null, 248 | "booleanMatcherData":null, 249 | "stringMatcherData":null, 250 | "betweenStringMatcherData":null 251 | } 252 | ] 253 | }, 254 | "partitions": [ 255 | { 256 | "treatment": "on", 257 | "size": 100 258 | }, 259 | { 260 | "treatment": "of", 261 | "size": 0 262 | } 263 | ], 264 | "label": "in segment all" 265 | } 266 | ] 267 | }, 268 | { 269 | "trafficTypeName": "user", 270 | "name": "unsupported", 271 | "trafficAllocation": 100, 272 | "trafficAllocationSeed": 1314112417, 273 | "seed": -2059033614, 274 | "status": "ACTIVE", 275 | "killed": false, 276 | "defaultTreatment": "of", 277 | "changeNumber": 1491244291288, 278 | "algo": 2, 279 | "configurations": { 280 | "on": "{\"color\": \"blue\",\"size\": 13}" 281 | }, 282 | "conditions": [ 283 | { 284 | "conditionType": "ROLLOUT", 285 | "matcherGroup": { 286 | "combiner": "AND", 287 | "matchers": [ 288 | { 289 | "keySelector": { 290 | "trafficType": "user", 291 | "attribute": null 292 | }, 293 | "matcherType": "UNSUPPORTED", 294 | "negate": false, 295 | "userDefinedSegmentMatcherData": null, 296 | "whitelistMatcherData": null, 297 | "unaryNumericMatcherData": null, 298 | "betweenMatcherData": null 299 | } 300 | ] 301 | }, 302 | "partitions": [ 303 | { 304 | "treatment": "on", 305 | "size": 0 306 | }, 307 | { 308 | "treatment": "of", 309 | "size": 100 310 | } 311 | ], 312 | "label": "in segment all" 313 | } 314 | ] 315 | } 316 | ], 317 | "since": 1491244291288, 318 | "till": 1491244291288 319 | } --------------------------------------------------------------------------------