From e3ab1c29dd188614b5d68f6c9ebb447dcecb6cb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:57:15 +0000 Subject: [PATCH 01/26] Bump SonarSource/sonarcloud-github-action from 2.1.1 to 5.0.0 Bumps [SonarSource/sonarcloud-github-action](https://github.com/sonarsource/sonarcloud-github-action) from 2.1.1 to 5.0.0. - [Release notes](https://github.com/sonarsource/sonarcloud-github-action/releases) - [Commits](https://github.com/sonarsource/sonarcloud-github-action/compare/v2.1.1...v5.0.0) --- updated-dependencies: - dependency-name: SonarSource/sonarcloud-github-action dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80f703f..64e3ce0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: run: make binaries_release - name: SonarQube Scan - uses: SonarSource/sonarcloud-github-action@v2.1.1 + uses: SonarSource/sonarcloud-github-action@v5.0.0 env: SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0b5d66c5a7bbd1d68432f4676909d380a48dc466 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 00:36:40 +0000 Subject: [PATCH 02/26] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/unstable.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80f703f..5020fbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7155ff0..645599a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,7 +18,7 @@ jobs: fips_mode: [enabled, disabled] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Login to DockerHub uses: docker/login-action@v3 diff --git a/.github/workflows/unstable.yml b/.github/workflows/unstable.yml index a1e813c..546bc30 100644 --- a/.github/workflows/unstable.yml +++ b/.github/workflows/unstable.yml @@ -17,7 +17,7 @@ jobs: fips_mode: [enabled, disabled] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Login to DockerHub uses: docker/login-action@v3 From 43016eeb0d37265627927e5fe319c5cd7f4c196d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:45:10 +0000 Subject: [PATCH 03/26] Bump actions/github-script from 7.0.1 to 8.0.0 Bumps [actions/github-script](https://github.com/actions/github-script) from 7.0.1 to 8.0.0. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7.0.1...v8.0.0) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: 8.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80f703f..c908acb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Fail workflow if tag exists if: ${{ github.event_name == 'pull_request' && steps.checkTag.outputs.exists == 'true' }} - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: core.setFailed('[ERROR] Tag already exists.') From fc582f64ee06d554fc8569aada6820e50a0e5510 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:47:37 +0000 Subject: [PATCH 04/26] Bump aws-actions/configure-aws-credentials from 4 to 5 Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 4 to 5. - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v4...v5) --- updated-dependencies: - dependency-name: aws-actions/configure-aws-credentials dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 2 +- .github/workflows/unstable.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7155ff0..bf1567a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,7 +28,7 @@ jobs: - name: Configure AWS credentials if: ${{ github.event_name == 'push' }} - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ vars.ECR_TESTING_ROLE_ARN }} aws-region: us-east-1 diff --git a/.github/workflows/unstable.yml b/.github/workflows/unstable.yml index a1e813c..72b3aa4 100644 --- a/.github/workflows/unstable.yml +++ b/.github/workflows/unstable.yml @@ -27,7 +27,7 @@ jobs: - name: Configure AWS credentials if: ${{ github.event_name == 'push' }} - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: ${{ vars.ECR_TESTING_ROLE_ARN }} aws-region: us-east-1 From 4cd936c898d00d66c110f4dff4bddd7210172abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:47:42 +0000 Subject: [PATCH 05/26] Bump actions/setup-go from 5 to 6 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80f703f..64a3426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: script: core.setFailed('[ERROR] Tag already exists.') - name: Setup Go version - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: '^1.20.7' From b3cd653f4c51fc902501ed4595b31049dd591efe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:54:23 +0000 Subject: [PATCH 06/26] Bump golang.org/x/net from 0.34.0 to 0.38.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.38.0. - [Commits](https://github.com/golang/net/compare/v0.34.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.38.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index b094406..80dd14e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/splitio/go-toolkit/v5 v5.4.0 github.com/stretchr/testify v1.9.0 github.com/vmihailenco/msgpack/v5 v5.3.5 - golang.org/x/sync v0.10.0 + golang.org/x/sync v0.12.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -39,10 +39,10 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.34.1 // indirect ) diff --git a/go.sum b/go.sum index ffa9c93..2468d0a 100644 --- a/go.sum +++ b/go.sum @@ -82,20 +82,20 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From c919f12ca4a8bfc819dc35c5d16e76457bf8f0cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 01:26:36 +0000 Subject: [PATCH 07/26] Bump golang.org/x/crypto from 0.32.0 to 0.45.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index b094406..a879bb9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/splitio/splitd -go 1.23.6 +go 1.24.0 require ( github.com/gin-gonic/gin v1.10.0 @@ -8,7 +8,7 @@ require ( github.com/splitio/go-toolkit/v5 v5.4.0 github.com/stretchr/testify v1.9.0 github.com/vmihailenco/msgpack/v5 v5.3.5 - golang.org/x/sync v0.10.0 + golang.org/x/sync v0.18.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -39,10 +39,10 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.32.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect google.golang.org/protobuf v1.34.1 // indirect ) diff --git a/go.sum b/go.sum index ffa9c93..bf92302 100644 --- a/go.sum +++ b/go.sum @@ -82,20 +82,20 @@ github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From dc9d9541799d1ca334a70170a0091487b77fb631 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 19 Jan 2026 12:11:37 -0300 Subject: [PATCH 08/26] [FME-12436] Updated commons version --- external/commons/mocks/evaluator.go | 2 +- external/commons/mocks/impmanager.go | 4 +- external/commons/mocks/imprecorder.go | 4 +- external/commons/mocks/splitstorage.go | 14 ++- go.mod | 6 +- go.sum | 11 +- splitio/link/client/types/interfaces.go | 2 +- splitio/link/client/v1/impl.go | 2 +- splitio/link/client/v1/impl_test.go | 2 +- splitio/link/service/v1/clientmgr_test.go | 2 +- splitio/sdk/conf/conf.go | 8 +- splitio/sdk/helpers.go | 123 +++++++++++++++++----- splitio/sdk/helpers_test.go | 2 +- splitio/sdk/integration_test.go | 16 +-- splitio/sdk/results.go | 2 +- splitio/sdk/sdk.go | 46 ++++++-- splitio/sdk/sdk_test.go | 8 +- splitio/sdk/storage/multi_test.go | 2 +- splitio/sdk/storage/storages.go | 2 +- splitio/sdk/validators.go | 2 +- splitio/sdk/workers/events.go | 8 +- splitio/sdk/workers/events_test.go | 6 +- splitio/sdk/workers/impressions.go | 8 +- splitio/sdk/workers/impressions_test.go | 6 +- 24 files changed, 195 insertions(+), 93 deletions(-) diff --git a/external/commons/mocks/evaluator.go b/external/commons/mocks/evaluator.go index 0b3b84c..d598cf1 100644 --- a/external/commons/mocks/evaluator.go +++ b/external/commons/mocks/evaluator.go @@ -1,7 +1,7 @@ package mocks import ( - "github.com/splitio/go-split-commons/v6/engine/evaluator" + "github.com/splitio/go-split-commons/v9/engine/evaluator" "github.com/stretchr/testify/mock" ) diff --git a/external/commons/mocks/impmanager.go b/external/commons/mocks/impmanager.go index c5ee391..1f6cbdd 100644 --- a/external/commons/mocks/impmanager.go +++ b/external/commons/mocks/impmanager.go @@ -1,8 +1,8 @@ package mocks import ( - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/provisional" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/provisional" "github.com/stretchr/testify/mock" ) diff --git a/external/commons/mocks/imprecorder.go b/external/commons/mocks/imprecorder.go index 8a47d48..810ec72 100644 --- a/external/commons/mocks/imprecorder.go +++ b/external/commons/mocks/imprecorder.go @@ -1,8 +1,8 @@ package mocks import ( - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/service" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/service" "github.com/stretchr/testify/mock" ) diff --git a/external/commons/mocks/splitstorage.go b/external/commons/mocks/splitstorage.go index 937640b..97f909d 100644 --- a/external/commons/mocks/splitstorage.go +++ b/external/commons/mocks/splitstorage.go @@ -1,8 +1,8 @@ package mocks import ( - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/storage" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/storage" "github.com/splitio/go-toolkit/v5/datastructures/set" "github.com/stretchr/testify/mock" ) @@ -72,4 +72,14 @@ func (m *SplitStorageMock) LargeSegmentNames() *set.ThreadUnsafeSet { return m.Called().Get(0).(*set.ThreadUnsafeSet) } +func (m *SplitStorageMock) ReplaceAll(toAdd []dtos.SplitDTO, changeNumber int64) error { + args := m.Called(toAdd, changeNumber) + return args.Error(0) +} + +func (m *SplitStorageMock) RuleBasedSegmentNames() *set.ThreadUnsafeSet { + args := m.Called() + return args.Get(0).(*set.ThreadUnsafeSet) +} + var _ storage.SplitStorage = (*SplitStorageMock)(nil) diff --git a/go.mod b/go.mod index b094406..d26eb35 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.23.6 require ( github.com/gin-gonic/gin v1.10.0 - github.com/splitio/go-split-commons/v6 v6.1.0 - github.com/splitio/go-toolkit/v5 v5.4.0 - github.com/stretchr/testify v1.9.0 + github.com/splitio/go-split-commons/v9 v9.1.0 + github.com/splitio/go-toolkit/v5 v5.4.1 + github.com/stretchr/testify v1.11.1 github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sync v0.10.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index ffa9c93..7ba9b5a 100644 --- a/go.sum +++ b/go.sum @@ -51,10 +51,10 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/splitio/go-split-commons/v6 v6.1.0 h1:k3mwr12DF6gbEaV8XXU/tSAQlPkIEuzIgTEneYhGg2I= -github.com/splitio/go-split-commons/v6 v6.1.0/go.mod h1:D/XIY/9Hmfk9ivWsRsJVp439kEdmHbzUi3PKzQQDOXY= -github.com/splitio/go-toolkit/v5 v5.4.0 h1:g5WFpRhQomnXCmvfsNOWV4s5AuUrWIZ+amM68G8NBKM= -github.com/splitio/go-toolkit/v5 v5.4.0/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko= +github.com/splitio/go-split-commons/v9 v9.1.0 h1:sfmPMuEDTtbIOJ+MeWNbfYl2/xKB/25d4/J95OUD+X0= +github.com/splitio/go-split-commons/v9 v9.1.0/go.mod h1:gJuaKo04Swlh4w9C1b2jBAqAdFxEd/Vpd8jnFINOeDY= +github.com/splitio/go-toolkit/v5 v5.4.1 h1:srTyvDBJZMUcJ/KiiQDMyjCuELVgTBh2TGRVn0sOXEE= +github.com/splitio/go-toolkit/v5 v5.4.1/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -67,8 +67,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= diff --git a/splitio/link/client/types/interfaces.go b/splitio/link/client/types/interfaces.go index 0d7ac20..4e11855 100644 --- a/splitio/link/client/types/interfaces.go +++ b/splitio/link/client/types/interfaces.go @@ -1,7 +1,7 @@ package types import ( - "github.com/splitio/go-split-commons/v6/dtos" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/splitd/splitio/sdk" ) diff --git a/splitio/link/client/v1/impl.go b/splitio/link/client/v1/impl.go index 47cf7fb..fba91ce 100644 --- a/splitio/link/client/v1/impl.go +++ b/splitio/link/client/v1/impl.go @@ -3,7 +3,7 @@ package v1 import ( "fmt" - "github.com/splitio/go-split-commons/v6/dtos" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio" "github.com/splitio/splitd/splitio/common/lang" diff --git a/splitio/link/client/v1/impl_test.go b/splitio/link/client/v1/impl_test.go index f174400..2bdbee6 100644 --- a/splitio/link/client/v1/impl_test.go +++ b/splitio/link/client/v1/impl_test.go @@ -3,7 +3,7 @@ package v1 import ( "testing" - "github.com/splitio/go-split-commons/v6/dtos" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio/common/lang" v1 "github.com/splitio/splitd/splitio/link/protocol/v1" diff --git a/splitio/link/service/v1/clientmgr_test.go b/splitio/link/service/v1/clientmgr_test.go index 277c044..f9ee8e9 100644 --- a/splitio/link/service/v1/clientmgr_test.go +++ b/splitio/link/service/v1/clientmgr_test.go @@ -5,7 +5,7 @@ import ( "io" "testing" - "github.com/splitio/go-split-commons/v6/dtos" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio/common/lang" "github.com/splitio/splitd/splitio/link/protocol" diff --git a/splitio/sdk/conf/conf.go b/splitio/sdk/conf/conf.go index 1314186..1d2a9d0 100644 --- a/splitio/sdk/conf/conf.go +++ b/splitio/sdk/conf/conf.go @@ -3,10 +3,10 @@ package conf import ( "time" - "github.com/splitio/go-split-commons/v6/conf" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/flagsets" - "github.com/splitio/go-split-commons/v6/service/api/specs" + "github.com/splitio/go-split-commons/v9/conf" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/flagsets" + "github.com/splitio/go-split-commons/v9/service/api/specs" ) const ( diff --git a/splitio/sdk/helpers.go b/splitio/sdk/helpers.go index ffe973e..1903b83 100644 --- a/splitio/sdk/helpers.go +++ b/splitio/sdk/helpers.go @@ -2,28 +2,32 @@ package sdk import ( "fmt" + "regexp" + "strings" sdkConf "github.com/splitio/splitd/splitio/sdk/conf" sss "github.com/splitio/splitd/splitio/sdk/storage" "github.com/splitio/splitd/splitio/sdk/workers" - "github.com/splitio/go-split-commons/v6/conf" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/flagsets" - "github.com/splitio/go-split-commons/v6/healthcheck/application" - "github.com/splitio/go-split-commons/v6/provisional" - "github.com/splitio/go-split-commons/v6/provisional/strategy" - "github.com/splitio/go-split-commons/v6/service/api" - "github.com/splitio/go-split-commons/v6/storage" - "github.com/splitio/go-split-commons/v6/storage/filter" - "github.com/splitio/go-split-commons/v6/storage/inmemory" - "github.com/splitio/go-split-commons/v6/storage/inmemory/mutexmap" - "github.com/splitio/go-split-commons/v6/synchronizer" - "github.com/splitio/go-split-commons/v6/synchronizer/worker/impressionscount" - "github.com/splitio/go-split-commons/v6/synchronizer/worker/segment" - "github.com/splitio/go-split-commons/v6/synchronizer/worker/split" - "github.com/splitio/go-split-commons/v6/tasks" - "github.com/splitio/go-split-commons/v6/telemetry" + "github.com/splitio/go-split-commons/v9/conf" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/engine/grammar" + "github.com/splitio/go-split-commons/v9/flagsets" + "github.com/splitio/go-split-commons/v9/healthcheck/application" + "github.com/splitio/go-split-commons/v9/provisional" + "github.com/splitio/go-split-commons/v9/provisional/strategy" + "github.com/splitio/go-split-commons/v9/service/api" + "github.com/splitio/go-split-commons/v9/service/api/specs" + "github.com/splitio/go-split-commons/v9/storage" + "github.com/splitio/go-split-commons/v9/storage/filter" + "github.com/splitio/go-split-commons/v9/storage/inmemory" + "github.com/splitio/go-split-commons/v9/storage/inmemory/mutexmap" + "github.com/splitio/go-split-commons/v9/synchronizer" + "github.com/splitio/go-split-commons/v9/synchronizer/worker/impressionscount" + "github.com/splitio/go-split-commons/v9/synchronizer/worker/segment" + "github.com/splitio/go-split-commons/v9/synchronizer/worker/split" + "github.com/splitio/go-split-commons/v9/tasks" + "github.com/splitio/go-split-commons/v9/telemetry" "github.com/splitio/go-toolkit/v5/logging" ) @@ -36,6 +40,12 @@ const ( impressionsCountPeriodTaskInMemory = 1800 // 30 min impressionsCountPeriodTaskRedis = 300 // 5 min impressionsBulkSizeRedis = 100 + // Max Flag name length + MaxFlagNameLength = 100 + // Max Treatment length + MaxTreatmentLength = 100 + // Treatment regexp + TreatmentRegexp = "^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$" ) func setupWorkers( @@ -47,10 +57,11 @@ func setupWorkers( flagSetsFilter flagsets.FlagSetFilter, md dtos.Metadata, impComponents impComponents, + ruleBuilder grammar.RuleBuilder, ) *synchronizer.Workers { return &synchronizer.Workers{ - SplitUpdater: split.NewSplitUpdater(str.splits, api.SplitFetcher, logger, str.telemetry, hc, flagSetsFilter), - SegmentUpdater: segment.NewSegmentUpdater(str.splits, str.segments, api.SegmentFetcher, logger, str.telemetry, hc), + SplitUpdater: split.NewSplitUpdater(str.splits, str.ruleBasedSegments, api.SplitFetcher, logger, str.telemetry, hc, flagSetsFilter, ruleBuilder, false, specs.FLAG_V1_3), + SegmentUpdater: segment.NewSegmentUpdater(str.splits, str.segments, str.ruleBasedSegments, api.SegmentFetcher, logger, str.telemetry, hc), ImpressionRecorder: workers.NewImpressionsWorker(logger, str.telemetry, api.ImpressionRecorder, str.impressions, &cfg.Impressions), EventRecorder: workers.NewEventsWorker(logger, str.telemetry, api.EventRecorder, str.events, &cfg.Events), ImpressionsCountRecorder: impressionscount.NewRecorderSingle(impComponents.counter, api.ImpressionRecorder, md, logger, str.telemetry), @@ -132,11 +143,12 @@ func setupImpressionsComponents(c *sdkConf.Impressions, telemetry storage.Teleme } type storages struct { - splits storage.SplitStorage - segments storage.SegmentStorage - telemetry storage.TelemetryStorage - impressions *sss.ImpressionsStorage - events *sss.EventsStorage + splits storage.SplitStorage + segments storage.SegmentStorage + ruleBasedSegments storage.RuleBasedSegmentsStorage + telemetry storage.TelemetryStorage + impressions *sss.ImpressionsStorage + events *sss.EventsStorage } func setupStorages(cfg *sdkConf.Config, flagSetsFilter flagsets.FlagSetFilter) *storages { @@ -145,14 +157,67 @@ func setupStorages(cfg *sdkConf.Config, flagSetsFilter flagsets.FlagSetFilter) * eq, _ := sss.NewEventsQueue(cfg.Events.QueueSize) return &storages{ - splits: mutexmap.NewMMSplitStorage(flagSetsFilter), - segments: mutexmap.NewMMSegmentStorage(), - impressions: iq, - events: eq, - telemetry: ts, + splits: mutexmap.NewMMSplitStorage(flagSetsFilter), + segments: mutexmap.NewMMSegmentStorage(), + ruleBasedSegments: mutexmap.NewRuleBasedSegmentsStorage(), + impressions: iq, + events: eq, + telemetry: ts, } } +func SanitizeGlobalFallbackTreatment(global *dtos.FallbackTreatment, logger logging.LoggerInterface) *dtos.FallbackTreatment { + if global == nil { + return nil + } + if !isValidTreatment(global) { + logger.Error(fmt.Sprintf("Fallback treatments - Discarded global fallback: Invalid treatment (max %d chars and comply with %s)", MaxTreatmentLength, TreatmentRegexp)) + return nil + } + return &dtos.FallbackTreatment{ + Treatment: global.Treatment, + Config: global.Config, + } +} + +func isValidTreatment(fallbackTreatment *dtos.FallbackTreatment) bool { + if fallbackTreatment == nil || fallbackTreatment.Treatment == nil { + return false + } + value := *fallbackTreatment.Treatment + pattern := regexp.MustCompile(TreatmentRegexp) + return len(value) <= MaxTreatmentLength && pattern.MatchString(value) +} + +func SanitizeByFlagFallBackTreatment(byFlag map[string]dtos.FallbackTreatment, logger logging.LoggerInterface) map[string]dtos.FallbackTreatment { + sanitized := map[string]dtos.FallbackTreatment{} + if len(byFlag) == 0 { + return sanitized + } + for flagName, treatment := range byFlag { + if !isValidFlagName(&flagName) { + logger.Error(fmt.Sprintf("Fallback treatments - Discarded flag: Invalid flag name (max %d chars, no spaces)", MaxFlagNameLength)) + continue + } + if !isValidTreatment(&treatment) { + logger.Error(fmt.Sprintf("Fallback treatments - Discarded global fallback: Invalid treatment (max %d chars and comply with %s)", MaxTreatmentLength, TreatmentRegexp)) + continue + } + sanitized[flagName] = dtos.FallbackTreatment{ + Treatment: treatment.Treatment, + Config: treatment.Config, + } + } + return sanitized +} + +func isValidFlagName(flagName *string) bool { + if flagName == nil { + return false + } + return len(*flagName) <= MaxFlagNameLength && !strings.Contains(*flagName, " ") +} + // Temporary for running without impressions/events/telemetry/etc type NoOpTask struct{} diff --git a/splitio/sdk/helpers_test.go b/splitio/sdk/helpers_test.go index 9d54b96..d83db88 100644 --- a/splitio/sdk/helpers_test.go +++ b/splitio/sdk/helpers_test.go @@ -3,7 +3,7 @@ package sdk import ( "testing" - "github.com/splitio/go-split-commons/v6/flagsets" + "github.com/splitio/go-split-commons/v9/flagsets" sdkConf "github.com/splitio/splitd/splitio/sdk/conf" "github.com/stretchr/testify/assert" ) diff --git a/splitio/sdk/integration_test.go b/splitio/sdk/integration_test.go index af620e6..a906782 100644 --- a/splitio/sdk/integration_test.go +++ b/splitio/sdk/integration_test.go @@ -10,8 +10,8 @@ import ( "sync/atomic" "testing" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/service/api/specs" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/service/api/specs" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio/sdk/conf" "github.com/splitio/splitd/splitio/sdk/types" @@ -50,14 +50,16 @@ func TestInstantiationAndGetTreatmentE2E(t *testing.T) { assert.Equal(t, "GET", r.Method) assert.Equal(t, "/splitChanges", r.URL.Path) - splitChanges := dtos.SplitChangesDTO{ - Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2, mockedSplit3}, - Since: 3, - Till: 3, + splitChanges := dtos.RuleChangesDTO{ + FeatureFlags: dtos.FeatureFlagsDTO{ + Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2, mockedSplit3}, + Since: 3, + Till: 3, + }, } assert.Equal(t, "-1", r.URL.Query().Get("since")) - assert.Equal(t, specs.FLAG_V1_1, r.URL.Query().Get("s")) + assert.Equal(t, specs.FLAG_V1_3, r.URL.Query().Get("s")) raw, err := json.Marshal(splitChanges) assert.Nil(t, err) diff --git a/splitio/sdk/results.go b/splitio/sdk/results.go index 12d19b7..b7aaa69 100644 --- a/splitio/sdk/results.go +++ b/splitio/sdk/results.go @@ -1,6 +1,6 @@ package sdk -import "github.com/splitio/go-split-commons/v6/dtos" +import "github.com/splitio/go-split-commons/v9/dtos" type EvaluationResult struct { Treatment string diff --git a/splitio/sdk/sdk.go b/splitio/sdk/sdk.go index 06cc765..51838d3 100644 --- a/splitio/sdk/sdk.go +++ b/splitio/sdk/sdk.go @@ -9,15 +9,17 @@ import ( "github.com/splitio/splitd/splitio/sdk/storage" "github.com/splitio/splitd/splitio/sdk/types" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/engine" - "github.com/splitio/go-split-commons/v6/engine/evaluator" - "github.com/splitio/go-split-commons/v6/flagsets" - "github.com/splitio/go-split-commons/v6/healthcheck/application" - "github.com/splitio/go-split-commons/v6/provisional" - "github.com/splitio/go-split-commons/v6/service/api" - commonStorage "github.com/splitio/go-split-commons/v6/storage" - "github.com/splitio/go-split-commons/v6/synchronizer" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/engine" + "github.com/splitio/go-split-commons/v9/engine/evaluator" + "github.com/splitio/go-split-commons/v9/engine/grammar" + "github.com/splitio/go-split-commons/v9/engine/grammar/constants" + "github.com/splitio/go-split-commons/v9/flagsets" + "github.com/splitio/go-split-commons/v9/healthcheck/application" + "github.com/splitio/go-split-commons/v9/provisional" + "github.com/splitio/go-split-commons/v9/service/api" + commonStorage "github.com/splitio/go-split-commons/v9/storage" + "github.com/splitio/go-split-commons/v9/synchronizer" "github.com/splitio/go-toolkit/v5/common" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio" @@ -33,6 +35,16 @@ var ( ErrSplitNotFound = errors.New("split not found") ) +var featureFlagsRules = []string{constants.MatcherTypeAllKeys, constants.MatcherTypeInSegment, constants.MatcherTypeWhitelist, constants.MatcherTypeEqualTo, constants.MatcherTypeGreaterThanOrEqualTo, constants.MatcherTypeLessThanOrEqualTo, constants.MatcherTypeBetween, + constants.MatcherTypeEqualToSet, constants.MatcherTypePartOfSet, constants.MatcherTypeContainsAllOfSet, constants.MatcherTypeContainsAnyOfSet, constants.MatcherTypeStartsWith, constants.MatcherTypeEndsWith, constants.MatcherTypeContainsString, constants.MatcherTypeInSplitTreatment, + constants.MatcherTypeEqualToBoolean, constants.MatcherTypeMatchesString, constants.MatcherEqualToSemver, constants.MatcherTypeGreaterThanOrEqualToSemver, constants.MatcherTypeLessThanOrEqualToSemver, constants.MatcherTypeBetweenSemver, constants.MatcherTypeInListSemver, + constants.MatcherTypeInRuleBasedSegment} + +var ruleBasedSegmentRules = []string{constants.MatcherTypeAllKeys, constants.MatcherTypeInSegment, constants.MatcherTypeWhitelist, constants.MatcherTypeEqualTo, constants.MatcherTypeGreaterThanOrEqualTo, constants.MatcherTypeLessThanOrEqualTo, constants.MatcherTypeBetween, + constants.MatcherTypeEqualToSet, constants.MatcherTypePartOfSet, constants.MatcherTypeContainsAllOfSet, constants.MatcherTypeContainsAnyOfSet, constants.MatcherTypeStartsWith, constants.MatcherTypeEndsWith, constants.MatcherTypeContainsString, + constants.MatcherTypeEqualToBoolean, constants.MatcherTypeMatchesString, constants.MatcherEqualToSemver, constants.MatcherTypeGreaterThanOrEqualToSemver, constants.MatcherTypeLessThanOrEqualToSemver, constants.MatcherTypeBetweenSemver, constants.MatcherTypeInListSemver, + constants.MatcherTypeInRuleBasedSegment} + type Attributes = map[string]interface{} type Interface interface { @@ -84,7 +96,10 @@ func New(logger logging.LoggerInterface, apikey string, c *conf.Config) (*Impl, queueFullChan := make(chan string, 2) splitApi := api.NewSplitAPI(apikey, *advCfg, logger, md) - workers := setupWorkers(logger, splitApi, stores, hc, c, flagSetsFilter, md, impc) + fallbackTreatmentCalculator := createFallbackTreatmentCalculator(&advCfg.FallbackTreatment, logger) + evaluator := evaluator.NewEvaluator(stores.splits, stores.segments, stores.ruleBasedSegments, nil, engine.NewEngine(logger), logger, featureFlagsRules, ruleBasedSegmentRules, fallbackTreatmentCalculator) + ruleBuilder := grammar.NewRuleBuilder(stores.segments, stores.ruleBasedSegments, nil, featureFlagsRules, ruleBasedSegmentRules, logger, evaluator) + workers := setupWorkers(logger, splitApi, stores, hc, c, flagSetsFilter, md, impc, ruleBuilder) tasks := setupTasks(c, logger, workers, impc) sync := synchronizer.NewSynchronizer(*advCfg, *tasks, *workers, logger, queueFullChan) @@ -105,7 +120,7 @@ func New(logger logging.LoggerInterface, apikey string, c *conf.Config) (*Impl, logger: logger, sm: manager, ss: sync, - ev: evaluator.NewEvaluator(stores.splits, stores.segments, engine.NewEngine(logger), logger), + ev: evaluator, is: stores.impressions, es: stores.events, iq: impc.manager, @@ -313,4 +328,13 @@ func timeMillis() int64 { return time.Now().UTC().UnixMilli() } +func createFallbackTreatmentCalculator(fallbackTreatmentConfig *dtos.FallbackTreatmentConfig, logger logging.LoggerInterface) dtos.FallbackTreatmentCalculator { + fallbackTreatmentConf := dtos.FallbackTreatmentConfig{} + if fallbackTreatmentConfig != nil { + fallbackTreatmentConf.GlobalFallbackTreatment = SanitizeGlobalFallbackTreatment(fallbackTreatmentConfig.GlobalFallbackTreatment, logger) + fallbackTreatmentConf.ByFlagFallbackTreatment = SanitizeByFlagFallBackTreatment(fallbackTreatmentConfig.ByFlagFallbackTreatment, logger) + } + return dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConf) +} + var _ Interface = (*Impl)(nil) diff --git a/splitio/sdk/sdk_test.go b/splitio/sdk/sdk_test.go index 0e6f1a4..4079a1f 100644 --- a/splitio/sdk/sdk_test.go +++ b/splitio/sdk/sdk_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/engine/evaluator" - "github.com/splitio/go-split-commons/v6/storage/inmemory" - "github.com/splitio/go-split-commons/v6/synchronizer" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/engine/evaluator" + "github.com/splitio/go-split-commons/v9/storage/inmemory" + "github.com/splitio/go-split-commons/v9/synchronizer" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/external/commons/mocks" "github.com/splitio/splitd/splitio/common/lang" diff --git a/splitio/sdk/storage/multi_test.go b/splitio/sdk/storage/multi_test.go index 611f6cb..aa516bf 100644 --- a/splitio/sdk/storage/multi_test.go +++ b/splitio/sdk/storage/multi_test.go @@ -3,7 +3,7 @@ package storage import ( "testing" - "github.com/splitio/go-split-commons/v6/dtos" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/splitd/splitio/sdk/types" "github.com/stretchr/testify/assert" diff --git a/splitio/sdk/storage/storages.go b/splitio/sdk/storage/storages.go index f979fa0..7124edc 100644 --- a/splitio/sdk/storage/storages.go +++ b/splitio/sdk/storage/storages.go @@ -3,7 +3,7 @@ package storage import ( "math" - "github.com/splitio/go-split-commons/v6/dtos" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/splitd/splitio/sdk/types" ) diff --git a/splitio/sdk/validators.go b/splitio/sdk/validators.go index 93e53f7..888b337 100644 --- a/splitio/sdk/validators.go +++ b/splitio/sdk/validators.go @@ -4,7 +4,7 @@ import ( "errors" "strings" - "github.com/splitio/go-split-commons/v6/storage" + "github.com/splitio/go-split-commons/v9/storage" "github.com/splitio/go-toolkit/v5/logging" ) diff --git a/splitio/sdk/workers/events.go b/splitio/sdk/workers/events.go index 9537191..3cfc364 100644 --- a/splitio/sdk/workers/events.go +++ b/splitio/sdk/workers/events.go @@ -9,10 +9,10 @@ import ( "github.com/splitio/splitd/splitio/sdk/types" serrors "github.com/splitio/splitd/splitio/util/errors" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/service" - "github.com/splitio/go-split-commons/v6/storage" - "github.com/splitio/go-split-commons/v6/synchronizer/worker/event" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/service" + "github.com/splitio/go-split-commons/v9/storage" + "github.com/splitio/go-split-commons/v9/synchronizer/worker/event" "github.com/splitio/go-toolkit/v5/logging" gtsync "github.com/splitio/go-toolkit/v5/sync" ) diff --git a/splitio/sdk/workers/events_test.go b/splitio/sdk/workers/events_test.go index 26e1033..1f709da 100644 --- a/splitio/sdk/workers/events_test.go +++ b/splitio/sdk/workers/events_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/service" - "github.com/splitio/go-split-commons/v6/storage/inmemory" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/service" + "github.com/splitio/go-split-commons/v9/storage/inmemory" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio/sdk/conf" sss "github.com/splitio/splitd/splitio/sdk/storage" diff --git a/splitio/sdk/workers/impressions.go b/splitio/sdk/workers/impressions.go index 8f3132b..b029251 100644 --- a/splitio/sdk/workers/impressions.go +++ b/splitio/sdk/workers/impressions.go @@ -9,10 +9,10 @@ import ( "github.com/splitio/splitd/splitio/sdk/types" serrors "github.com/splitio/splitd/splitio/util/errors" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/service" - "github.com/splitio/go-split-commons/v6/storage" - "github.com/splitio/go-split-commons/v6/synchronizer/worker/impression" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/service" + "github.com/splitio/go-split-commons/v9/storage" + "github.com/splitio/go-split-commons/v9/synchronizer/worker/impression" "github.com/splitio/go-toolkit/v5/logging" gtsync "github.com/splitio/go-toolkit/v5/sync" ) diff --git a/splitio/sdk/workers/impressions_test.go b/splitio/sdk/workers/impressions_test.go index 6499bae..219d503 100644 --- a/splitio/sdk/workers/impressions_test.go +++ b/splitio/sdk/workers/impressions_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - "github.com/splitio/go-split-commons/v6/dtos" - "github.com/splitio/go-split-commons/v6/service" - "github.com/splitio/go-split-commons/v6/storage/inmemory" + "github.com/splitio/go-split-commons/v9/dtos" + "github.com/splitio/go-split-commons/v9/service" + "github.com/splitio/go-split-commons/v9/storage/inmemory" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio/sdk/conf" sss "github.com/splitio/splitd/splitio/sdk/storage" From 92a6f853ff7a190ff36b1793a35fa32cd4590cc3 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 19 Jan 2026 14:32:16 -0300 Subject: [PATCH 09/26] Updated log message --- splitio/sdk/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/sdk/helpers.go b/splitio/sdk/helpers.go index 1903b83..77ab56d 100644 --- a/splitio/sdk/helpers.go +++ b/splitio/sdk/helpers.go @@ -200,7 +200,7 @@ func SanitizeByFlagFallBackTreatment(byFlag map[string]dtos.FallbackTreatment, l continue } if !isValidTreatment(&treatment) { - logger.Error(fmt.Sprintf("Fallback treatments - Discarded global fallback: Invalid treatment (max %d chars and comply with %s)", MaxTreatmentLength, TreatmentRegexp)) + logger.Error(fmt.Sprintf("Fallback treatments - Discarded treatment for flag '%s': Invalid treatment (max %d chars and comply with %s)", flagName, MaxTreatmentLength, TreatmentRegexp)) continue } sanitized[flagName] = dtos.FallbackTreatment{ From ef3dfd1cca4f2a3b811086d25ebcb18cca2ab13b Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 19 Jan 2026 22:36:37 -0300 Subject: [PATCH 10/26] Added fallback treatment --- splitio/sdk/conf/conf.go | 18 +++-- splitio/sdk/integration_test.go | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 8 deletions(-) diff --git a/splitio/sdk/conf/conf.go b/splitio/sdk/conf/conf.go index 1d2a9d0..f06d900 100644 --- a/splitio/sdk/conf/conf.go +++ b/splitio/sdk/conf/conf.go @@ -15,14 +15,15 @@ const ( ) type Config struct { - LabelsEnabled bool - StreamingEnabled bool - Splits Splits - Segments Segments - Impressions Impressions - Events Events - URLs URLs - FlagSetsFilter []string + LabelsEnabled bool + StreamingEnabled bool + Splits Splits + Segments Segments + Impressions Impressions + Events Events + URLs URLs + FlagSetsFilter []string + FallbackTreatment dtos.FallbackTreatmentConfig } type Splits struct { @@ -81,6 +82,7 @@ func (c *Config) ToAdvancedConfig() *conf.AdvancedConfig { d.ImpressionsQueueSize = c.Impressions.QueueSize d.AuthSpecVersion = specs.FLAG_V1_1 d.FlagsSpecVersion = specs.FLAG_V1_1 + d.FallbackTreatment = c.FallbackTreatment return &d } diff --git a/splitio/sdk/integration_test.go b/splitio/sdk/integration_test.go index a906782..aba90b7 100644 --- a/splitio/sdk/integration_test.go +++ b/splitio/sdk/integration_test.go @@ -104,6 +104,20 @@ func TestInstantiationAndGetTreatmentE2E(t *testing.T) { sdkConf.URLs.SDK = sdkServer.URL sdkConf.URLs.Telemetry = telemetryServer.URL sdkConf.StreamingEnabled = false + stringConfig := "flag1_config" + globalTreatment := "global_treatment" + flag1Treatment := "flag1_treatment" + sdkConf.FallbackTreatment = dtos.FallbackTreatmentConfig{ + GlobalFallbackTreatment: &dtos.FallbackTreatment{ + Treatment: &globalTreatment, + }, + ByFlagFallbackTreatment: map[string]dtos.FallbackTreatment{ + "flag1": { + Treatment: &flag1Treatment, + Config: &stringConfig, + }, + }, + } logger := logging.NewLogger(nil) client, err := New(logger, "someApikey", sdkConf) @@ -115,3 +129,115 @@ func TestInstantiationAndGetTreatmentE2E(t *testing.T) { assert.Nil(t, client.Shutdown()) assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) } + +func TestInstantiationAndGetTreatmentE2EWithFallbackTreatment(t *testing.T) { + metricsInitCalled := 0 + mockedSplit1 := dtos.SplitDTO{ + Algo: 2, + ChangeNumber: 123, + DefaultTreatment: "default", + Killed: false, + Name: "split", + Seed: 1234, + Status: "ACTIVE", + TrafficAllocation: 1, + TrafficAllocationSeed: -1667452163, + TrafficTypeName: "tt1", + Conditions: []dtos.ConditionDTO{ + { + ConditionType: "ROLLOUT", + Label: "in segment all", + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{{MatcherType: "ALL_KEYS"}}, + }, + Partitions: []dtos.PartitionDTO{{Size: 100, Treatment: "on"}}, + }, + }, + } + mockedSplit2 := dtos.SplitDTO{Name: "split2", Killed: true, Status: "ACTIVE"} + mockedSplit3 := dtos.SplitDTO{Name: "split3", Killed: true, Status: "INACTIVE"} + + sdkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/splitChanges", r.URL.Path) + + splitChanges := dtos.RuleChangesDTO{ + FeatureFlags: dtos.FeatureFlagsDTO{ + Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2, mockedSplit3}, + Since: 3, + Till: 3, + }, + } + + assert.Equal(t, "-1", r.URL.Query().Get("since")) + assert.Equal(t, specs.FLAG_V1_3, r.URL.Query().Get("s")) + + raw, err := json.Marshal(splitChanges) + assert.Nil(t, err) + + w.Write(raw) + })) + defer sdkServer.Close() + + var eventsCalls int32 + eventsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&eventsCalls, 1) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/testImpressions/bulk", r.URL.Path) + + body, err := io.ReadAll(r.Body) + assert.Nil(t, err) + + var imps []dtos.ImpressionsDTO + assert.Nil(t, json.Unmarshal(body, &imps)) + + assert.Equal(t, "not_exist", imps[0].TestName) + assert.Equal(t, 1, len(imps[0].KeyImpressions)) + w.WriteHeader(200) + })) + defer eventsServer.Close() + + telemetryServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/metrics/config": + metricsInitCalled++ + rBody, _ := ioutil.ReadAll(r.Body) + var dataInPost dtos.Config + err := json.Unmarshal(rBody, &dataInPost) + assert.Nil(t, err) + } + fmt.Fprintln(w, "ok") + })) + defer telemetryServer.Close() + + sdkConf := conf.DefaultConfig() + sdkConf.URLs.Events = eventsServer.URL + sdkConf.URLs.SDK = sdkServer.URL + sdkConf.URLs.Telemetry = telemetryServer.URL + sdkConf.StreamingEnabled = false + stringConfig := "flag1_config" + globalTreatment := "global_treatment" + flag1Treatment := "flag1_treatment" + sdkConf.FallbackTreatment = dtos.FallbackTreatmentConfig{ + GlobalFallbackTreatment: &dtos.FallbackTreatment{ + Treatment: &globalTreatment, + }, + ByFlagFallbackTreatment: map[string]dtos.FallbackTreatment{ + "flag1": { + Treatment: &flag1Treatment, + Config: &stringConfig, + }, + }, + } + + logger := logging.NewLogger(nil) + client, err := New(logger, "someApikey", sdkConf) + assert.Nil(t, err) + + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "not_exist", nil) + assert.Equal(t, "global_treatment", res1.Treatment) + + assert.Nil(t, client.Shutdown()) + assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) +} From f3c1ffd99c246ebefe47912c09133c852cd70503 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Tue, 20 Jan 2026 15:58:54 -0300 Subject: [PATCH 11/26] Add impression properties in treatments --- splitio/sdk/integration_test.go | 9 +++- splitio/sdk/mocks/sdk.go | 4 ++ splitio/sdk/sdk.go | 72 ++++++++++++++++++++++------ splitio/sdk/sdk_test.go | 83 +++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 14 deletions(-) diff --git a/splitio/sdk/integration_test.go b/splitio/sdk/integration_test.go index aba90b7..3de8e54 100644 --- a/splitio/sdk/integration_test.go +++ b/splitio/sdk/integration_test.go @@ -235,8 +235,15 @@ func TestInstantiationAndGetTreatmentE2EWithFallbackTreatment(t *testing.T) { client, err := New(logger, "someApikey", sdkConf) assert.Nil(t, err) - res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "not_exist", nil) + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "pleassssse": "holaaaaa", + }, + } + + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "not_exist", nil, client.WithEvaluationOptions(&opts)) assert.Equal(t, "global_treatment", res1.Treatment) + assert.Equal(t, "{\"pleassssse\":\"holaaaaa\"}", res1.Impression.Properties) assert.Nil(t, client.Shutdown()) assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) diff --git a/splitio/sdk/mocks/sdk.go b/splitio/sdk/mocks/sdk.go index bffb988..dd6abb6 100644 --- a/splitio/sdk/mocks/sdk.go +++ b/splitio/sdk/mocks/sdk.go @@ -17,6 +17,7 @@ func (m *SDKMock) Treatment( bucketingKey *string, feature string, attributes map[string]interface{}, + optFns ...sdk.OptFn, ) (*sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, feature, attributes) return args.Get(0).(*sdk.EvaluationResult), args.Error(1) @@ -29,6 +30,7 @@ func (m *SDKMock) Treatments( bucketingKey *string, features []string, attributes map[string]interface{}, + optFns ...sdk.OptFn, ) (map[string]sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, features, attributes) return args.Get(0).(map[string]sdk.EvaluationResult), args.Error(1) @@ -41,6 +43,7 @@ func (m *SDKMock) TreatmentsByFlagSet( bucketingKey *string, flagSet string, attributes map[string]interface{}, + optFns ...sdk.OptFn, ) (map[string]sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, flagSet, attributes) return args.Get(0).(map[string]sdk.EvaluationResult), args.Error(1) @@ -53,6 +56,7 @@ func (m *SDKMock) TreatmentsByFlagSets( bucketingKey *string, flagSets []string, attributes map[string]interface{}, + optFns ...sdk.OptFn, ) (map[string]sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, flagSets, attributes) return args.Get(0).(map[string]sdk.EvaluationResult), args.Error(1) diff --git a/splitio/sdk/sdk.go b/splitio/sdk/sdk.go index 51838d3..64febce 100644 --- a/splitio/sdk/sdk.go +++ b/splitio/sdk/sdk.go @@ -1,6 +1,7 @@ package sdk import ( + "encoding/json" "errors" "fmt" "time" @@ -48,10 +49,10 @@ var ruleBasedSegmentRules = []string{constants.MatcherTypeAllKeys, constants.Mat type Attributes = map[string]interface{} type Interface interface { - Treatment(cfg *types.ClientConfig, key string, bucketingKey *string, feature string, attributes map[string]interface{}) (*EvaluationResult, error) - Treatments(cfg *types.ClientConfig, key string, bucketingKey *string, features []string, attributes map[string]interface{}) (map[string]EvaluationResult, error) - TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bucketingKey *string, flagSet string, attributes map[string]interface{}) (map[string]EvaluationResult, error) - TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) (map[string]EvaluationResult, error) + Treatment(cfg *types.ClientConfig, key string, bucketingKey *string, feature string, attributes map[string]interface{}, optFns ...OptFn) (*EvaluationResult, error) + Treatments(cfg *types.ClientConfig, key string, bucketingKey *string, features []string, attributes map[string]interface{}, optFns ...OptFn) (map[string]EvaluationResult, error) + TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bucketingKey *string, flagSet string, attributes map[string]interface{}, optFns ...OptFn) (map[string]EvaluationResult, error) + TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}, optFns ...OptFn) (map[string]EvaluationResult, error) Track(cfg *types.ClientConfig, key string, trafficType string, eventType string, value *float64, properties map[string]interface{}) error SplitNames() ([]string, error) Splits() ([]SplitView, error) @@ -73,6 +74,22 @@ type Impl struct { validator Validator } +type options struct { + evaluationOptions *dtos.EvaluationOptions +} + +type OptFn = func(o *options) + +func (c *Impl) WithEvaluationOptions(e *dtos.EvaluationOptions) OptFn { + return func(o *options) { o.evaluationOptions = e } +} + +func defaultOpts() options { + return options{ + evaluationOptions: nil, + } +} + func New(logger logging.LoggerInterface, apikey string, c *conf.Config) (*Impl, error) { if warnings := c.Normalize(); len(warnings) > 0 { @@ -132,13 +149,14 @@ func New(logger logging.LoggerInterface, apikey string, c *conf.Config) (*Impl, } // Treatment implements Interface -func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, feature string, attributes Attributes) (*EvaluationResult, error) { +func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, feature string, attributes Attributes, optFns ...OptFn) (*EvaluationResult, error) { + options := getOptions(optFns...) res := i.ev.EvaluateFeature(key, bk, feature, attributes) if res == nil { return nil, fmt.Errorf("nil result") } - imp := i.handleImpression(key, bk, feature, res, cfg.Metadata) + imp := i.handleImpression(key, bk, feature, res, cfg.Metadata, serializeProperties(options.evaluationOptions)) return &EvaluationResult{ Treatment: res.Treatment, Impression: imp, @@ -146,9 +164,18 @@ func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, featur }, nil } +func getOptions(optFns ...OptFn) options { + options := defaultOpts() + for _, optFn := range optFns { + optFn(&options) + } + return options +} + // Treatment implements Interface -func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, features []string, attributes Attributes) (map[string]EvaluationResult, error) { +func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, features []string, attributes Attributes, optFns ...OptFn) (map[string]EvaluationResult, error) { + options := getOptions(optFns...) res := i.ev.EvaluateFeatures(key, bk, features, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for _, feature := range features { @@ -161,7 +188,7 @@ func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, featu var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, serializeProperties(options.evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -170,14 +197,15 @@ func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, featu } // TreatmentsByFlagSet implements Interface -func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *string, flagSet string, attributes Attributes) (map[string]EvaluationResult, error) { +func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *string, flagSet string, attributes Attributes, optFns ...OptFn) (map[string]EvaluationResult, error) { + options := getOptions(optFns...) res := i.ev.EvaluateFeatureByFlagSets(key, bk, []string{flagSet}, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for feature, curr := range res.Evaluations { var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, serializeProperties(options.evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -186,14 +214,15 @@ func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *stri } // TreatmentsByFlagSets implements Interface -func (i *Impl) TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bk *string, flagSets []string, attributes Attributes) (map[string]EvaluationResult, error) { +func (i *Impl) TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bk *string, flagSets []string, attributes Attributes, optFns ...OptFn) (map[string]EvaluationResult, error) { + options := getOptions(optFns...) res := i.ev.EvaluateFeatureByFlagSets(key, bk, flagSets, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for feature, curr := range res.Evaluations { var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, serializeProperties(options.evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -266,7 +295,7 @@ func (i *Impl) Shutdown() error { return nil } -func (i *Impl) handleImpression(key string, bk *string, f string, r *evaluator.Result, cm types.ClientMetadata) *dtos.Impression { +func (i *Impl) handleImpression(key string, bk *string, f string, r *evaluator.Result, cm types.ClientMetadata, properties string) *dtos.Impression { var label string if i.cfg.LabelsEnabled { label = r.Label @@ -281,6 +310,7 @@ func (i *Impl) handleImpression(key string, bk *string, f string, r *evaluator.R Treatment: r.Treatment, Time: timeMillis(), Disabled: r.ImpressionsDisabled, + Properties: properties, } forLog, _ := i.iq.Process([]dtos.Impression{imp}, false) @@ -337,4 +367,20 @@ func createFallbackTreatmentCalculator(fallbackTreatmentConfig *dtos.FallbackTre return dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConf) } +func serializeProperties(opts *dtos.EvaluationOptions) string { + if opts == nil { + return "" + } + if len(opts.Properties) == 0 { + return "" + } + + properties, err := json.Marshal(opts.Properties) + if err != nil { + return "" + } + + return string(properties) +} + var _ Interface = (*Impl)(nil) diff --git a/splitio/sdk/sdk_test.go b/splitio/sdk/sdk_test.go index 4079a1f..1202487 100644 --- a/splitio/sdk/sdk_test.go +++ b/splitio/sdk/sdk_test.go @@ -256,6 +256,88 @@ func TestTreatments(t *testing.T) { } +func TestTreatmentsWithImpressionProperties(t *testing.T) { + is, _ := storage.NewImpressionsQueue(100) + + ev := &mocks.EvaluatorMock{} + ev.On("EvaluateFeatures", "key1", (*string)(nil), []string{"f1", "f2", "f3"}, Attributes{"a": 1}). + Return(evaluator.Results{Evaluations: map[string]evaluator.Result{ + "f1": {Treatment: "on", Label: "label1", EvaluationTime: 1 * time.Millisecond, SplitChangeNumber: 123}, + "f2": {Treatment: "on", Label: "label2", EvaluationTime: 2 * time.Millisecond, SplitChangeNumber: 124}, + "f3": {Treatment: "on", Label: "label3", EvaluationTime: 3 * time.Millisecond, SplitChangeNumber: 125}, + }}). + Once() + + expectedImpressions := []dtos.Impression{ + {KeyName: "key1", BucketingKey: "", FeatureName: "f1", Treatment: "on", Label: "label1", ChangeNumber: 123, Properties: "{\"pleassssse\":\"holaaaaa\"}"}, + {KeyName: "key1", BucketingKey: "", FeatureName: "f2", Treatment: "on", Label: "label2", ChangeNumber: 124, Properties: "{\"pleassssse\":\"holaaaaa\"}"}, + {KeyName: "key1", BucketingKey: "", FeatureName: "f3", Treatment: "on", Label: "label3", ChangeNumber: 125, Properties: "{\"pleassssse\":\"holaaaaa\"}"}, + } + im := &mocks.ImpressionManagerMock{} + im.On("Process", mock.Anything). + Run(func(args mock.Arguments) { + assertImpEq(t, &expectedImpressions[0], &args.Get(0).([]dtos.Impression)[0]) + }). + Return([]dtos.Impression{expectedImpressions[0]}, []dtos.Impression{}). + Once() + im.On("Process", mock.Anything). + Run(func(args mock.Arguments) { + assertImpEq(t, &expectedImpressions[1], &args.Get(0).([]dtos.Impression)[0]) + }). + Return([]dtos.Impression{expectedImpressions[1]}, []dtos.Impression{}). + Once() + im.On("Process", mock.Anything). + Run(func(args mock.Arguments) { + assertImpEq(t, &expectedImpressions[2], &args.Get(0).([]dtos.Impression)[0]) + }). + Return([]dtos.Impression{expectedImpressions[2]}, []dtos.Impression{}). + Once() + + client := &Impl{ + logger: logging.NewLogger(nil), + is: is, + ev: ev, + iq: im, + cfg: conf.Config{LabelsEnabled: true}, + } + + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "pleassssse": "holaaaaa", + }, + } + res, err := client.Treatments( + &types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, + "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}, client.WithEvaluationOptions(&opts)) + assert.Nil(t, err) + assert.Nil(t, res["f1"].Config) + assert.Nil(t, res["f2"].Config) + assert.Nil(t, res["f3"].Config) + assertImpEq(t, &expectedImpressions[0], res["f1"].Impression) + assertImpEq(t, &expectedImpressions[1], res["f2"].Impression) + assertImpEq(t, &expectedImpressions[2], res["f3"].Impression) + + err = is.RangeAndClear(func(md types.ClientMetadata, st *storage.LockingQueue[dtos.Impression]) { + assert.Equal(t, types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}, md) + assert.Equal(t, 3, st.Len()) + + var imps []dtos.Impression + n, _ := st.Pop(3, &imps) + assert.Nil(t, nil) + assert.Equal(t, 3, n) + assert.Equal(t, 3, len(imps)) + assertImpEq(t, &expectedImpressions[0], &imps[0]) + assertImpEq(t, &expectedImpressions[1], &imps[1]) + assertImpEq(t, &expectedImpressions[2], &imps[2]) + n, err = st.Pop(1, &imps) + assert.Equal(t, 0, n) + assert.ErrorIs(t, err, storage.ErrQueueEmpty) + + }) + assert.Nil(t, err) + +} + func TestTreatmentsByFlagSet(t *testing.T) { is, _ := storage.NewImpressionsQueue(100) @@ -726,6 +808,7 @@ func assertImpEq(t *testing.T, i1, i2 *dtos.Impression) { assert.Equal(t, i1.Treatment, i2.Treatment) assert.Equal(t, i1.Label, i2.Label) assert.Equal(t, i1.ChangeNumber, i2.ChangeNumber) + assert.Equal(t, i1.Properties, i2.Properties) } func assertEventEq(t *testing.T, e1, e2 *dtos.EventDTO) { From 230dcb41d1013feee17c0f74e723322df6e238ee Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 21 Jan 2026 14:34:38 -0300 Subject: [PATCH 12/26] Added test cases for prerequistes --- splitio/sdk/integration_test.go | 256 ++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/splitio/sdk/integration_test.go b/splitio/sdk/integration_test.go index 3de8e54..ab59dbd 100644 --- a/splitio/sdk/integration_test.go +++ b/splitio/sdk/integration_test.go @@ -248,3 +248,259 @@ func TestInstantiationAndGetTreatmentE2EWithFallbackTreatment(t *testing.T) { assert.Nil(t, client.Shutdown()) assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) } + +func TestInstantiationAndGetTreatmentE2EWithPrerequistesNotAchive(t *testing.T) { + metricsInitCalled := 0 + mockedSplit1 := dtos.SplitDTO{ + Algo: 2, + ChangeNumber: 123, + DefaultTreatment: "default", + Killed: false, + Name: "split", + Seed: 1234, + Status: "ACTIVE", + TrafficAllocation: 1, + TrafficAllocationSeed: -1667452163, + TrafficTypeName: "tt1", + Prerequisites: []dtos.Prerequisite{ + { + FeatureFlagName: "ff1", + Treatments: []string{ + "off", + "v1", + }, + }, + }, + Conditions: []dtos.ConditionDTO{ + { + ConditionType: "ROLLOUT", + Label: "in segment all", + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{{MatcherType: "ALL_KEYS"}}, + }, + Partitions: []dtos.PartitionDTO{{Size: 100, Treatment: "on"}}, + }, + }, + } + + mockedSplit2 := dtos.SplitDTO{ + Algo: 2, + ChangeNumber: 123, + DefaultTreatment: "off", + Killed: false, + Name: "ff1", + Seed: 1234, + Status: "ACTIVE", + TrafficAllocation: 1, + TrafficAllocationSeed: -1667452163, + TrafficTypeName: "tt1", + Conditions: []dtos.ConditionDTO{ + { + ConditionType: "ROLLOUT", + Label: "in segment all", + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{{MatcherType: "ALL_KEYS"}}, + }, + Partitions: []dtos.PartitionDTO{{Size: 100, Treatment: "on"}}, + }, + }, + } + + sdkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/splitChanges", r.URL.Path) + + splitChanges := dtos.RuleChangesDTO{ + FeatureFlags: dtos.FeatureFlagsDTO{ + Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2}, + Since: 3, + Till: 3, + }, + } + + assert.Equal(t, "-1", r.URL.Query().Get("since")) + assert.Equal(t, specs.FLAG_V1_3, r.URL.Query().Get("s")) + + raw, err := json.Marshal(splitChanges) + assert.Nil(t, err) + + w.Write(raw) + })) + defer sdkServer.Close() + + var eventsCalls int32 + eventsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&eventsCalls, 1) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/testImpressions/bulk", r.URL.Path) + + body, err := io.ReadAll(r.Body) + assert.Nil(t, err) + + var imps []dtos.ImpressionsDTO + assert.Nil(t, json.Unmarshal(body, &imps)) + + assert.Equal(t, "split", imps[0].TestName) + assert.Equal(t, 1, len(imps[0].KeyImpressions)) + w.WriteHeader(200) + })) + defer eventsServer.Close() + + telemetryServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/metrics/config": + metricsInitCalled++ + rBody, _ := ioutil.ReadAll(r.Body) + var dataInPost dtos.Config + err := json.Unmarshal(rBody, &dataInPost) + assert.Nil(t, err) + } + fmt.Fprintln(w, "ok") + })) + defer telemetryServer.Close() + + sdkConf := conf.DefaultConfig() + sdkConf.URLs.Events = eventsServer.URL + sdkConf.URLs.SDK = sdkServer.URL + sdkConf.URLs.Telemetry = telemetryServer.URL + sdkConf.StreamingEnabled = false + logger := logging.NewLogger(nil) + client, err := New(logger, "someApikey", sdkConf) + assert.Nil(t, err) + + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil) + assert.Equal(t, "default", res1.Treatment) + + assert.Nil(t, client.Shutdown()) + assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) +} + +func TestInstantiationAndGetTreatmentE2EWithPrerequistesAchive(t *testing.T) { + metricsInitCalled := 0 + mockedSplit1 := dtos.SplitDTO{ + Algo: 2, + ChangeNumber: 123, + DefaultTreatment: "default", + Killed: false, + Name: "split", + Seed: 1234, + Status: "ACTIVE", + TrafficAllocation: 1, + TrafficAllocationSeed: -1667452163, + TrafficTypeName: "tt1", + Prerequisites: []dtos.Prerequisite{ + { + FeatureFlagName: "ff1", + Treatments: []string{ + "on", + "v1", + }, + }, + }, + Conditions: []dtos.ConditionDTO{ + { + ConditionType: "ROLLOUT", + Label: "in segment all", + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{{MatcherType: "ALL_KEYS"}}, + }, + Partitions: []dtos.PartitionDTO{{Size: 100, Treatment: "on"}}, + }, + }, + } + + mockedSplit2 := dtos.SplitDTO{ + Algo: 2, + ChangeNumber: 123, + DefaultTreatment: "off", + Killed: false, + Name: "ff1", + Seed: 1234, + Status: "ACTIVE", + TrafficAllocation: 1, + TrafficAllocationSeed: -1667452163, + TrafficTypeName: "tt1", + Conditions: []dtos.ConditionDTO{ + { + ConditionType: "ROLLOUT", + Label: "in segment all", + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{{MatcherType: "ALL_KEYS"}}, + }, + Partitions: []dtos.PartitionDTO{{Size: 100, Treatment: "on"}}, + }, + }, + } + + sdkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/splitChanges", r.URL.Path) + + splitChanges := dtos.RuleChangesDTO{ + FeatureFlags: dtos.FeatureFlagsDTO{ + Splits: []dtos.SplitDTO{mockedSplit1, mockedSplit2}, + Since: 3, + Till: 3, + }, + } + + assert.Equal(t, "-1", r.URL.Query().Get("since")) + assert.Equal(t, specs.FLAG_V1_3, r.URL.Query().Get("s")) + + raw, err := json.Marshal(splitChanges) + assert.Nil(t, err) + + w.Write(raw) + })) + defer sdkServer.Close() + + var eventsCalls int32 + eventsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&eventsCalls, 1) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/testImpressions/bulk", r.URL.Path) + + body, err := io.ReadAll(r.Body) + assert.Nil(t, err) + + var imps []dtos.ImpressionsDTO + assert.Nil(t, json.Unmarshal(body, &imps)) + + assert.Equal(t, "split", imps[0].TestName) + assert.Equal(t, 1, len(imps[0].KeyImpressions)) + w.WriteHeader(200) + })) + defer eventsServer.Close() + + telemetryServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/metrics/config": + metricsInitCalled++ + rBody, _ := ioutil.ReadAll(r.Body) + var dataInPost dtos.Config + err := json.Unmarshal(rBody, &dataInPost) + assert.Nil(t, err) + } + fmt.Fprintln(w, "ok") + })) + defer telemetryServer.Close() + + sdkConf := conf.DefaultConfig() + sdkConf.URLs.Events = eventsServer.URL + sdkConf.URLs.SDK = sdkServer.URL + sdkConf.URLs.Telemetry = telemetryServer.URL + sdkConf.StreamingEnabled = false + logger := logging.NewLogger(nil) + client, err := New(logger, "someApikey", sdkConf) + assert.Nil(t, err) + + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil) + assert.Equal(t, "on", res1.Treatment) + + assert.Nil(t, client.Shutdown()) + assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) +} From b5a9e87491ef99dcdd165ff4fabd1bb8d59ccdb7 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 21 Jan 2026 15:58:26 -0300 Subject: [PATCH 13/26] Updated changelog and version --- CHANGES | 6 ++++++ splitio/version.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index a2cb5e2..700c21f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,9 @@ +1.7.0 (Jan 21, 2026) +- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. +- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. +- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. +- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. + 1.6.0 (Feb 5, 2025) - Added Health & Readiness endpoints. - Fixing vulnerabilities. diff --git a/splitio/version.go b/splitio/version.go index fff15bc..e8af325 100644 --- a/splitio/version.go +++ b/splitio/version.go @@ -1,3 +1,3 @@ package splitio -const Version = "1.6.3" +const Version = "1.7.0-rc" From 2a0d9abbcc94f297a2464605b97e99fb3b98ddae Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 21 Jan 2026 17:20:13 -0300 Subject: [PATCH 14/26] Added tests for rule-based --- splitio/sdk/integration_test.go | 151 ++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/splitio/sdk/integration_test.go b/splitio/sdk/integration_test.go index ab59dbd..97ccdaf 100644 --- a/splitio/sdk/integration_test.go +++ b/splitio/sdk/integration_test.go @@ -504,3 +504,154 @@ func TestInstantiationAndGetTreatmentE2EWithPrerequistesAchive(t *testing.T) { assert.Nil(t, client.Shutdown()) assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) } + +func TestInstantiationAndGetTreatmentE2EWithRBS(t *testing.T) { + metricsInitCalled := 0 + mockedSplit1 := dtos.SplitDTO{ + Algo: 2, + ChangeNumber: 123, + DefaultTreatment: "default", + Killed: false, + Name: "split", + Seed: 1234, + Status: "ACTIVE", + TrafficAllocation: 1, + TrafficAllocationSeed: -1667452163, + TrafficTypeName: "tt1", + Conditions: []dtos.ConditionDTO{ + { + ConditionType: "ROLLOUT", + Label: "default rule", + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{ + { + KeySelector: &dtos.KeySelectorDTO{ + TrafficType: "user", + }, + MatcherType: "IN_RULE_BASED_SEGMENT", + UserDefinedSegment: &dtos.UserDefinedSegmentMatcherDataDTO{ + SegmentName: "rbsegment1", + }, + Negate: false, + }, + }, + }, + Partitions: []dtos.PartitionDTO{ + { + Size: 100, + Treatment: "on", + }, + { + Size: 0, + Treatment: "off", + }, + }, + }, + }, + } + + semver := "3.4.5" + attribute := "version" + + rbsegment1 := dtos.RuleBasedSegmentDTO{ + Name: "rbsegment1", + Status: "ACTIVE", + Conditions: []dtos.RuleBasedConditionDTO{ + { + MatcherGroup: dtos.MatcherGroupDTO{ + Combiner: "AND", + Matchers: []dtos.MatcherDTO{ + { + KeySelector: &dtos.KeySelectorDTO{ + TrafficType: "user", + Attribute: &attribute, + }, + MatcherType: "EQUAL_TO_SEMVER", + String: &semver, + Whitelist: nil, + Negate: false, + }, + }, + }, + }, + }, + TrafficTypeName: "user", + } + + sdkServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "/splitChanges", r.URL.Path) + + splitChanges := dtos.RuleChangesDTO{ + FeatureFlags: dtos.FeatureFlagsDTO{ + Splits: []dtos.SplitDTO{mockedSplit1}, + Since: 3, + Till: 3, + }, + RuleBasedSegments: dtos.RuleBasedSegmentsDTO{ + RuleBasedSegments: []dtos.RuleBasedSegmentDTO{rbsegment1}, + Since: 3, + Till: 3, + }, + } + + assert.Equal(t, "-1", r.URL.Query().Get("since")) + assert.Equal(t, specs.FLAG_V1_3, r.URL.Query().Get("s")) + + raw, err := json.Marshal(splitChanges) + assert.Nil(t, err) + + w.Write(raw) + })) + defer sdkServer.Close() + + var eventsCalls int32 + eventsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&eventsCalls, 1) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/testImpressions/bulk", r.URL.Path) + + body, err := io.ReadAll(r.Body) + assert.Nil(t, err) + + var imps []dtos.ImpressionsDTO + assert.Nil(t, json.Unmarshal(body, &imps)) + + assert.Equal(t, "split", imps[0].TestName) + assert.Equal(t, 1, len(imps[0].KeyImpressions)) + w.WriteHeader(200) + })) + defer eventsServer.Close() + + telemetryServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/metrics/config": + metricsInitCalled++ + rBody, _ := ioutil.ReadAll(r.Body) + var dataInPost dtos.Config + err := json.Unmarshal(rBody, &dataInPost) + assert.Nil(t, err) + } + fmt.Fprintln(w, "ok") + })) + defer telemetryServer.Close() + + sdkConf := conf.DefaultConfig() + sdkConf.URLs.Events = eventsServer.URL + sdkConf.URLs.SDK = sdkServer.URL + sdkConf.URLs.Telemetry = telemetryServer.URL + sdkConf.StreamingEnabled = false + + logger := logging.NewLogger(nil) + client, err := New(logger, "someApikey", sdkConf) + assert.Nil(t, err) + attributes := make(map[string]interface{}) + attributes["version"] = "3.4.5" + + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", attributes) + assert.Equal(t, "on", res1.Treatment) + + assert.Nil(t, client.Shutdown()) + assert.Equal(t, int32(1), atomic.LoadInt32(&eventsCalls)) +} From d41fa88996d4f541282ccd984ecbdc8c8f46d6c7 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 22 Jan 2026 15:48:37 -0300 Subject: [PATCH 15/26] [FME-12564] Added impression properties in link package --- splitio/link/client/types/interfaces.go | 14 +++++--- splitio/link/client/v1/impl.go | 44 +++++++++++++++++++------ splitio/link/client/v1/impl_test.go | 9 ++++- splitio/sdk/sdk.go | 10 +++--- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/splitio/link/client/types/interfaces.go b/splitio/link/client/types/interfaces.go index 4e11855..a3da0bc 100644 --- a/splitio/link/client/types/interfaces.go +++ b/splitio/link/client/types/interfaces.go @@ -6,10 +6,10 @@ import ( ) type ClientInterface interface { - Treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}) (*Result, error) - Treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}) (Results, error) - TreatmentWithConfig(key string, bucketingKey string, feature string, attrs map[string]interface{}) (*Result, error) - TreatmentsWithConfig(key string, bucketingKey string, features []string, attrs map[string]interface{}) (Results, error) + Treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}, optFns ...OptFn) (*Result, error) + Treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}, optFns ...OptFn) (Results, error) + TreatmentWithConfig(key string, bucketingKey string, feature string, attrs map[string]interface{}, optFns ...OptFn) (*Result, error) + TreatmentsWithConfig(key string, bucketingKey string, features []string, attrs map[string]interface{}, optFns ...OptFn) (Results, error) Track(key string, trafficType string, eventType string, value *float64, properties map[string]interface{}) error SplitNames() ([]string, error) Split(name string) (*sdk.SplitView, error) @@ -24,3 +24,9 @@ type Result struct { } type Results = map[string]Result + +type Options struct { + EvaluationOptions *dtos.EvaluationOptions +} + +type OptFn = func(o *Options) diff --git a/splitio/link/client/v1/impl.go b/splitio/link/client/v1/impl.go index fba91ce..e33d070 100644 --- a/splitio/link/client/v1/impl.go +++ b/splitio/link/client/v1/impl.go @@ -26,6 +26,16 @@ type Impl struct { listenerFeedback bool } +func (c *Impl) WithEvaluationOptions(e *dtos.EvaluationOptions) types.OptFn { + return func(o *types.Options) { o.EvaluationOptions = e } +} + +func defaultOpts() types.Options { + return types.Options{ + EvaluationOptions: nil, + } +} + func New(id string, logger logging.LoggerInterface, conn transfer.RawConn, serializer serializer.Interface, listenerFeedback bool) (*Impl, error) { i := &Impl{ logger: logger, @@ -43,23 +53,27 @@ func New(id string, logger logging.LoggerInterface, conn transfer.RawConn, seria } // Treatment implements Interface -func (c *Impl) Treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}) (*types.Result, error) { - return c.treatment(key, bucketingKey, feature, attrs, false) +func (c *Impl) Treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}, optFns ...types.OptFn) (*types.Result, error) { + options := getOptions(optFns...) + return c.treatment(key, bucketingKey, feature, attrs, false, options.EvaluationOptions) } // TreatmentWithConfig implements types.ClientInterface -func (c *Impl) TreatmentWithConfig(key string, bucketingKey string, feature string, attrs map[string]interface{}) (*types.Result, error) { - return c.treatment(key, bucketingKey, feature, attrs, true) +func (c *Impl) TreatmentWithConfig(key string, bucketingKey string, feature string, attrs map[string]interface{}, optFns ...types.OptFn) (*types.Result, error) { + options := getOptions(optFns...) + return c.treatment(key, bucketingKey, feature, attrs, true, options.EvaluationOptions) } // Treatment implements Interface -func (c *Impl) Treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}) (types.Results, error) { - return c.treatments(key, bucketingKey, features, attrs, false) +func (c *Impl) Treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}, optFns ...types.OptFn) (types.Results, error) { + options := getOptions(optFns...) + return c.treatments(key, bucketingKey, features, attrs, false, options.EvaluationOptions) } // TreatmentsWithConfig implements types.ClientInterface -func (c *Impl) TreatmentsWithConfig(key string, bucketingKey string, features []string, attrs map[string]interface{}) (types.Results, error) { - return c.treatments(key, bucketingKey, features, attrs, true) +func (c *Impl) TreatmentsWithConfig(key string, bucketingKey string, features []string, attrs map[string]interface{}, optFns ...types.OptFn) (types.Results, error) { + options := getOptions(optFns...) + return c.treatments(key, bucketingKey, features, attrs, true, options.EvaluationOptions) } // Track implements types.ClientInterface @@ -139,7 +153,7 @@ func (c *Impl) Splits() ([]sdk.SplitView, error) { return views, nil } -func (c *Impl) treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}, withConfig bool) (*types.Result, error) { +func (c *Impl) treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}, withConfig bool, evaluationOptions *dtos.EvaluationOptions) (*types.Result, error) { var bkp *string if bucketingKey != "" { bkp = &bucketingKey @@ -174,6 +188,7 @@ func (c *Impl) treatment(key string, bucketingKey string, feature string, attrs ChangeNumber: resp.Payload.ListenerData.ChangeNumber, Label: resp.Payload.ListenerData.Label, BucketingKey: bucketingKey, + Properties: sdk.SerializeProperties(evaluationOptions), } } @@ -185,7 +200,7 @@ func (c *Impl) treatment(key string, bucketingKey string, feature string, attrs return toRet, nil } -func (c *Impl) treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}, withConfig bool) (types.Results, error) { +func (c *Impl) treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}, withConfig bool, evaluationOptions *dtos.EvaluationOptions) (types.Results, error) { var bkp *string if bucketingKey != "" { bkp = &bucketingKey @@ -221,6 +236,7 @@ func (c *Impl) treatments(key string, bucketingKey string, features []string, at ChangeNumber: resp.Payload.Results[idx].ListenerData.ChangeNumber, Label: resp.Payload.Results[idx].ListenerData.Label, BucketingKey: bucketingKey, + Properties: sdk.SerializeProperties(evaluationOptions), } } @@ -286,4 +302,12 @@ func (c *Impl) Shutdown() error { return c.conn.Shutdown() } +func getOptions(optFns ...types.OptFn) types.Options { + options := defaultOpts() + for _, optFn := range optFns { + optFn(&options) + } + return options +} + var _ types.ClientInterface = (*Impl)(nil) diff --git a/splitio/link/client/v1/impl_test.go b/splitio/link/client/v1/impl_test.go index 2bdbee6..d9460af 100644 --- a/splitio/link/client/v1/impl_test.go +++ b/splitio/link/client/v1/impl_test.go @@ -144,7 +144,12 @@ func TestClientGetTreatmentWithImpression(t *testing.T) { assert.NotNil(t, client) assert.Nil(t, err) - res, err := client.Treatment("key1", "buck1", "feat1", map[string]interface{}{"a": 1}) + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "pleassssse": "holaaaaa", + }, + } + res, err := client.Treatment("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, client.WithEvaluationOptions(&opts)) assert.Nil(t, err) assert.Equal(t, "on", res.Treatment) validateImpression(t, &dtos.Impression{ @@ -155,6 +160,7 @@ func TestClientGetTreatmentWithImpression(t *testing.T) { Label: "l1", ChangeNumber: 1234, Time: 123, + Properties: "{\"pleassssse\":\"holaaaaa\"}", }, res.Impression) } @@ -435,5 +441,6 @@ func validateImpression(t *testing.T, expected *dtos.Impression, actual *dtos.Im assert.Equal(t, expected.Time, actual.Time) assert.Equal(t, expected.Treatment, actual.Treatment) assert.Equal(t, expected.Label, actual.Label) + assert.Equal(t, expected.Properties, actual.Properties) } diff --git a/splitio/sdk/sdk.go b/splitio/sdk/sdk.go index 64febce..af8e6bb 100644 --- a/splitio/sdk/sdk.go +++ b/splitio/sdk/sdk.go @@ -156,7 +156,7 @@ func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, featur return nil, fmt.Errorf("nil result") } - imp := i.handleImpression(key, bk, feature, res, cfg.Metadata, serializeProperties(options.evaluationOptions)) + imp := i.handleImpression(key, bk, feature, res, cfg.Metadata, SerializeProperties(options.evaluationOptions)) return &EvaluationResult{ Treatment: res.Treatment, Impression: imp, @@ -188,7 +188,7 @@ func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, featu var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, serializeProperties(options.evaluationOptions)) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(options.evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -205,7 +205,7 @@ func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *stri for feature, curr := range res.Evaluations { var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, serializeProperties(options.evaluationOptions)) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(options.evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -222,7 +222,7 @@ func (i *Impl) TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bk *str for feature, curr := range res.Evaluations { var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, serializeProperties(options.evaluationOptions)) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(options.evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -367,7 +367,7 @@ func createFallbackTreatmentCalculator(fallbackTreatmentConfig *dtos.FallbackTre return dtos.NewFallbackTreatmentCalculatorImp(&fallbackTreatmentConf) } -func serializeProperties(opts *dtos.EvaluationOptions) string { +func SerializeProperties(opts *dtos.EvaluationOptions) string { if opts == nil { return "" } From 0050c336f68e71abe3c6c3785c39fed772d85b3b Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 26 Jan 2026 10:51:42 -0300 Subject: [PATCH 16/26] Updated sdk and link --- splitio/link/client/v1/impl_test.go | 6 +- splitio/link/protocol/v1/mocks/mocks.go | 4 +- splitio/link/protocol/v1/rpcs.go | 106 ++++++++++++++++-------- splitio/link/service/v1/clientmgr.go | 9 +- splitio/sdk/integration_test.go | 10 +-- splitio/sdk/mocks/sdk.go | 9 +- splitio/sdk/sdk.go | 52 +++--------- splitio/sdk/sdk_test.go | 20 ++--- 8 files changed, 115 insertions(+), 101 deletions(-) diff --git a/splitio/link/client/v1/impl_test.go b/splitio/link/client/v1/impl_test.go index d9460af..8801c14 100644 --- a/splitio/link/client/v1/impl_test.go +++ b/splitio/link/client/v1/impl_test.go @@ -31,7 +31,7 @@ func TestClientGetTreatmentNoImpression(t *testing.T) { *args.Get(1).(*v1.ResponseWrapper[v1.RegisterPayload]) = v1.ResponseWrapper[v1.RegisterPayload]{Status: v1.ResultOk} }).Once() - serializerMock.On("Serialize", proto1Mocks.NewTreatmentRPC("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, false)). + serializerMock.On("Serialize", proto1Mocks.NewTreatmentRPC("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, nil, false)). Return([]byte("treatmentMessage"), nil).Once() serializerMock.On("Parse", []byte("treatmentResult"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { *args.Get(1).(*v1.ResponseWrapper[v1.TreatmentPayload]) = v1.ResponseWrapper[v1.TreatmentPayload]{ @@ -65,7 +65,7 @@ func TestClientGetTreatmentWithConfig(t *testing.T) { *args.Get(1).(*v1.ResponseWrapper[v1.RegisterPayload]) = v1.ResponseWrapper[v1.RegisterPayload]{Status: v1.ResultOk} }).Once() - serializerMock.On("Serialize", proto1Mocks.NewTreatmentRPC("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, true)). + serializerMock.On("Serialize", proto1Mocks.NewTreatmentRPC("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, nil, true)). Return([]byte("treatmentWithConfigMessage"), nil).Once() serializerMock.On("Parse", []byte("treatmentWithConfigResult"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { *args.Get(1).(*v1.ResponseWrapper[v1.TreatmentPayload]) = v1.ResponseWrapper[v1.TreatmentPayload]{ @@ -129,7 +129,7 @@ func TestClientGetTreatmentWithImpression(t *testing.T) { *args.Get(1).(*v1.ResponseWrapper[v1.RegisterPayload]) = v1.ResponseWrapper[v1.RegisterPayload]{Status: v1.ResultOk} }).Once() - serializerMock.On("Serialize", proto1Mocks.NewTreatmentRPC("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, false)). + serializerMock.On("Serialize", proto1Mocks.NewTreatmentRPC("key1", "buck1", "feat1", map[string]interface{}{"a": 1}, nil, false)). Return([]byte("treatmentMessage"), nil).Once() serializerMock.On("Parse", []byte("treatmentResult"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { *args.Get(1).(*v1.ResponseWrapper[v1.TreatmentPayload]) = v1.ResponseWrapper[v1.TreatmentPayload]{ diff --git a/splitio/link/protocol/v1/mocks/mocks.go b/splitio/link/protocol/v1/mocks/mocks.go index cf56663..1473e99 100644 --- a/splitio/link/protocol/v1/mocks/mocks.go +++ b/splitio/link/protocol/v1/mocks/mocks.go @@ -22,11 +22,11 @@ func NewRegisterRPC(id string, listener bool) *v1.RPC { } } -func NewTreatmentRPC(key string, bucketing string, feature string, attrs map[string]interface{}, withConfig bool) *v1.RPC { +func NewTreatmentRPC(key string, bucketing string, feature string, attrs map[string]interface{}, impressionProperties map[string]interface{}, withConfig bool) *v1.RPC { rpc := &v1.RPC{ RPCBase: protocol.RPCBase{Version: protocol.V1}, OpCode: v1.OCTreatment, - Args: []interface{}{key, bucketing, feature, attrs}, + Args: []interface{}{key, bucketing, feature, attrs, impressionProperties}, } if withConfig { rpc.OpCode = v1.OCTreatmentWithConfig diff --git a/splitio/link/protocol/v1/rpcs.go b/splitio/link/protocol/v1/rpcs.go index 2a8a741..1c2ab23 100644 --- a/splitio/link/protocol/v1/rpcs.go +++ b/splitio/link/protocol/v1/rpcs.go @@ -123,17 +123,19 @@ func (r *RegisterArgs) PopulateFromRPC(rpc *RPC) error { } const ( - TreatmentArgKeyIdx int = 0 - TreatmentArgBucketingKeyIdx int = 1 - TreatmentArgFeatureIdx int = 2 - TreatmentArgAttributesIdx int = 3 + TreatmentArgKeyIdx int = 0 + TreatmentArgBucketingKeyIdx int = 1 + TreatmentArgFeatureIdx int = 2 + TreatmentArgAttributesIdx int = 3 + TreatmentArgImpressionPropertiesIdx int = 4 ) type TreatmentArgs struct { - Key string `msgpack:"k"` - BucketingKey *string `msgpack:"b"` - Feature string `msgpack:"f"` - Attributes map[string]interface{} `msgpack:"a"` + Key string `msgpack:"k"` + BucketingKey *string `msgpack:"b"` + Feature string `msgpack:"f"` + Attributes map[string]interface{} `msgpack:"a"` + ImpressionProperties map[string]interface{} `msgpack:"i"` } func (r TreatmentArgs) Encode() []interface{} { @@ -141,7 +143,7 @@ func (r TreatmentArgs) Encode() []interface{} { if r.BucketingKey != nil { bk = *r.BucketingKey } - return []interface{}{r.Key, bk, r.Feature, r.Attributes} + return []interface{}{r.Key, bk, r.Feature, r.Attributes, r.ImpressionProperties} } func (t *TreatmentArgs) PopulateFromRPC(rpc *RPC) error { @@ -177,21 +179,31 @@ func (t *TreatmentArgs) PopulateFromRPC(rpc *RPC) error { t.Attributes = sanitizeAttributes(rawAttrs) } + if len(rpc.Args) == 5 && rpc.Args[TreatmentArgImpressionPropertiesIdx] != nil { + rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentArgImpressionPropertiesIdx]) + if err != nil { + return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentArgImpressionPropertiesIdx)} + } + t.ImpressionProperties = rawAttrs + } + return nil } const ( - TreatmentsArgKeyIdx int = 0 - TreatmentsArgBucketingKeyIdx int = 1 - TreatmentsArgFeaturesIdx int = 2 - TreatmentsArgAttributesIdx int = 3 + TreatmentsArgKeyIdx int = 0 + TreatmentsArgBucketingKeyIdx int = 1 + TreatmentsArgFeaturesIdx int = 2 + TreatmentsArgAttributesIdx int = 3 + TreatmentsArgImpressionPropertiesIdx int = 4 ) type TreatmentsArgs struct { - Key string `msgpack:"k"` - BucketingKey *string `msgpack:"b"` - Features []string `msgpack:"f"` - Attributes map[string]interface{} `msgpack:"a"` + Key string `msgpack:"k"` + BucketingKey *string `msgpack:"b"` + Features []string `msgpack:"f"` + Attributes map[string]interface{} `msgpack:"a"` + ImpressionProperties map[string]interface{} `msgpack:"i"` } func (r TreatmentsArgs) Encode() []interface{} { @@ -238,21 +250,31 @@ func (t *TreatmentsArgs) PopulateFromRPC(rpc *RPC) error { } t.Attributes = sanitizeAttributes(rawAttrs) + if len(rpc.Args) == 5 && rpc.Args[TreatmentsArgImpressionPropertiesIdx] != nil { + rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentsArgImpressionPropertiesIdx]) + if err != nil { + return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsArgImpressionPropertiesIdx)} + } + t.ImpressionProperties = rawAttrs + } + return nil } const ( - TreatmentsByFlagSetArgKeyIdx int = 0 - TreatmentsByFlagSetArgBucketingKeyIdx int = 1 - TreatmentsByFlagSetArgFlagSetIdx int = 2 - TreatmentsByFlagSetArgAttributesIdx int = 3 + TreatmentsByFlagSetArgKeyIdx int = 0 + TreatmentsByFlagSetArgBucketingKeyIdx int = 1 + TreatmentsByFlagSetArgFlagSetIdx int = 2 + TreatmentsByFlagSetArgAttributesIdx int = 3 + TreatmentsByFlagSetArgImpressionPropertiesIdx int = 4 ) type TreatmentsByFlagSetArgs struct { - Key string `msgpack:"k"` - BucketingKey *string `msgpack:"b"` - FlagSet string `msgpack:"f"` - Attributes map[string]interface{} `msgpack:"a"` + Key string `msgpack:"k"` + BucketingKey *string `msgpack:"b"` + FlagSet string `msgpack:"f"` + Attributes map[string]interface{} `msgpack:"a"` + ImpressionProperties map[string]interface{} `msgpack:"i"` } func (r TreatmentsByFlagSetArgs) Encode() []interface{} { @@ -292,21 +314,31 @@ func (t *TreatmentsByFlagSetArgs) PopulateFromRPC(rpc *RPC) error { } t.Attributes = sanitizeAttributes(rawAttrs) + if len(rpc.Args) == 5 && rpc.Args[TreatmentsByFlagSetArgImpressionPropertiesIdx] != nil { + rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentsByFlagSetArgImpressionPropertiesIdx]) + if err != nil { + return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsByFlagSetArgImpressionPropertiesIdx)} + } + t.ImpressionProperties = rawAttrs + } + return nil } const ( - TreatmentsByFlagSetsArgKeyIdx int = 0 - TreatmentsByFlagSetsArgBucketingKeyIdx int = 1 - TreatmentsByFlagSetsArgFlagSetsIdx int = 2 - TreatmentsByFlagSetsArgAttributesIdx int = 3 + TreatmentsByFlagSetsArgKeyIdx int = 0 + TreatmentsByFlagSetsArgBucketingKeyIdx int = 1 + TreatmentsByFlagSetsArgFlagSetsIdx int = 2 + TreatmentsByFlagSetsArgAttributesIdx int = 3 + TreatmentsByFlagSetsArgImpressionPropertiesIdx int = 4 ) type TreatmentsByFlagSetsArgs struct { - Key string `msgpack:"k"` - BucketingKey *string `msgpack:"b"` - FlagSets []string `msgpack:"f"` - Attributes map[string]interface{} `msgpack:"a"` + Key string `msgpack:"k"` + BucketingKey *string `msgpack:"b"` + FlagSets []string `msgpack:"f"` + Attributes map[string]interface{} `msgpack:"a"` + ImpressionProperties map[string]interface{} `msgpack:"i"` } func (r TreatmentsByFlagSetsArgs) Encode() []interface{} { @@ -352,6 +384,14 @@ func (t *TreatmentsByFlagSetsArgs) PopulateFromRPC(rpc *RPC) error { } t.Attributes = sanitizeAttributes(rawAttrs) + if len(rpc.Args) == 5 && rpc.Args[TreatmentsByFlagSetsArgImpressionPropertiesIdx] != nil { + rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentsByFlagSetsArgImpressionPropertiesIdx]) + if err != nil { + return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsByFlagSetsArgImpressionPropertiesIdx)} + } + t.ImpressionProperties = rawAttrs + } + return nil } diff --git a/splitio/link/service/v1/clientmgr.go b/splitio/link/service/v1/clientmgr.go index 6c74f4f..eb2388b 100644 --- a/splitio/link/service/v1/clientmgr.go +++ b/splitio/link/service/v1/clientmgr.go @@ -7,6 +7,7 @@ import ( "os" "runtime/debug" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-toolkit/v5/logging" protov1 "github.com/splitio/splitd/splitio/link/protocol/v1" @@ -169,7 +170,7 @@ func (m *ClientManager) handleGetTreatment(rpc *protov1.RPC, withConfig bool) (i return nil, fmt.Errorf("error parsing treatment arguments: %w", err) } - res, err := m.splitSDK.Treatment(m.clientConfig, args.Key, args.BucketingKey, args.Feature, args.Attributes) + res, err := m.splitSDK.Treatment(m.clientConfig, args.Key, args.BucketingKey, args.Feature, args.Attributes, &dtos.EvaluationOptions{Properties: args.ImpressionProperties}) if err != nil { return &protov1.ResponseWrapper[protov1.TreatmentPayload]{Status: protov1.ResultInternalError}, err } @@ -201,7 +202,7 @@ func (m *ClientManager) handleGetTreatments(rpc *protov1.RPC, withConfig bool) ( return nil, fmt.Errorf("error parsing treatments arguments: %w", err) } - res, err := m.splitSDK.Treatments(m.clientConfig, args.Key, args.BucketingKey, args.Features, args.Attributes) + res, err := m.splitSDK.Treatments(m.clientConfig, args.Key, args.BucketingKey, args.Features, args.Attributes, &dtos.EvaluationOptions{Properties: args.ImpressionProperties}) if err != nil { return &protov1.ResponseWrapper[protov1.TreatmentsPayload]{Status: protov1.ResultInternalError}, err } @@ -243,7 +244,7 @@ func (m *ClientManager) handleGetTreatmentsByFlagSet(rpc *protov1.RPC, withConfi return nil, fmt.Errorf("error parsing treatmentsByFlagSet arguments: %w", err) } - res, err := m.splitSDK.TreatmentsByFlagSet(m.clientConfig, args.Key, args.BucketingKey, args.FlagSet, args.Attributes) + res, err := m.splitSDK.TreatmentsByFlagSet(m.clientConfig, args.Key, args.BucketingKey, args.FlagSet, args.Attributes, &dtos.EvaluationOptions{Properties: args.ImpressionProperties}) if err != nil { return &protov1.ResponseWrapper[protov1.TreatmentsWithFeaturePayload]{Status: protov1.ResultInternalError}, err } @@ -284,7 +285,7 @@ func (m *ClientManager) handleGetTreatmentsByFlagSets(rpc *protov1.RPC, withConf return nil, fmt.Errorf("error parsing treatmentsByFlagSets arguments: %w", err) } - res, err := m.splitSDK.TreatmentsByFlagSets(m.clientConfig, args.Key, args.BucketingKey, args.FlagSets, args.Attributes) + res, err := m.splitSDK.TreatmentsByFlagSets(m.clientConfig, args.Key, args.BucketingKey, args.FlagSets, args.Attributes, &dtos.EvaluationOptions{Properties: args.ImpressionProperties}) if err != nil { return &protov1.ResponseWrapper[protov1.TreatmentsWithFeaturePayload]{Status: protov1.ResultInternalError}, err } diff --git a/splitio/sdk/integration_test.go b/splitio/sdk/integration_test.go index 97ccdaf..7a39227 100644 --- a/splitio/sdk/integration_test.go +++ b/splitio/sdk/integration_test.go @@ -123,7 +123,7 @@ func TestInstantiationAndGetTreatmentE2E(t *testing.T) { client, err := New(logger, "someApikey", sdkConf) assert.Nil(t, err) - res, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil) + res, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil, nil) assert.Equal(t, "on", res.Treatment) assert.Nil(t, client.Shutdown()) @@ -241,7 +241,7 @@ func TestInstantiationAndGetTreatmentE2EWithFallbackTreatment(t *testing.T) { }, } - res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "not_exist", nil, client.WithEvaluationOptions(&opts)) + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "not_exist", nil, &opts) assert.Equal(t, "global_treatment", res1.Treatment) assert.Equal(t, "{\"pleassssse\":\"holaaaaa\"}", res1.Impression.Properties) @@ -370,7 +370,7 @@ func TestInstantiationAndGetTreatmentE2EWithPrerequistesNotAchive(t *testing.T) client, err := New(logger, "someApikey", sdkConf) assert.Nil(t, err) - res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil) + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil, nil) assert.Equal(t, "default", res1.Treatment) assert.Nil(t, client.Shutdown()) @@ -498,7 +498,7 @@ func TestInstantiationAndGetTreatmentE2EWithPrerequistesAchive(t *testing.T) { client, err := New(logger, "someApikey", sdkConf) assert.Nil(t, err) - res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil) + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", nil, nil) assert.Equal(t, "on", res1.Treatment) assert.Nil(t, client.Shutdown()) @@ -649,7 +649,7 @@ func TestInstantiationAndGetTreatmentE2EWithRBS(t *testing.T) { attributes := make(map[string]interface{}) attributes["version"] = "3.4.5" - res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", attributes) + res1, _ := client.Treatment(&types.ClientConfig{}, "aaaaaaklmnbv", nil, "split", attributes, nil) assert.Equal(t, "on", res1.Treatment) assert.Nil(t, client.Shutdown()) diff --git a/splitio/sdk/mocks/sdk.go b/splitio/sdk/mocks/sdk.go index dd6abb6..1d349f4 100644 --- a/splitio/sdk/mocks/sdk.go +++ b/splitio/sdk/mocks/sdk.go @@ -1,6 +1,7 @@ package mocks import ( + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/splitd/splitio/sdk" "github.com/splitio/splitd/splitio/sdk/types" "github.com/stretchr/testify/mock" @@ -17,7 +18,7 @@ func (m *SDKMock) Treatment( bucketingKey *string, feature string, attributes map[string]interface{}, - optFns ...sdk.OptFn, + evaluationOptions *dtos.EvaluationOptions, ) (*sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, feature, attributes) return args.Get(0).(*sdk.EvaluationResult), args.Error(1) @@ -30,7 +31,7 @@ func (m *SDKMock) Treatments( bucketingKey *string, features []string, attributes map[string]interface{}, - optFns ...sdk.OptFn, + evaluationOptions *dtos.EvaluationOptions, ) (map[string]sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, features, attributes) return args.Get(0).(map[string]sdk.EvaluationResult), args.Error(1) @@ -43,7 +44,7 @@ func (m *SDKMock) TreatmentsByFlagSet( bucketingKey *string, flagSet string, attributes map[string]interface{}, - optFns ...sdk.OptFn, + evaluationOptions *dtos.EvaluationOptions, ) (map[string]sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, flagSet, attributes) return args.Get(0).(map[string]sdk.EvaluationResult), args.Error(1) @@ -56,7 +57,7 @@ func (m *SDKMock) TreatmentsByFlagSets( bucketingKey *string, flagSets []string, attributes map[string]interface{}, - optFns ...sdk.OptFn, + evaluationOptions *dtos.EvaluationOptions, ) (map[string]sdk.EvaluationResult, error) { args := m.Called(md, key, bucketingKey, flagSets, attributes) return args.Get(0).(map[string]sdk.EvaluationResult), args.Error(1) diff --git a/splitio/sdk/sdk.go b/splitio/sdk/sdk.go index af8e6bb..7ec5b01 100644 --- a/splitio/sdk/sdk.go +++ b/splitio/sdk/sdk.go @@ -49,10 +49,10 @@ var ruleBasedSegmentRules = []string{constants.MatcherTypeAllKeys, constants.Mat type Attributes = map[string]interface{} type Interface interface { - Treatment(cfg *types.ClientConfig, key string, bucketingKey *string, feature string, attributes map[string]interface{}, optFns ...OptFn) (*EvaluationResult, error) - Treatments(cfg *types.ClientConfig, key string, bucketingKey *string, features []string, attributes map[string]interface{}, optFns ...OptFn) (map[string]EvaluationResult, error) - TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bucketingKey *string, flagSet string, attributes map[string]interface{}, optFns ...OptFn) (map[string]EvaluationResult, error) - TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}, optFns ...OptFn) (map[string]EvaluationResult, error) + Treatment(cfg *types.ClientConfig, key string, bucketingKey *string, feature string, attributes map[string]interface{}, evaluationOptions *dtos.EvaluationOptions) (*EvaluationResult, error) + Treatments(cfg *types.ClientConfig, key string, bucketingKey *string, features []string, attributes map[string]interface{}, evaluationOptions *dtos.EvaluationOptions) (map[string]EvaluationResult, error) + TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bucketingKey *string, flagSet string, attributes map[string]interface{}, evaluationOptions *dtos.EvaluationOptions) (map[string]EvaluationResult, error) + TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}, evaluationOptions *dtos.EvaluationOptions) (map[string]EvaluationResult, error) Track(cfg *types.ClientConfig, key string, trafficType string, eventType string, value *float64, properties map[string]interface{}) error SplitNames() ([]string, error) Splits() ([]SplitView, error) @@ -74,22 +74,6 @@ type Impl struct { validator Validator } -type options struct { - evaluationOptions *dtos.EvaluationOptions -} - -type OptFn = func(o *options) - -func (c *Impl) WithEvaluationOptions(e *dtos.EvaluationOptions) OptFn { - return func(o *options) { o.evaluationOptions = e } -} - -func defaultOpts() options { - return options{ - evaluationOptions: nil, - } -} - func New(logger logging.LoggerInterface, apikey string, c *conf.Config) (*Impl, error) { if warnings := c.Normalize(); len(warnings) > 0 { @@ -149,14 +133,13 @@ func New(logger logging.LoggerInterface, apikey string, c *conf.Config) (*Impl, } // Treatment implements Interface -func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, feature string, attributes Attributes, optFns ...OptFn) (*EvaluationResult, error) { - options := getOptions(optFns...) +func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, feature string, attributes Attributes, evaluationOptions *dtos.EvaluationOptions) (*EvaluationResult, error) { res := i.ev.EvaluateFeature(key, bk, feature, attributes) if res == nil { return nil, fmt.Errorf("nil result") } - imp := i.handleImpression(key, bk, feature, res, cfg.Metadata, SerializeProperties(options.evaluationOptions)) + imp := i.handleImpression(key, bk, feature, res, cfg.Metadata, SerializeProperties(evaluationOptions)) return &EvaluationResult{ Treatment: res.Treatment, Impression: imp, @@ -164,18 +147,9 @@ func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, featur }, nil } -func getOptions(optFns ...OptFn) options { - options := defaultOpts() - for _, optFn := range optFns { - optFn(&options) - } - return options -} - // Treatment implements Interface -func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, features []string, attributes Attributes, optFns ...OptFn) (map[string]EvaluationResult, error) { +func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, features []string, attributes Attributes, evaluationOptions *dtos.EvaluationOptions) (map[string]EvaluationResult, error) { - options := getOptions(optFns...) res := i.ev.EvaluateFeatures(key, bk, features, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for _, feature := range features { @@ -188,7 +162,7 @@ func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, featu var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(options.evaluationOptions)) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -197,15 +171,14 @@ func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, featu } // TreatmentsByFlagSet implements Interface -func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *string, flagSet string, attributes Attributes, optFns ...OptFn) (map[string]EvaluationResult, error) { +func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *string, flagSet string, attributes Attributes, evaluationOptions *dtos.EvaluationOptions) (map[string]EvaluationResult, error) { - options := getOptions(optFns...) res := i.ev.EvaluateFeatureByFlagSets(key, bk, []string{flagSet}, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for feature, curr := range res.Evaluations { var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(options.evaluationOptions)) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } @@ -214,15 +187,14 @@ func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *stri } // TreatmentsByFlagSets implements Interface -func (i *Impl) TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bk *string, flagSets []string, attributes Attributes, optFns ...OptFn) (map[string]EvaluationResult, error) { +func (i *Impl) TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bk *string, flagSets []string, attributes Attributes, evaluationOptions *dtos.EvaluationOptions) (map[string]EvaluationResult, error) { - options := getOptions(optFns...) res := i.ev.EvaluateFeatureByFlagSets(key, bk, flagSets, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for feature, curr := range res.Evaluations { var eres EvaluationResult eres.Treatment = curr.Treatment - eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(options.evaluationOptions)) + eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(evaluationOptions)) eres.Config = curr.Config toRet[feature] = eres } diff --git a/splitio/sdk/sdk_test.go b/splitio/sdk/sdk_test.go index 1202487..230534f 100644 --- a/splitio/sdk/sdk_test.go +++ b/splitio/sdk/sdk_test.go @@ -61,7 +61,7 @@ func TestTreatmentsWithImpressionsDisabled(t *testing.T) { res, err := client.Treatments( &types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, - "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}) + "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res["f1"].Config) assert.Nil(t, res["f2"].Config) @@ -101,7 +101,7 @@ func TestTreatmentLabelsDisabled(t *testing.T) { cfg: conf.Config{LabelsEnabled: false}, } - res, err := client.Treatment(&types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, "key1", nil, "f1", Attributes{"a": 1}) + res, err := client.Treatment(&types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, "key1", nil, "f1", Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res.Config) assertImpEq(t, expectedImpression, res.Impression) @@ -156,7 +156,7 @@ func TestTreatmentLabelsEnabled(t *testing.T) { cfg: conf.Config{LabelsEnabled: true}, } - res, err := client.Treatment(&types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, "key1", nil, "f1", Attributes{"a": 1}) + res, err := client.Treatment(&types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, "key1", nil, "f1", Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res.Config) assertImpEq(t, expectedImpression, res.Impression) @@ -226,7 +226,7 @@ func TestTreatments(t *testing.T) { res, err := client.Treatments( &types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, - "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}) + "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res["f1"].Config) assert.Nil(t, res["f2"].Config) @@ -308,7 +308,7 @@ func TestTreatmentsWithImpressionProperties(t *testing.T) { } res, err := client.Treatments( &types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, - "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}, client.WithEvaluationOptions(&opts)) + "key1", nil, []string{"f1", "f2", "f3"}, Attributes{"a": 1}, &opts) assert.Nil(t, err) assert.Nil(t, res["f1"].Config) assert.Nil(t, res["f2"].Config) @@ -397,7 +397,7 @@ func TestTreatmentsByFlagSet(t *testing.T) { res, err := client.TreatmentsByFlagSet( &types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, - "key1", nil, "set", Attributes{"a": 1}) + "key1", nil, "set", Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res["f1"].Config) assert.Nil(t, res["f2"].Config) @@ -486,7 +486,7 @@ func TestTreatmentsByFlagSets(t *testing.T) { res, err := client.TreatmentsByFlagSets( &types.ClientConfig{Metadata: types.ClientMetadata{ID: "some", SdkVersion: "go-1.2.3"}}, - "key1", nil, []string{"set_1", "set_2"}, Attributes{"a": 1}) + "key1", nil, []string{"set_1", "set_2"}, Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res["f1"].Config) assert.Nil(t, res["f2"].Config) @@ -555,7 +555,7 @@ func TestImpressionsQueueFull(t *testing.T) { for idx := 0; idx < 4; idx++ { feature := fmt.Sprintf("f%d", idx) expectedImpression.FeatureName = feature - res, err := client.Treatment(clientConf, "key1", nil, feature, Attributes{"a": 1}) + res, err := client.Treatment(clientConf, "key1", nil, feature, Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res.Config) assertImpEq(t, expectedImpression, res.Impression) @@ -567,7 +567,7 @@ func TestImpressionsQueueFull(t *testing.T) { for idx := 4; idx < 8; idx++ { feature := fmt.Sprintf("f%d", idx) expectedImpression.FeatureName = feature - res, err := client.Treatment(clientConf, "key1", nil, feature, Attributes{"a": 1}) + res, err := client.Treatment(clientConf, "key1", nil, feature, Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res.Config) assertImpEq(t, expectedImpression, res.Impression) @@ -577,7 +577,7 @@ func TestImpressionsQueueFull(t *testing.T) { feature := "f8" expectedImpression.FeatureName = feature - res, err := client.Treatment(clientConf, "key1", nil, feature, Attributes{"a": 1}) + res, err := client.Treatment(clientConf, "key1", nil, feature, Attributes{"a": 1}, nil) assert.Nil(t, err) assert.Nil(t, res.Config) assertImpEq(t, expectedImpression, res.Impression) From be31486e41b12d278c97c3fcc0a958de2778a1a5 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 26 Jan 2026 11:36:17 -0300 Subject: [PATCH 17/26] Updated docker file --- infra/sidecar.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/sidecar.Dockerfile b/infra/sidecar.Dockerfile index a26df79..e46f6e6 100644 --- a/infra/sidecar.Dockerfile +++ b/infra/sidecar.Dockerfile @@ -1,5 +1,5 @@ # ----- Builder image -ARG GOLANG_VERSION=1.23.6 +ARG GOLANG_VERSION=1.24.0 FROM golang:${GOLANG_VERSION}-bookworm AS builder ARG FIPS_MODE From eba3941e9972dfabca5590e728fc6cff56657e79 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Tue, 27 Jan 2026 12:28:31 -0300 Subject: [PATCH 18/26] Updated splitCli --- splitio/commitsha.go | 2 +- splitio/conf/splitcli.go | 61 +++++++++++++++++++++-------------- splitio/conf/splitcli_test.go | 2 ++ 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/splitio/commitsha.go b/splitio/commitsha.go index dfdefd0..49e4c75 100644 --- a/splitio/commitsha.go +++ b/splitio/commitsha.go @@ -1,3 +1,3 @@ package splitio -const CommitSHA = "a651b23" +const CommitSHA = "be31486" diff --git a/splitio/conf/splitcli.go b/splitio/conf/splitcli.go index b192f9b..01b3e07 100644 --- a/splitio/conf/splitcli.go +++ b/splitio/conf/splitcli.go @@ -25,15 +25,16 @@ type CliArgs struct { WriteTimeoutMS int // command - Method string - Key string - BucketingKey string - Feature string - Features []string - TrafficType string - EventType string - EventVal *float64 - Attributes map[string]interface{} + Method string + Key string + BucketingKey string + Feature string + Features []string + TrafficType string + EventType string + EventVal *float64 + Attributes map[string]interface{} + ImpressionProperties map[string]interface{} } func (a *CliArgs) LinkOpts() (*link.ConsumerOptions, error) { @@ -85,6 +86,7 @@ func ParseCliArgs() (*CliArgs, error) { et := cliFlags.String("event-type", "", "event type") ev := cliFlags.String("value", "", "event associated value") at := cliFlags.String("attributes", "", "json representation of attributes") + pr := cliFlags.String("impression-properties", "", "json representation of") err := cliFlags.Parse(os.Args[1:]) if err != nil { return nil, fmt.Errorf("error parsing arguments: %w", err) @@ -107,22 +109,31 @@ func ParseCliArgs() (*CliArgs, error) { return nil, fmt.Errorf("error parsing attributes: %w", err) } + if *pr == "" { + *pr = "null" + } + impressionPorperties := make(map[string]interface{}) + if err = json.Unmarshal([]byte(*pr), &impressionPorperties); err != nil { + return nil, fmt.Errorf("error parsing impression properties: %w", err) + } + return &CliArgs{ - ID: *id, - Serialization: *s, - Protocol: *p, - LogLevel: *ll, - ConnType: *ct, - ConnAddr: *ca, - BufSize: *bs, - Method: *m, - Key: *k, - BucketingKey: *bk, - Feature: *f, - Features: strings.Split(*fs, ","), - TrafficType: *tt, - EventType: *et, - EventVal: eventVal, - Attributes: attrs, + ID: *id, + Serialization: *s, + Protocol: *p, + LogLevel: *ll, + ConnType: *ct, + ConnAddr: *ca, + BufSize: *bs, + Method: *m, + Key: *k, + BucketingKey: *bk, + Feature: *f, + Features: strings.Split(*fs, ","), + TrafficType: *tt, + EventType: *et, + EventVal: eventVal, + Attributes: attrs, + ImpressionProperties: impressionPorperties, }, nil } diff --git a/splitio/conf/splitcli_test.go b/splitio/conf/splitcli_test.go index 102491b..c7e524a 100644 --- a/splitio/conf/splitcli_test.go +++ b/splitio/conf/splitcli_test.go @@ -25,6 +25,7 @@ func TestCliConfig(t *testing.T) { "-event-type=someEventType", "-value=0.123", `-attributes={"some": "attribute"}`, + `-impression-properties={"userId": "123", "age": 30, "premium": true, "balance": 99.5}`, } parsed, err := ParseCliArgs() @@ -42,6 +43,7 @@ func TestCliConfig(t *testing.T) { assert.Equal(t, "someEventType", parsed.EventType) assert.Equal(t, lang.Ref(float64(0.123)), parsed.EventVal) assert.Equal(t, map[string]interface{}{"some": "attribute"}, parsed.Attributes) + assert.Equal(t, map[string]interface{}{"userId": "123", "age": float64(30), "premium": true, "balance": 99.5}, parsed.ImpressionProperties) // test bad buffer size os.Args = []string{os.Args[0], "-buffer-size=sarasa"} From fb71b466f80ddbd5a0a65b844a90af51af99775b Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Fri, 30 Jan 2026 12:28:34 -0300 Subject: [PATCH 19/26] Updated rpcs logic to accept more than four params --- splitio/commitsha.go | 2 +- splitio/link/protocol/v1/rpcs.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/splitio/commitsha.go b/splitio/commitsha.go index 49e4c75..4c56802 100644 --- a/splitio/commitsha.go +++ b/splitio/commitsha.go @@ -1,3 +1,3 @@ package splitio -const CommitSHA = "be31486" +const CommitSHA = "eba3941" diff --git a/splitio/link/protocol/v1/rpcs.go b/splitio/link/protocol/v1/rpcs.go index 1c2ab23..8be45b4 100644 --- a/splitio/link/protocol/v1/rpcs.go +++ b/splitio/link/protocol/v1/rpcs.go @@ -218,7 +218,7 @@ func (t *TreatmentsArgs) PopulateFromRPC(rpc *RPC) error { if rpc.OpCode != OCTreatments && rpc.OpCode != OCTreatmentsWithConfig { return RPCParseError{Code: PECOpCodeMismatch} } - if len(rpc.Args) != 4 { + if len(rpc.Args) < 4 { return RPCParseError{Code: PECWrongArgCount} } From d2ecf77b859b391fc6e54ecaebbd0d9ef6e777c1 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Fri, 30 Jan 2026 13:01:57 -0300 Subject: [PATCH 20/26] Updated rpcs param condition --- splitio/link/protocol/v1/rpcs.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/splitio/link/protocol/v1/rpcs.go b/splitio/link/protocol/v1/rpcs.go index 8be45b4..4518739 100644 --- a/splitio/link/protocol/v1/rpcs.go +++ b/splitio/link/protocol/v1/rpcs.go @@ -150,7 +150,7 @@ func (t *TreatmentArgs) PopulateFromRPC(rpc *RPC) error { if rpc.OpCode != OCTreatment && rpc.OpCode != OCTreatmentWithConfig { return RPCParseError{Code: PECOpCodeMismatch} } - if len(rpc.Args) != 4 { + if len(rpc.Args) < 4 { return RPCParseError{Code: PECWrongArgCount} } @@ -289,7 +289,7 @@ func (t *TreatmentsByFlagSetArgs) PopulateFromRPC(rpc *RPC) error { if rpc.OpCode != OCTreatmentsByFlagSet && rpc.OpCode != OCTreatmentsWithConfigByFlagSet { return RPCParseError{Code: PECOpCodeMismatch} } - if len(rpc.Args) != 4 { + if len(rpc.Args) < 4 { return RPCParseError{Code: PECWrongArgCount} } @@ -353,7 +353,7 @@ func (t *TreatmentsByFlagSetsArgs) PopulateFromRPC(rpc *RPC) error { if rpc.OpCode != OCTreatmentsByFlagSets && rpc.OpCode != OCTreatmentsWithConfigByFlagSets { return RPCParseError{Code: PECOpCodeMismatch} } - if len(rpc.Args) != 4 { + if len(rpc.Args) < 4 { return RPCParseError{Code: PECWrongArgCount} } From 085f07bffe9e6b8ee82ab71da69fceecafecc175 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Tue, 3 Mar 2026 12:17:15 -0300 Subject: [PATCH 21/26] Pr suggestions --- splitio/commitsha.go | 2 +- splitio/link/protocol/v1/rpcs.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/splitio/commitsha.go b/splitio/commitsha.go index 4c56802..7aa77c9 100644 --- a/splitio/commitsha.go +++ b/splitio/commitsha.go @@ -1,3 +1,3 @@ package splitio -const CommitSHA = "eba3941" +const CommitSHA = "d2ecf77" diff --git a/splitio/link/protocol/v1/rpcs.go b/splitio/link/protocol/v1/rpcs.go index 4518739..368ab1c 100644 --- a/splitio/link/protocol/v1/rpcs.go +++ b/splitio/link/protocol/v1/rpcs.go @@ -179,7 +179,7 @@ func (t *TreatmentArgs) PopulateFromRPC(rpc *RPC) error { t.Attributes = sanitizeAttributes(rawAttrs) } - if len(rpc.Args) == 5 && rpc.Args[TreatmentArgImpressionPropertiesIdx] != nil { + if len(rpc.Args) >= 5 && rpc.Args[TreatmentArgImpressionPropertiesIdx] != nil { rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentArgImpressionPropertiesIdx]) if err != nil { return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentArgImpressionPropertiesIdx)} @@ -250,7 +250,7 @@ func (t *TreatmentsArgs) PopulateFromRPC(rpc *RPC) error { } t.Attributes = sanitizeAttributes(rawAttrs) - if len(rpc.Args) == 5 && rpc.Args[TreatmentsArgImpressionPropertiesIdx] != nil { + if len(rpc.Args) >= 5 && rpc.Args[TreatmentsArgImpressionPropertiesIdx] != nil { rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentsArgImpressionPropertiesIdx]) if err != nil { return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsArgImpressionPropertiesIdx)} @@ -314,7 +314,7 @@ func (t *TreatmentsByFlagSetArgs) PopulateFromRPC(rpc *RPC) error { } t.Attributes = sanitizeAttributes(rawAttrs) - if len(rpc.Args) == 5 && rpc.Args[TreatmentsByFlagSetArgImpressionPropertiesIdx] != nil { + if len(rpc.Args) >= 5 && rpc.Args[TreatmentsByFlagSetArgImpressionPropertiesIdx] != nil { rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentsByFlagSetArgImpressionPropertiesIdx]) if err != nil { return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsByFlagSetArgImpressionPropertiesIdx)} @@ -384,7 +384,7 @@ func (t *TreatmentsByFlagSetsArgs) PopulateFromRPC(rpc *RPC) error { } t.Attributes = sanitizeAttributes(rawAttrs) - if len(rpc.Args) == 5 && rpc.Args[TreatmentsByFlagSetsArgImpressionPropertiesIdx] != nil { + if len(rpc.Args) >= 5 && rpc.Args[TreatmentsByFlagSetsArgImpressionPropertiesIdx] != nil { rawAttrs, err := getOptional[map[string]interface{}](rpc.Args[TreatmentsByFlagSetsArgImpressionPropertiesIdx]) if err != nil { return RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsByFlagSetsArgImpressionPropertiesIdx)} From 89541bd351aa58aebfc74cf07e8b3e179172b536 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 4 Mar 2026 14:50:30 -0300 Subject: [PATCH 22/26] Updated to add logic for fallback treatment --- splitd.yaml.tpl | 4 ++ splitio/commitsha.go | 2 +- splitio/conf/splitd.go | 131 +++++++++++++++++++++++++++++++++--- splitio/conf/splitd_test.go | 44 ++++++++++++ splitio/sdk/sdk.go | 64 +++++++++++++++--- 5 files changed, 225 insertions(+), 20 deletions(-) diff --git a/splitd.yaml.tpl b/splitd.yaml.tpl index 2158359..9fcc009 100644 --- a/splitd.yaml.tpl +++ b/splitd.yaml.tpl @@ -8,6 +8,10 @@ sdk: apikey: labelsEnabled: true streamingEnabled: true + fallbackTreatment: + global_fallback_treatment: + treatment: other + by_flag_fallback_treatment: {} urls: auth: https://auth.split.io sdk: https://sdk.split.io/api diff --git a/splitio/commitsha.go b/splitio/commitsha.go index 7aa77c9..251a44f 100644 --- a/splitio/commitsha.go +++ b/splitio/commitsha.go @@ -1,3 +1,3 @@ package splitio -const CommitSHA = "d2ecf77" +const CommitSHA = "085f07b" diff --git a/splitio/conf/splitd.go b/splitio/conf/splitd.go index f80d183..e1ea685 100644 --- a/splitio/conf/splitd.go +++ b/splitio/conf/splitd.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/splitd/splitio/common/lang" "github.com/splitio/splitd/splitio/link" @@ -122,14 +123,15 @@ func (l *Link) ToListenerOpts() (*link.ListenerOptions, error) { } type SDK struct { - Apikey string `yaml:"apikey"` - LabelsEnabled *bool `yaml:"labelsEnabled"` - StreamingEnabled *bool `yaml:"streamingEnabled"` - URLs URLs `yaml:"urls"` - FeatureFlags FeatureFlags `yaml:"featureFlags"` - Impressions Impressions `yaml:"impressions"` - Events Events `yaml:"events"` - FlagSetsFilter []string `yaml:"flagSetsFilter"` + Apikey string `yaml:"apikey"` + LabelsEnabled *bool `yaml:"labelsEnabled"` + StreamingEnabled *bool `yaml:"streamingEnabled"` + FallbackTreatment fallbackTreatmentInput `yaml:"fallbackTreatment"` + URLs URLs `yaml:"urls"` + FeatureFlags FeatureFlags `yaml:"featureFlags"` + Impressions Impressions `yaml:"impressions"` + Events Events `yaml:"events"` + FlagSetsFilter []string `yaml:"flagSetsFilter"` } func (s *SDK) PopulateWithDefaults() { @@ -137,6 +139,7 @@ func (s *SDK) PopulateWithDefaults() { s.Apikey = apikeyPlaceHolder s.LabelsEnabled = lang.Ref(cfg.LabelsEnabled) s.StreamingEnabled = lang.Ref(cfg.StreamingEnabled) + s.FallbackTreatment = fallbackTreatmentFromConfig(cfg.FallbackTreatment) s.URLs.PopulateWithDefaults() s.FeatureFlags.PopulateWithDefaults() s.Impressions.PopulateWithDefaults() @@ -216,6 +219,11 @@ func (s *SDK) ToSDKConf() *sdkConf.Config { if len(s.FlagSetsFilter) > 0 { cfg.FlagSetsFilter = s.FlagSetsFilter } + if parsed, err := (&s.FallbackTreatment).toConfig(); err != nil { + log.Printf("[splitd] fallbackTreatment: %v", err) + } else if parsed != nil { + cfg.FallbackTreatment = *parsed + } return cfg } @@ -310,6 +318,113 @@ func (p *Profiling) PopulateWithDefaults() { p.Port = 8888 } +// fallbackTreatmentFromConfig maps the SDK default config's FallbackTreatment into our input type. +func fallbackTreatmentFromConfig(c dtos.FallbackTreatmentConfig) fallbackTreatmentInput { + parsed := new(dtos.FallbackTreatmentConfig) + *parsed = c + return fallbackTreatmentInput{parsed: parsed} +} + +type fallbackTreatmentEntry struct { + Treatment *string `json:"treatment" yaml:"treatment"` + Config *string `json:"config,omitempty" yaml:"config,omitempty"` +} + +type fallbackTreatmentInput struct { + parsed *dtos.FallbackTreatmentConfig + raw string +} + +func (f *fallbackTreatmentInput) UnmarshalYAML(value *yaml.Node) error { + if value == nil { + return nil + } + switch value.Kind { + case yaml.ScalarNode: + var s string + if err := value.Decode(&s); err != nil { + return err + } + f.parsed = nil + f.raw = strings.TrimSpace(s) + return nil + case yaml.MappingNode: + var m struct { + Global *fallbackTreatmentEntry `yaml:"global_fallback_treatment"` + ByFlag map[string]fallbackTreatmentEntry `yaml:"by_flag_fallback_treatment"` + } + if err := value.Decode(&m); err != nil { + return err + } + out := dtos.FallbackTreatmentConfig{} + if m.Global != nil && m.Global.Treatment != nil { + out.GlobalFallbackTreatment = &dtos.FallbackTreatment{ + Treatment: m.Global.Treatment, + Config: m.Global.Config, + } + } + if len(m.ByFlag) > 0 { + out.ByFlagFallbackTreatment = make(map[string]dtos.FallbackTreatment) + for name, v := range m.ByFlag { + if v.Treatment != nil { + out.ByFlagFallbackTreatment[name] = dtos.FallbackTreatment{ + Treatment: v.Treatment, + Config: v.Config, + } + } + } + } + f.parsed = &out + return nil + } + return nil +} + +func (f *fallbackTreatmentInput) toConfig() (*dtos.FallbackTreatmentConfig, error) { + if f == nil { + return nil, nil + } + if f.raw != "" { + return parseFallbackTreatmentJSON(f.raw) + } + if f.parsed != nil { + return f.parsed, nil + } + return nil, nil +} + +func parseFallbackTreatmentJSON(raw string) (*dtos.FallbackTreatmentConfig, error) { + var wrapper struct { + FallbackTreatment struct { + GlobalFallbackTreatment *fallbackTreatmentEntry `json:"global_fallback_treatment"` + ByFlagFallbackTreatment map[string]fallbackTreatmentEntry `json:"by_flag_fallback_treatment"` + } `json:"fallback_treatment"` + } + if err := json.Unmarshal([]byte(raw), &wrapper); err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + out := dtos.FallbackTreatmentConfig{} + inner := &wrapper.FallbackTreatment + if inner.GlobalFallbackTreatment != nil && inner.GlobalFallbackTreatment.Treatment != nil { + out.GlobalFallbackTreatment = &dtos.FallbackTreatment{ + Treatment: inner.GlobalFallbackTreatment.Treatment, + Config: inner.GlobalFallbackTreatment.Config, + } + } + if len(inner.ByFlagFallbackTreatment) > 0 { + out.ByFlagFallbackTreatment = make(map[string]dtos.FallbackTreatment) + for name, v := range inner.ByFlagFallbackTreatment { + if v.Treatment != nil { + out.ByFlagFallbackTreatment[name] = dtos.FallbackTreatment{ + Treatment: v.Treatment, + Config: v.Config, + } + } + } + } + return &out, nil +} + func ReadConfig() (*Config, error) { cfgFN := defaultConfigFN if fromEnv := os.Getenv("SPLITD_CONF_FILE"); fromEnv != "" { diff --git a/splitio/conf/splitd_test.go b/splitio/conf/splitd_test.go index d14c3c4..955513b 100644 --- a/splitio/conf/splitd_test.go +++ b/splitio/conf/splitd_test.go @@ -15,6 +15,8 @@ import ( "github.com/splitio/splitd/splitio/link/transfer" "github.com/splitio/splitd/splitio/sdk/conf" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestConfig(t *testing.T) { @@ -30,6 +32,7 @@ func TestConfig(t *testing.T) { cfg = Config{} assert.Nil(t, cfg.parse(dir+string(filepath.Separator)+"splitd.yaml.tpl")) + expected.SDK.FallbackTreatment = cfg.SDK.FallbackTreatment assert.Equal(t, expected, cfg) assert.Error(t, cfg.parse("someNonexistantFile")) @@ -185,3 +188,44 @@ func TestDefaultConf(t *testing.T) { assert.Equal(t, defaultLogLevel, *c.Logger.Level) assert.Equal(t, defaultLogOutput, *c.Logger.Output) } + +func TestFallbackTreatmentToSDKConf(t *testing.T) { + // JSON string form + var cfg Config + cfg.PopulateWithDefaults() + err := yaml.Unmarshal([]byte(` +sdk: + apikey: test + fallbackTreatment: '{"fallback_treatment":{"global_fallback_treatment":{"treatment":"control"},"by_flag_fallback_treatment":{"my_flag":{"treatment":"off"}}}}' +`), &cfg) + assert.Nil(t, err) + sdkConf := cfg.SDK.ToSDKConf() + require.NotNil(t, sdkConf) + require.NotNil(t, sdkConf.FallbackTreatment.GlobalFallbackTreatment) + require.NotEmpty(t, sdkConf.FallbackTreatment.ByFlagFallbackTreatment) + assert.Equal(t, "control", *sdkConf.FallbackTreatment.GlobalFallbackTreatment.Treatment) + assert.Equal(t, "off", *sdkConf.FallbackTreatment.ByFlagFallbackTreatment["my_flag"].Treatment) + + // Native YAML object form + var cfg2 Config + cfg2.PopulateWithDefaults() + err = yaml.Unmarshal([]byte(` +sdk: + apikey: test + fallbackTreatment: + global_fallback_treatment: + treatment: global_val + by_flag_fallback_treatment: + some_flag: + treatment: on + config: "{}" +`), &cfg2) + assert.Nil(t, err) + sdkConf2 := cfg2.SDK.ToSDKConf() + require.NotNil(t, sdkConf2) + require.NotNil(t, sdkConf2.FallbackTreatment.GlobalFallbackTreatment) + require.Contains(t, sdkConf2.FallbackTreatment.ByFlagFallbackTreatment, "some_flag") + assert.Equal(t, "global_val", *sdkConf2.FallbackTreatment.GlobalFallbackTreatment.Treatment) + assert.Equal(t, "on", *sdkConf2.FallbackTreatment.ByFlagFallbackTreatment["some_flag"].Treatment) + assert.Equal(t, "{}", *sdkConf2.FallbackTreatment.ByFlagFallbackTreatment["some_flag"].Config) +} diff --git a/splitio/sdk/sdk.go b/splitio/sdk/sdk.go index 7ec5b01..7c97825 100644 --- a/splitio/sdk/sdk.go +++ b/splitio/sdk/sdk.go @@ -138,12 +138,18 @@ func (i *Impl) Treatment(cfg *types.ClientConfig, key string, bk *string, featur if res == nil { return nil, fmt.Errorf("nil result") } - + treatment := res.Treatment + config := res.Config + if treatment == defaultFallbackTreatment { + if t, c := i.getFallbackTreatment(feature); t != defaultFallbackTreatment { + treatment, config = t, c + } + } imp := i.handleImpression(key, bk, feature, res, cfg.Metadata, SerializeProperties(evaluationOptions)) return &EvaluationResult{ - Treatment: res.Treatment, + Treatment: treatment, Impression: imp, - Config: res.Config, + Config: config, }, nil } @@ -156,14 +162,21 @@ func (i *Impl) Treatments(cfg *types.ClientConfig, key string, bk *string, featu curr, ok := res.Evaluations[feature] if !ok { - toRet[feature] = EvaluationResult{Treatment: "control"} + treatment, config := i.getFallbackTreatment(feature) + toRet[feature] = EvaluationResult{Treatment: treatment, Config: config} continue } - + treatment := curr.Treatment + config := curr.Config + if treatment == defaultFallbackTreatment { + if t, c := i.getFallbackTreatment(feature); t != defaultFallbackTreatment { + treatment, config = t, c + } + } var eres EvaluationResult - eres.Treatment = curr.Treatment + eres.Treatment = treatment eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(evaluationOptions)) - eres.Config = curr.Config + eres.Config = config toRet[feature] = eres } @@ -176,10 +189,16 @@ func (i *Impl) TreatmentsByFlagSet(cfg *types.ClientConfig, key string, bk *stri res := i.ev.EvaluateFeatureByFlagSets(key, bk, []string{flagSet}, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for feature, curr := range res.Evaluations { + treatment, config := curr.Treatment, curr.Config + if treatment == defaultFallbackTreatment { + if t, c := i.getFallbackTreatment(feature); t != defaultFallbackTreatment { + treatment, config = t, c + } + } var eres EvaluationResult - eres.Treatment = curr.Treatment + eres.Treatment = treatment eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(evaluationOptions)) - eres.Config = curr.Config + eres.Config = config toRet[feature] = eres } @@ -192,10 +211,16 @@ func (i *Impl) TreatmentsByFlagSets(cfg *types.ClientConfig, key string, bk *str res := i.ev.EvaluateFeatureByFlagSets(key, bk, flagSets, attributes) toRet := make(map[string]EvaluationResult, len(res.Evaluations)) for feature, curr := range res.Evaluations { + treatment, config := curr.Treatment, curr.Config + if treatment == defaultFallbackTreatment { + if t, c := i.getFallbackTreatment(feature); t != defaultFallbackTreatment { + treatment, config = t, c + } + } var eres EvaluationResult - eres.Treatment = curr.Treatment + eres.Treatment = treatment eres.Impression = i.handleImpression(key, bk, feature, &curr, cfg.Metadata, SerializeProperties(evaluationOptions)) - eres.Config = curr.Config + eres.Config = config toRet[feature] = eres } @@ -326,6 +351,23 @@ func splitToView(s *dtos.SplitDTO) *SplitView { } } +const defaultFallbackTreatment = "control" + +func (i *Impl) getFallbackTreatment(feature string) (treatment string, config *string) { + treatment = defaultFallbackTreatment + ft := i.cfg.FallbackTreatment + if byFlag, ok := ft.ByFlagFallbackTreatment[feature]; ok && byFlag.Treatment != nil { + treatment = *byFlag.Treatment + config = byFlag.Config + return treatment, config + } + if ft.GlobalFallbackTreatment != nil && ft.GlobalFallbackTreatment.Treatment != nil { + treatment = *ft.GlobalFallbackTreatment.Treatment + config = ft.GlobalFallbackTreatment.Config + } + return treatment, config +} + func timeMillis() int64 { return time.Now().UTC().UnixMilli() } From 9ad70baa1294921c66269dc00288d23b793a80fb Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 9 Mar 2026 11:31:15 -0300 Subject: [PATCH 23/26] Updated splitcli --- splitio/conf/splitcli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/splitio/conf/splitcli.go b/splitio/conf/splitcli.go index 01b3e07..574e627 100644 --- a/splitio/conf/splitcli.go +++ b/splitio/conf/splitcli.go @@ -86,7 +86,7 @@ func ParseCliArgs() (*CliArgs, error) { et := cliFlags.String("event-type", "", "event type") ev := cliFlags.String("value", "", "event associated value") at := cliFlags.String("attributes", "", "json representation of attributes") - pr := cliFlags.String("impression-properties", "", "json representation of") + pr := cliFlags.String("impression-properties", "null", "json representation of") err := cliFlags.Parse(os.Args[1:]) if err != nil { return nil, fmt.Errorf("error parsing arguments: %w", err) From b25eea759a454079f0386f7ef831d559caaab848 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 18 Mar 2026 07:43:02 -0500 Subject: [PATCH 24/26] Updated splitd and tests --- splitio/commitsha.go | 2 +- splitio/conf/splitd.go | 80 ++++++++++++++++----------- splitio/link/protocol/v1/rpcs_test.go | 29 ++++++++++ 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/splitio/commitsha.go b/splitio/commitsha.go index 251a44f..639516e 100644 --- a/splitio/commitsha.go +++ b/splitio/commitsha.go @@ -1,3 +1,3 @@ package splitio -const CommitSHA = "085f07b" +const CommitSHA = "26a03c9" diff --git a/splitio/conf/splitd.go b/splitio/conf/splitd.go index e1ea685..0c164c6 100644 --- a/splitio/conf/splitd.go +++ b/splitio/conf/splitd.go @@ -339,44 +339,62 @@ func (f *fallbackTreatmentInput) UnmarshalYAML(value *yaml.Node) error { if value == nil { return nil } + switch value.Kind { case yaml.ScalarNode: - var s string - if err := value.Decode(&s); err != nil { - return err - } - f.parsed = nil - f.raw = strings.TrimSpace(s) - return nil + return f.unmarshalScalar(value) case yaml.MappingNode: - var m struct { - Global *fallbackTreatmentEntry `yaml:"global_fallback_treatment"` - ByFlag map[string]fallbackTreatmentEntry `yaml:"by_flag_fallback_treatment"` - } - if err := value.Decode(&m); err != nil { - return err - } - out := dtos.FallbackTreatmentConfig{} - if m.Global != nil && m.Global.Treatment != nil { - out.GlobalFallbackTreatment = &dtos.FallbackTreatment{ - Treatment: m.Global.Treatment, - Config: m.Global.Config, - } + return f.unmarshalMapping(value) + default: + return nil + } +} + +func (f *fallbackTreatmentInput) unmarshalScalar(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + f.parsed = nil + f.raw = strings.TrimSpace(s) + return nil +} + +func (f *fallbackTreatmentInput) unmarshalMapping(value *yaml.Node) error { + var m struct { + Global *fallbackTreatmentEntry `yaml:"global_fallback_treatment"` + ByFlag map[string]fallbackTreatmentEntry `yaml:"by_flag_fallback_treatment"` + } + + if err := value.Decode(&m); err != nil { + return err + } + + out := dtos.FallbackTreatmentConfig{} + + // Procesar Global + if m.Global != nil && m.Global.Treatment != nil { + out.GlobalFallbackTreatment = &dtos.FallbackTreatment{ + Treatment: m.Global.Treatment, + Config: m.Global.Config, } - if len(m.ByFlag) > 0 { - out.ByFlagFallbackTreatment = make(map[string]dtos.FallbackTreatment) - for name, v := range m.ByFlag { - if v.Treatment != nil { - out.ByFlagFallbackTreatment[name] = dtos.FallbackTreatment{ - Treatment: v.Treatment, - Config: v.Config, - } + } + + // Procesar ByFlag + if len(m.ByFlag) > 0 { + out.ByFlagFallbackTreatment = make(map[string]dtos.FallbackTreatment) + for name, v := range m.ByFlag { + if v.Treatment != nil { + out.ByFlagFallbackTreatment[name] = dtos.FallbackTreatment{ + Treatment: v.Treatment, + Config: v.Config, } } } - f.parsed = &out - return nil } + + f.parsed = &out + f.raw = "" return nil } @@ -396,7 +414,7 @@ func (f *fallbackTreatmentInput) toConfig() (*dtos.FallbackTreatmentConfig, erro func parseFallbackTreatmentJSON(raw string) (*dtos.FallbackTreatmentConfig, error) { var wrapper struct { FallbackTreatment struct { - GlobalFallbackTreatment *fallbackTreatmentEntry `json:"global_fallback_treatment"` + GlobalFallbackTreatment *fallbackTreatmentEntry `json:"global_fallback_treatment"` ByFlagFallbackTreatment map[string]fallbackTreatmentEntry `json:"by_flag_fallback_treatment"` } `json:"fallback_treatment"` } diff --git a/splitio/link/protocol/v1/rpcs_test.go b/splitio/link/protocol/v1/rpcs_test.go index 1b36a66..6d5a0d8 100644 --- a/splitio/link/protocol/v1/rpcs_test.go +++ b/splitio/link/protocol/v1/rpcs_test.go @@ -117,6 +117,35 @@ func TestTreatmentRPCParsing(t *testing.T) { assert.Equal(t, lang.Ref("bk"), r.BucketingKey) assert.Equal(t, "feat1", r.Feature) assert.Nil(t, r.Attributes) + + // impression properties (5th arg): valid map + r = TreatmentArgs{} + err = r.PopulateFromRPC(&RPC{ + RPCBase: protocol.RPCBase{Version: protocol.V1}, + OpCode: OCTreatment, + Args: []interface{}{"key", "bk", "feat1", nil, map[string]interface{}{"p": "v"}}}) + assert.Nil(t, err) + assert.Equal(t, "key", r.Key) + assert.Equal(t, "feat1", r.Feature) + assert.NotNil(t, r.ImpressionProperties) + assert.Equal(t, map[string]interface{}{"p": "v"}, r.ImpressionProperties) + + // impression properties (5th arg): invalid type returns PECInvalidArgType at index 4 + assert.Equal(t, + RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentArgImpressionPropertiesIdx)}, + r.PopulateFromRPC(&RPC{ + RPCBase: protocol.RPCBase{Version: protocol.V1}, + OpCode: OCTreatment, + Args: []interface{}{"key", "bk", "feat1", nil, 123}})) + + // 4 args only: impression properties not set (optional) + r = TreatmentArgs{} + err = r.PopulateFromRPC(&RPC{ + RPCBase: protocol.RPCBase{Version: protocol.V1}, + OpCode: OCTreatment, + Args: []interface{}{"key", "bk", "feat1", map[string]interface{}{"a": 1}}}) + assert.Nil(t, err) + assert.Nil(t, r.ImpressionProperties) } func TestTreatmentsRPCParsing(t *testing.T) { From 339d841138f9e0bcbfb9b0c05e29ee0a0a7fa736 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 18 Mar 2026 07:57:01 -0500 Subject: [PATCH 25/26] Add more test cases for rpcs and sdk --- splitio/link/protocol/v1/rpcs_test.go | 24 +++++ splitio/sdk/sdk_test.go | 148 ++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/splitio/link/protocol/v1/rpcs_test.go b/splitio/link/protocol/v1/rpcs_test.go index 6d5a0d8..20d18f6 100644 --- a/splitio/link/protocol/v1/rpcs_test.go +++ b/splitio/link/protocol/v1/rpcs_test.go @@ -334,6 +334,30 @@ func TestTreatmentsByFlagSetsRPCParsing(t *testing.T) { assert.Equal(t, lang.Ref("bk"), r.BucketingKey) assert.Equal(t, []string{"set_1", "set_2"}, r.FlagSets) assert.Nil(t, r.Attributes) + assert.Nil(t, r.ImpressionProperties) + + // impression properties (5th arg): valid map + r = TreatmentsByFlagSetsArgs{} + err = r.PopulateFromRPC(&RPC{ + RPCBase: protocol.RPCBase{Version: protocol.V1}, + OpCode: OCTreatmentsByFlagSets, + Args: []interface{}{ + "key", "bk", []interface{}{"set_1", "set_2"}, nil, + map[string]interface{}{"p": "v"}, + }}) + assert.Nil(t, err) + assert.Equal(t, "key", r.Key) + assert.Equal(t, []string{"set_1", "set_2"}, r.FlagSets) + assert.NotNil(t, r.ImpressionProperties) + assert.Equal(t, map[string]interface{}{"p": "v"}, r.ImpressionProperties) + + // impression properties (5th arg): invalid type + assert.Equal(t, + RPCParseError{Code: PECInvalidArgType, Data: int64(TreatmentsByFlagSetsArgImpressionPropertiesIdx)}, + r.PopulateFromRPC(&RPC{ + RPCBase: protocol.RPCBase{Version: protocol.V1}, + OpCode: OCTreatmentsByFlagSets, + Args: []interface{}{"key", "bk", []interface{}{"s"}, nil, 123}})) } func TestTrackRPCParsing(t *testing.T) { diff --git a/splitio/sdk/sdk_test.go b/splitio/sdk/sdk_test.go index 230534f..b8311a4 100644 --- a/splitio/sdk/sdk_test.go +++ b/splitio/sdk/sdk_test.go @@ -19,6 +19,7 @@ import ( "github.com/splitio/splitd/splitio/sdk/workers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) func TestTreatmentsWithImpressionsDisabled(t *testing.T) { @@ -179,6 +180,153 @@ func TestTreatmentLabelsEnabled(t *testing.T) { assert.Nil(t, err) } +func TestTreatment_ReplacesControlWithGlobalFallback(t *testing.T) { + is, _ := storage.NewImpressionsQueue(100) + ev := &mocks.EvaluatorMock{} + ev.On("EvaluateFeature", "key1", (*string)(nil), "unknown_feature", Attributes(nil)). + Return(&evaluator.Result{Treatment: "control", Label: "l", EvaluationTime: 1, SplitChangeNumber: 0}). + Once() + + fb := "my_fallback" + cfgStr := `{"k":1}` + im := &mocks.ImpressionManagerMock{} + im.On("Process", mock.Anything). + Return([]dtos.Impression{{KeyName: "key1", FeatureName: "unknown_feature", Treatment: "control"}}, []dtos.Impression{}). + Once() + + client := &Impl{ + logger: logging.NewLogger(nil), + is: is, + ev: ev, + iq: im, + cfg: conf.Config{ + LabelsEnabled: false, + FallbackTreatment: dtos.FallbackTreatmentConfig{ + GlobalFallbackTreatment: &dtos.FallbackTreatment{Treatment: &fb, Config: &cfgStr}, + }, + }, + } + + res, err := client.Treatment( + &types.ClientConfig{Metadata: types.ClientMetadata{ID: "c", SdkVersion: "go-1"}}, + "key1", nil, "unknown_feature", nil, nil) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, "my_fallback", res.Treatment) + require.NotNil(t, res.Config) + assert.Equal(t, cfgStr, *res.Config) + ev.AssertExpectations(t) +} + +func TestTreatment_ReplacesControlWithByFlagFallback(t *testing.T) { + is, _ := storage.NewImpressionsQueue(100) + ev := &mocks.EvaluatorMock{} + ev.On("EvaluateFeature", "key1", (*string)(nil), "flag_a", Attributes(nil)). + Return(&evaluator.Result{Treatment: "control", Config: nil, Label: "l", EvaluationTime: 1, SplitChangeNumber: 0}). + Once() + + byFlag := "per_flag" + im := &mocks.ImpressionManagerMock{} + im.On("Process", mock.Anything). + Return([]dtos.Impression{{KeyName: "key1", FeatureName: "flag_a", Treatment: "control"}}, []dtos.Impression{}). + Once() + + client := &Impl{ + logger: logging.NewLogger(nil), + is: is, + ev: ev, + iq: im, + cfg: conf.Config{ + FallbackTreatment: dtos.FallbackTreatmentConfig{ + ByFlagFallbackTreatment: map[string]dtos.FallbackTreatment{ + "flag_a": {Treatment: &byFlag}, + }, + }, + }, + } + + res, err := client.Treatment( + &types.ClientConfig{Metadata: types.ClientMetadata{ID: "c", SdkVersion: "go-1"}}, + "key1", nil, "flag_a", nil, nil) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, "per_flag", res.Treatment) + assert.Nil(t, res.Config) +} + +func TestTreatment_KeepsControlWhenConfiguredFallbackIsControl(t *testing.T) { + is, _ := storage.NewImpressionsQueue(100) + ev := &mocks.EvaluatorMock{} + ev.On("EvaluateFeature", "key1", (*string)(nil), "f", Attributes(nil)). + Return(&evaluator.Result{Treatment: "control", Label: "l", EvaluationTime: 1, SplitChangeNumber: 0}). + Once() + + ctrl := "control" + im := &mocks.ImpressionManagerMock{} + im.On("Process", mock.Anything). + Return([]dtos.Impression{{KeyName: "key1", FeatureName: "f", Treatment: "control"}}, []dtos.Impression{}). + Once() + + client := &Impl{ + logger: logging.NewLogger(nil), + is: is, + ev: ev, + iq: im, + cfg: conf.Config{ + FallbackTreatment: dtos.FallbackTreatmentConfig{ + GlobalFallbackTreatment: &dtos.FallbackTreatment{Treatment: &ctrl}, + }, + }, + } + + res, err := client.Treatment( + &types.ClientConfig{Metadata: types.ClientMetadata{ID: "c", SdkVersion: "go-1"}}, + "key1", nil, "f", nil, nil) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, "control", res.Treatment) +} + +func TestTreatments_ReplacesControlWithGlobalFallback(t *testing.T) { + is, _ := storage.NewImpressionsQueue(100) + ev := &mocks.EvaluatorMock{} + ev.On("EvaluateFeatures", "key1", (*string)(nil), []string{"missing", "ok"}, Attributes(nil)). + Return(evaluator.Results{Evaluations: map[string]evaluator.Result{ + "missing": {Treatment: "control", Label: "l1", EvaluationTime: 1, SplitChangeNumber: 0}, + "ok": {Treatment: "on", Label: "l2", EvaluationTime: 1, SplitChangeNumber: 1}, + }}). + Once() + + fb := "fallback_val" + im := &mocks.ImpressionManagerMock{} + im.On("Process", mock.Anything). + Return([]dtos.Impression{{FeatureName: "missing", Treatment: "control"}}, []dtos.Impression{}). + Once() + im.On("Process", mock.Anything). + Return([]dtos.Impression{{FeatureName: "ok", Treatment: "on"}}, []dtos.Impression{}). + Once() + + client := &Impl{ + logger: logging.NewLogger(nil), + is: is, + ev: ev, + iq: im, + cfg: conf.Config{ + LabelsEnabled: true, + FallbackTreatment: dtos.FallbackTreatmentConfig{ + GlobalFallbackTreatment: &dtos.FallbackTreatment{Treatment: &fb}, + }, + }, + } + + out, err := client.Treatments( + &types.ClientConfig{Metadata: types.ClientMetadata{ID: "c", SdkVersion: "go-1"}}, + "key1", nil, []string{"missing", "ok"}, nil, nil) + assert.NoError(t, err) + assert.Equal(t, "fallback_val", out["missing"].Treatment) + assert.Equal(t, "on", out["ok"].Treatment) +} + func TestTreatments(t *testing.T) { is, _ := storage.NewImpressionsQueue(100) From a7e670274c200966ace015fb323d9b5a6493ba0b Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 18 Mar 2026 08:05:23 -0500 Subject: [PATCH 26/26] Updated changes and version --- CHANGES | 2 +- splitio/version.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 700c21f..92df9e8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,4 @@ -1.7.0 (Jan 21, 2026) +1.7.0 (March 18, 2026) - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. - Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. diff --git a/splitio/version.go b/splitio/version.go index e8af325..650e099 100644 --- a/splitio/version.go +++ b/splitio/version.go @@ -1,3 +1,3 @@ package splitio -const Version = "1.7.0-rc" +const Version = "1.7.0"