Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

- name: SonarQube Scan (Push)
if: ${{ github.event_name == 'push' }}
uses: SonarSource/sonarqube-scan-action@v5.0.0
uses: SonarSource/sonarqube-scan-action@v6.0.0
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -46,7 +46,7 @@ jobs:

- name: SonarQube Scan (Pull Request)
if: ${{ github.event_name == 'pull_request' }}
uses: SonarSource/sonarqube-scan-action@v5.0.0
uses: SonarSource/sonarqube-scan-action@v6.0.0
env:
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
9.1.0 (Dec 17, 2025)
- Added impression properties.

9.0.0 (Nov 21, 2025)
- BREAKING CHANGE:
- Changed new evaluator.
Expand Down
6 changes: 6 additions & 0 deletions dtos/impression.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Impression struct {
Time int64 `json:"m"`
Pt int64 `json:"pt,omitempty"`
Disabled bool `json:"-"`
Properties string `json:"properties,omitempty"`
}

// ImpressionQueueObject struct mapping impressions
Expand All @@ -28,6 +29,7 @@ type ImpressionDTO struct {
Label string `json:"r"`
BucketingKey string `json:"b,omitempty"`
Pt int64 `json:"pt,omitempty"`
Properties string `json:"properties,omitempty"`
}

// ImpressionsDTO struct mapping impressions to post
Expand All @@ -47,3 +49,7 @@ type ImpressionsInTimeFrameDTO struct {
type ImpressionsCountDTO struct {
PerFeature []ImpressionsInTimeFrameDTO `json:"pf"`
}

type EvaluationOptions struct {
Properties map[string]interface{}
}
18 changes: 11 additions & 7 deletions engine/evaluator/mocks/mocks.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
package mocks

import "github.com/splitio/go-split-commons/v9/engine/evaluator"
import (
"github.com/splitio/go-split-commons/v9/engine/evaluator"
"github.com/stretchr/testify/mock"
)

// MockEvaluator mock evaluator
type MockEvaluator struct {
EvaluateFeatureCall func(key string, bucketingKey *string, feature string, attributes map[string]interface{}) *evaluator.Result
EvaluateFeaturesCall func(key string, bucketingKey *string, features []string, attributes map[string]interface{}) evaluator.Results
EvaluateFeatureByFlagSetsCall func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results
mock.Mock
}

// EvaluateFeature mock
func (m MockEvaluator) EvaluateFeature(key string, bucketingKey *string, feature string, attributes map[string]interface{}) *evaluator.Result {
return m.EvaluateFeatureCall(key, bucketingKey, feature, attributes)
args := m.Called(key, bucketingKey, feature, attributes)
return args.Get(0).(*evaluator.Result)
}

// EvaluateFeatures mock
func (m MockEvaluator) EvaluateFeatures(key string, bucketingKey *string, features []string, attributes map[string]interface{}) evaluator.Results {
return m.EvaluateFeaturesCall(key, bucketingKey, features, attributes)
args := m.Called(key, bucketingKey, features, attributes)
return args.Get(0).(evaluator.Results)
}

// EvaluateFeaturesByFlagSets mock
func (m MockEvaluator) EvaluateFeatureByFlagSets(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results {
return m.EvaluateFeatureByFlagSetsCall(key, bucketingKey, flagSets, attributes)
args := m.Called(key, bucketingKey, flagSets, attributes)
return args.Get(0).(evaluator.Results)
}
4 changes: 3 additions & 1 deletion provisional/strategy/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ func NewDebugImpl(impressionObserver ImpressionObserver, listenerEnabled bool) P
}

func (s *DebugImpl) apply(impression *dtos.Impression) bool {
impression.Pt, _ = s.impressionObserver.TestAndSet(impression.FeatureName, impression)
if len(impression.Properties) == 0 {
impression.Pt, _ = s.impressionObserver.TestAndSet(impression.FeatureName, impression)
}

return true
}
Expand Down
43 changes: 32 additions & 11 deletions provisional/strategy/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/splitio/go-split-commons/v9/dtos"
"github.com/stretchr/testify/assert"
)

func TestDebugMode(t *testing.T) {
Expand All @@ -22,15 +23,39 @@ func TestDebugMode(t *testing.T) {

toLog, toListener := debug.Apply([]dtos.Impression{imp})

if len(toLog) != 1 || len(toListener) != 1 {
t.Error("Should have 1 to log")
}
assert.Equal(t, 1, len(toLog), "Should have 1 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")

toLog, toListener = debug.Apply([]dtos.Impression{imp})

if len(toLog) != 1 || len(toListener) != 1 {
t.Error("Should have 1 to log")
assert.Equal(t, 1, len(toLog), "Should have 1 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")
}

func TestDebugModeWithProperties(t *testing.T) {
observer, _ := NewImpressionObserver(5000)
debug := NewDebugImpl(observer, true)

imp := dtos.Impression{
BucketingKey: "someBuck",
ChangeNumber: 123,
KeyName: "someKey",
Label: "someLabel",
Time: 123456,
Treatment: "on",
FeatureName: "feature-test",
Properties: "{'hello':'world'}",
}

toLog, toListener := debug.Apply([]dtos.Impression{imp})

assert.Equal(t, 1, len(toLog), "Should have 1 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")

toLog, toListener = debug.Apply([]dtos.Impression{imp})

assert.Equal(t, 1, len(toLog), "Should have 1 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")
}

func TestApplySingleDebug(t *testing.T) {
Expand All @@ -48,13 +73,9 @@ func TestApplySingleDebug(t *testing.T) {

toLog := debug.ApplySingle(&imp)

if !toLog {
t.Error("Should be true")
}
assert.True(t, toLog, "Should be true")

toLog = debug.ApplySingle(&imp)

if !toLog {
t.Error("Should be true")
}
assert.True(t, toLog, "Should be true")
}
3 changes: 3 additions & 0 deletions provisional/strategy/optimized.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func NewOptimizedImpl(impressionObserver ImpressionObserver, impressionCounter *
}

func (s *OptimizedImpl) apply(impression *dtos.Impression, now int64) bool {
if len(impression.Properties) != 0 {
return true
}
impression.Pt, _ = s.impressionObserver.TestAndSet(impression.FeatureName, impression)
if impression.Pt != 0 {
s.impressionsCounter.Inc(impression.FeatureName, now, 1)
Expand Down
65 changes: 42 additions & 23 deletions provisional/strategy/optimized_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/splitio/go-split-commons/v9/dtos"
"github.com/splitio/go-split-commons/v9/storage/inmemory"
"github.com/stretchr/testify/assert"
)

func TestOptimizedMode(t *testing.T) {
Expand All @@ -24,32 +25,55 @@ func TestOptimizedMode(t *testing.T) {
}

toLog, toListener := optimized.Apply([]dtos.Impression{imp})
assert.Equal(t, 1, len(toLog), "Should have 1 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")

if len(toLog) != 1 || len(toListener) != 1 {
t.Error("Should have 1 to log")
assert.Equal(t, 0, len(counter.impressionsCounts), "Should not have counts")

toLog, toListener = optimized.Apply([]dtos.Impression{imp})

assert.Equal(t, 0, len(toLog), "Should have 0 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")

rawCounts := counter.PopAll()
assert.Equal(t, 1, len(rawCounts), "Should have counts")
for key, counts := range counter.PopAll() {
assert.Equal(t, "feature-test", key.FeatureName, "Feature should be feature-test")
assert.Equal(t, 1, counts, "It should be tracked only once")
}
}

if len(counter.impressionsCounts) != 0 {
t.Error("Should not have counts")
func TestOptimizedModeWithProperties(t *testing.T) {
observer, _ := NewImpressionObserver(5000)
counter := NewImpressionsCounter()
runtimeTelemetry, _ := inmemory.NewTelemetryStorage()
optimized := NewOptimizedImpl(observer, counter, runtimeTelemetry, true)
imp := dtos.Impression{
BucketingKey: "someBuck",
ChangeNumber: 123,
KeyName: "someKey",
Label: "someLabel",
Time: time.Now().UTC().UnixNano(),
Treatment: "on",
FeatureName: "feature-test",
Properties: "{'hello':'world'}",
}

toLog, toListener := optimized.Apply([]dtos.Impression{imp})

assert.Equal(t, 1, len(toLog), "Should have 1 to log")
assert.Equal(t, 1, len(toListener), "Should have 1 to listener")

toLog, toListener = optimized.Apply([]dtos.Impression{imp})

if len(toLog) != 0 || len(toListener) != 1 {
t.Error("Should not have to log")
}
assert.Equal(t, 1, len(toLog), "toLog should be 1")
assert.Equal(t, 1, len(toListener), "toListener should be 1")

rawCounts := counter.PopAll()
if len(rawCounts) != 1 {
t.Error("Should have counts")
}
assert.Equal(t, 0, len(rawCounts), "Should doesn't have counts")
for key, counts := range counter.PopAll() {
if key.FeatureName != "feature-test" {
t.Error("Feature should be feature-test")
}
if counts != 1 {
t.Error("It should be tracked only once")
}
assert.Equal(t, "feature-test", key.FeatureName, "Feature should be feature-test")
assert.Equal(t, 1, counts, "It should be tracked empty")
}
}

Expand All @@ -70,13 +94,8 @@ func TestApplySingleOptimized(t *testing.T) {

toLog := optimized.ApplySingle(&imp)

if !toLog {
t.Error("Should be true")
}

assert.True(t, toLog, "Should be true")
toLog = optimized.ApplySingle(&imp)

if toLog {
t.Error("Should be false")
}
assert.False(t, toLog, "Should be false")
}
21 changes: 21 additions & 0 deletions service/api/http_recorders_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,27 @@ func TestImpressionRecord(t *testing.T) {
}
}

func TestImpressionRecordWithProperties(t *testing.T) {
impressionTXT := `{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key","properties":"value"}`
impressionRecord := &dtos.ImpressionDTO{
KeyName: "some_key",
Treatment: "off",
Time: 1234567890,
ChangeNumber: 55555555,
Label: "some label",
BucketingKey: "some_bucket_key",
Properties: "value"}

marshalImpression, err := json.Marshal(impressionRecord)
if err != nil {
t.Error(err)
}

if string(marshalImpression) != impressionTXT {
t.Error("Error marshaling impression")
}
}

func TestImpressionRecordBulk(t *testing.T) {
impressionTXT := `{"f":"some_feature","i":[{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key"}]}`
impressionRecords := &dtos.ImpressionsDTO{
Expand Down
1 change: 1 addition & 0 deletions synchronizer/worker/impression/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func (i *RecorderSingle) SynchronizeImpressions(bulkSize int64) error {
Label: impression.Label,
BucketingKey: impression.BucketingKey,
Pt: impression.Pt,
Properties: impression.Properties,
}
v, ok := impressionsToPost[impression.FeatureName]
if ok {
Expand Down
8 changes: 5 additions & 3 deletions synchronizer/worker/impression/single_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/splitio/go-split-commons/v9/storage/mocks"
"github.com/splitio/go-split-commons/v9/telemetry"
"github.com/splitio/go-toolkit/v5/logging"
"github.com/stretchr/testify/assert"
)

func TestImpressionRecorderError(t *testing.T) {
Expand Down Expand Up @@ -63,7 +64,7 @@ func TestImpressionRecorderWithoutImpressions(t *testing.T) {

func TestSynhronizeEventErrorRecorder(t *testing.T) {
impression := dtos.Impression{
BucketingKey: "someBucketingKey1", ChangeNumber: 123456789, FeatureName: "someFeature1",
BucketingKey: "someBucketingKey1", ChangeNumber: 123456789, FeatureName: "someFeature1", Properties: "{'prop':'val'}",
KeyName: "someKey1", Label: "someLabel", Time: 123456789, Treatment: "someTreatment1",
}

Expand Down Expand Up @@ -104,7 +105,7 @@ func TestImpressionRecorder(t *testing.T) {
before := time.Now().UTC()
impression1 := dtos.Impression{
BucketingKey: "someBucketingKey1", ChangeNumber: 123456789, FeatureName: "someFeature1",
KeyName: "someKey1", Label: "someLabel", Time: 123456789, Treatment: "someTreatment1",
KeyName: "someKey1", Label: "someLabel", Time: 123456789, Treatment: "someTreatment1", Properties: "{'prop':'val'}",
}
impression2 := dtos.Impression{
BucketingKey: "someBucketingKey2", ChangeNumber: 123456789, FeatureName: "someFeature2",
Expand Down Expand Up @@ -215,6 +216,7 @@ func TestImpressionRecorderSync(t *testing.T) {
if !ok || len(imp1.KeyImpressions) != 2 {
t.Error("Incorrect impressions received")
}
assert.Equal(t, "{'prop':'val'}", imp1.KeyImpressions[0].Properties)
imp2, ok := result["someFeature2"]
if !ok || len(imp2.KeyImpressions) != 1 {
t.Error("Incorrect impressions received")
Expand All @@ -227,7 +229,7 @@ func TestImpressionRecorderSync(t *testing.T) {

impression1 := dtos.Impression{
BucketingKey: "someBucketingKey1", ChangeNumber: 123456789, FeatureName: "someFeature1",
KeyName: "someKey1", Label: "someLabel", Time: 123456789, Treatment: "someTreatment1",
KeyName: "someKey1", Label: "someLabel", Time: 123456789, Treatment: "someTreatment1", Properties: "{'prop':'val'}",
}
impression2 := dtos.Impression{
BucketingKey: "someBucketingKey2", ChangeNumber: 123456789, FeatureName: "someFeature2",
Expand Down
8 changes: 8 additions & 0 deletions telemetry/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ func IsMethodValid(method *string) bool {
case "getTreatmentsByFlagSets", "get_treatments_by_flag_sets", "treatmentsByFlagSets", "TreatmentsByFlagSets":
case "getTreatmentsWithConfigByFlagSet", "get_treatments_with_config_by_flag_set", "treatmentsWithConfigByFlagSet", "TreatmentsWithConfigByFlagSet":
case "getTreatmentsWithConfigByFlagSets", "get_treatments_with_config_by_flag_sets", "treatmentsWithConfigByFlagSets", "TreatmentsWithConfigByFlagSets":
case "getTreatmentWithEvaluationOptions", "get_treatment_with_evaluation_options", "treatmentWithEvaluationOptions", "TreatmentWithEvaluationOptions":
case "getTreatmentsWithEvaluationOptions", "get_treatments_with_evaluation_options", "treatmentsWithEvaluationOptions", "TreatmentsWithEvaluationOptions":
case "getTreatmentWithConfigAndEvaluationOptions", "get_treatment_with_config_and_evaluation_options", "treatment_with_config_and_evaluation_options", "treatmentWithConfigAndEvaluationOptions", "TreatmentWithConfigAndEvaluationOptions":
case "getTreatmentsWithConfigAndEvaluationOption", "get_treatments_with_config_and_evaluation_options", "treatments_with_config_and_evaluation_options", "treatmentsWithConfigAndEvaluationOptions", "TreatmentsWithConfigAndEvaluationOptions":
case "getTreatmentsByFlagSetWithEvaluationOptions", "get_treatments_by_flag_set_with_evaluation_options", "treatments_by_flag_set_with_evaluation_options", "treatmentsByFlagSetWithEvaluationOptions", "TreatmentsByFlagSetWithEvaluationOptions":
case "getTreatmentsByFlagSetsWithEvaluationOptions", "get_treatments_by_flag_sets_with_evaluation_options", "treatments_by_flag_sets_with_evaluation_options", "treatmentsByFlagSetsWithEvaluationOptions", "TreatmentsByFlagSetsWithEvaluationOptions":
case "getTreatmentsWithConfigByFlagSetAndEvaluationOptions", "get_treatments_with_config_by_flag_set_and_evaluation_options", "treatments_with_config_by_flag_set_and_evaluation_options", "treatmentsWithConfigByFlagSetAndEvaluationOptions", "TreatmentsWithConfigByFlagSetAndEvaluationOptions":
case "getTreatmentsWithConfigByFlagSetsAndEvaluationOptions", "get_treatments_with_config_by_flag_sets_and_evaluation_options", "treatments_with_config_by_flag_sets_and_evaluation_options", "treatmentsWithConfigByFlagSetsAndEvaluationOptions", "TreatmentsWithConfigByFlagSetsAndEvaluationOptions":
case "track", "Track":
default:
return false
Expand Down