From 5193e610e050773c4df6b399bd7c4cdcf919e9c0 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 27 Nov 2025 11:31:50 -0300 Subject: [PATCH 01/12] Added properties in impressions and created evaluation properties dto --- dtos/impression.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dtos/impression.go b/dtos/impression.go index f58d5a9f..255a74fe 100644 --- a/dtos/impression.go +++ b/dtos/impression.go @@ -28,6 +28,7 @@ type ImpressionDTO struct { Label string `json:"r"` BucketingKey string `json:"b,omitempty"` Pt int64 `json:"pt,omitempty"` + Properties string `json:"properties"` } // ImpressionsDTO struct mapping impressions to post @@ -47,3 +48,7 @@ type ImpressionsInTimeFrameDTO struct { type ImpressionsCountDTO struct { PerFeature []ImpressionsInTimeFrameDTO `json:"pf"` } + +type EvaluationOptions struct { + properties map[string]interface{} +} From 4847216af2ebb21e9744b5af66ef7b3786b9d4ee Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 27 Nov 2025 12:13:22 -0300 Subject: [PATCH 02/12] Updated tests --- dtos/impression.go | 1 + service/api/http_recorders_test.go | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/dtos/impression.go b/dtos/impression.go index 255a74fe..094c49ae 100644 --- a/dtos/impression.go +++ b/dtos/impression.go @@ -11,6 +11,7 @@ type Impression struct { Time int64 `json:"m"` Pt int64 `json:"pt,omitempty"` Disabled bool `json:"-"` + Properties string `json:"properties"` } // ImpressionQueueObject struct mapping impressions diff --git a/service/api/http_recorders_test.go b/service/api/http_recorders_test.go index a21c6e25..019b9975 100644 --- a/service/api/http_recorders_test.go +++ b/service/api/http_recorders_test.go @@ -15,7 +15,7 @@ import ( ) func TestImpressionRecord(t *testing.T) { - impressionTXT := `{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key"}` + impressionTXT := `{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key","properties":""}` impressionRecord := &dtos.ImpressionDTO{ KeyName: "some_key", Treatment: "off", @@ -34,8 +34,29 @@ 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"}]}` + impressionTXT := `{"f":"some_feature","i":[{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key","properties":""}]}` impressionRecords := &dtos.ImpressionsDTO{ TestName: "some_feature", KeyImpressions: []dtos.ImpressionDTO{{ @@ -58,7 +79,7 @@ func TestImpressionRecordBulk(t *testing.T) { } func TestPostImpressions(t *testing.T) { - impressionsTXT := `[{"f":"some_test_2","i":[{"k":"some_key_1","t":"on","m":1234567890,"c":9876543210,"r":"some_label_1","b":"some_bucket_key_1"}]},{"f":"some_test","i":[{"k":"some_key_2","t":"off","m":1234567890,"c":9876543210,"r":"some_label_2","b":"some_bucket_key_2"}]}]` + impressionsTXT := `[{"f":"some_test_2","i":[{"k":"some_key_1","t":"on","m":1234567890,"c":9876543210,"r":"some_label_1","b":"some_bucket_key_1","properties":""}]},{"f":"some_test","i":[{"k":"some_key_2","t":"off","m":1234567890,"c":9876543210,"r":"some_label_2","b":"some_bucket_key_2","properties":""}]}]` logger := logging.NewLogger(&logging.LoggerOptions{}) var expectedPT int64 From b2c40fefa714472063cc750f35f8e563401fdf5b Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 27 Nov 2025 13:07:42 -0300 Subject: [PATCH 03/12] Updated impression properties --- dtos/impression.go | 4 ++-- service/api/http_recorders_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dtos/impression.go b/dtos/impression.go index 094c49ae..ae8be53e 100644 --- a/dtos/impression.go +++ b/dtos/impression.go @@ -11,7 +11,7 @@ type Impression struct { Time int64 `json:"m"` Pt int64 `json:"pt,omitempty"` Disabled bool `json:"-"` - Properties string `json:"properties"` + Properties string `json:"properties,omitempty"` } // ImpressionQueueObject struct mapping impressions @@ -29,7 +29,7 @@ type ImpressionDTO struct { Label string `json:"r"` BucketingKey string `json:"b,omitempty"` Pt int64 `json:"pt,omitempty"` - Properties string `json:"properties"` + Properties string `json:"properties,omitempty"` } // ImpressionsDTO struct mapping impressions to post diff --git a/service/api/http_recorders_test.go b/service/api/http_recorders_test.go index 019b9975..0551a6e6 100644 --- a/service/api/http_recorders_test.go +++ b/service/api/http_recorders_test.go @@ -15,7 +15,7 @@ import ( ) func TestImpressionRecord(t *testing.T) { - impressionTXT := `{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key","properties":""}` + impressionTXT := `{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key"}` impressionRecord := &dtos.ImpressionDTO{ KeyName: "some_key", Treatment: "off", @@ -56,7 +56,7 @@ func TestImpressionRecordWithProperties(t *testing.T) { } 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","properties":""}]}` + impressionTXT := `{"f":"some_feature","i":[{"k":"some_key","t":"off","m":1234567890,"c":55555555,"r":"some label","b":"some_bucket_key"}]}` impressionRecords := &dtos.ImpressionsDTO{ TestName: "some_feature", KeyImpressions: []dtos.ImpressionDTO{{ @@ -79,7 +79,7 @@ func TestImpressionRecordBulk(t *testing.T) { } func TestPostImpressions(t *testing.T) { - impressionsTXT := `[{"f":"some_test_2","i":[{"k":"some_key_1","t":"on","m":1234567890,"c":9876543210,"r":"some_label_1","b":"some_bucket_key_1","properties":""}]},{"f":"some_test","i":[{"k":"some_key_2","t":"off","m":1234567890,"c":9876543210,"r":"some_label_2","b":"some_bucket_key_2","properties":""}]}]` + impressionsTXT := `[{"f":"some_test_2","i":[{"k":"some_key_1","t":"on","m":1234567890,"c":9876543210,"r":"some_label_1","b":"some_bucket_key_1"}]},{"f":"some_test","i":[{"k":"some_key_2","t":"off","m":1234567890,"c":9876543210,"r":"some_label_2","b":"some_bucket_key_2"}]}]` logger := logging.NewLogger(&logging.LoggerOptions{}) var expectedPT int64 From 1f48a7e30dfd5ae805031b21b741ec894a611e7e Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Sun, 30 Nov 2025 23:11:16 -0300 Subject: [PATCH 04/12] Updated properties --- dtos/impression.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtos/impression.go b/dtos/impression.go index ae8be53e..6133359a 100644 --- a/dtos/impression.go +++ b/dtos/impression.go @@ -51,5 +51,5 @@ type ImpressionsCountDTO struct { } type EvaluationOptions struct { - properties map[string]interface{} + Properties map[string]interface{} } From 2d75a5208d2f61f3ac00f6624c331ec4d343dbb3 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 1 Dec 2025 11:06:27 -0300 Subject: [PATCH 05/12] Updated Evaluator mocks --- engine/evaluator/mocks/mocks.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/engine/evaluator/mocks/mocks.go b/engine/evaluator/mocks/mocks.go index 2d5e7cff..55a8cf60 100644 --- a/engine/evaluator/mocks/mocks.go +++ b/engine/evaluator/mocks/mocks.go @@ -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) } From efbf310404bccb10f0bac3a922a89147ad002118 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Mon, 1 Dec 2025 17:22:30 -0300 Subject: [PATCH 06/12] Updated impression optimize and debug --- provisional/strategy/debug.go | 4 +- provisional/strategy/debug_test.go | 43 ++++++++++++----- provisional/strategy/optimized.go | 3 ++ provisional/strategy/optimized_test.go | 65 +++++++++++++++++--------- 4 files changed, 80 insertions(+), 35 deletions(-) diff --git a/provisional/strategy/debug.go b/provisional/strategy/debug.go index 185b6aa8..af32de70 100644 --- a/provisional/strategy/debug.go +++ b/provisional/strategy/debug.go @@ -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 } diff --git a/provisional/strategy/debug_test.go b/provisional/strategy/debug_test.go index 0933292f..c0a80db4 100644 --- a/provisional/strategy/debug_test.go +++ b/provisional/strategy/debug_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/splitio/go-split-commons/v9/dtos" + "github.com/stretchr/testify/assert" ) func TestDebugMode(t *testing.T) { @@ -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) { @@ -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") } diff --git a/provisional/strategy/optimized.go b/provisional/strategy/optimized.go index 02215290..ad6bb683 100644 --- a/provisional/strategy/optimized.go +++ b/provisional/strategy/optimized.go @@ -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) diff --git a/provisional/strategy/optimized_test.go b/provisional/strategy/optimized_test.go index be249dad..aca1b446 100644 --- a/provisional/strategy/optimized_test.go +++ b/provisional/strategy/optimized_test.go @@ -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) { @@ -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") } } @@ -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") } From 69a1a4a8c1507679226e2056f6833d69c2ae6ee6 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Tue, 2 Dec 2025 15:43:15 -0300 Subject: [PATCH 07/12] Updated telemetry constants --- telemetry/constants.go | 40 +++++++++++++++++++++++++++++++++++++ telemetry/constants_test.go | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/telemetry/constants.go b/telemetry/constants.go index baaccfdd..00cb2ec7 100644 --- a/telemetry/constants.go +++ b/telemetry/constants.go @@ -19,6 +19,22 @@ const ( TreatmentsWithConfigByFlagSet = "treatmentsWithConfigByFlagSet" // TreatmentsWithConfigByFlagSets getTreatmentsWithConfigByFlagSets TreatmentsWithConfigByFlagSets = "treatmentsWithConfigByFlagSets" + // TreatmentWithEvaluationOptions getTreatmentWithEvaluationOptions + TreatmentWithEvaluationOptions = "treatmentWithEvaluationOptions" + // TreatmentsWithEvaluationOptions getTreatmentsWithEvaluationOptions + TreatmentsWithEvaluationOptions = "treatmentsWithEvaluationOptions" + // TreatmentWithConfigAndEvaluationOptions getTreatmentWithConfigAndEvaluationOptions + TreatmentWithConfigAndEvaluationOptions = "treatmentWithConfigAndEvaluationOptions" + // TreatmentsWithConfigAndEvaluationOptions getTreatmentsWithConfigAndEvaluationOptions + TreatmentsWithConfigAndEvaluationOptions = "treatmentsWithConfigAndEvaluationOptions" + // TreatmentsByFlagSetWithEvaluationOptions getTreatmentsByFlagSetWithEvaluationOptions + TreatmentsByFlagSetWithEvaluationOptions = "treatmentsByFlagSetWithEvaluationOptions" + // TreatmentsByFlagSetsWithEvaluationOptions getTreatmentsByFlagSetsWithEvaluationOptions + TreatmentsByFlagSetsWithEvaluationOptions = "treatmentsByFlagSetsWithEvaluationOptions" + // TreatmentsWithConfigByFlagSetAndEvaluationOptions getTreatmentsWithConfigByFlagSetAndEvaluationOptions + TreatmentsWithConfigByFlagSetAndEvaluationOptions = "treatmentsWithConfigByFlagSetAndEvaluationOptions" + // TreatmentsWithConfigByFlagSetsAndEvaluationOptions getTreatmentsWithConfigByFlagSetsAndEvaluationOptions + TreatmentsWithConfigByFlagSetsAndEvaluationOptions = "treatmentsWithConfigByFlagSetsAndEvaluationOptions" // Track track Track = "track" ) @@ -42,6 +58,22 @@ func ParseMethodFromRedisHash(method string) (normalized string, ok bool) { return TreatmentsWithConfigByFlagSet, true case "getTreatmentsWithConfigByFlagSets", "get_treatments_with_config_by_flag_sets", "treatments_with_config_by_flag_sets", "treatmentsWithConfigByFlagSets", "TreatmentsWithConfigByFlagSets": return TreatmentsWithConfigByFlagSets, true + case "getTreatmentWithEvaluationOptions", "get_treatment_with_evaluation_options", "treatmentWithEvaluationOptions", "TreatmentWithEvaluationOptions": + return TreatmentWithEvaluationOptions, true + case "getTreatmentsWithEvaluationOptions", "get_treatments_with_evaluation_options", "treatmentsWithEvaluationOptions", "TreatmentsWithEvaluationOptions": + return TreatmentsWithEvaluationOptions, true + case "getTreatmentWithConfigAndEvaluationOptions", "get_treatment_with_config_and_evaluation_options", "treatment_with_config_and_evaluation_options", "treatmentWithConfigAndEvaluationOptions", "TreatmentWithConfigAndEvaluationOptions": + return TreatmentWithConfigAndEvaluationOptions, true + case "getTreatmentsWithConfigAndEvaluationOption", "get_treatments_with_config_and_evaluation_options", "treatments_with_config_and_evaluation_options", "treatmentsWithConfigAndEvaluationOptions", "TreatmentsWithConfigAndEvaluationOptions": + return TreatmentsWithConfigAndEvaluationOptions, true + case "getTreatmentsByFlagSetWithEvaluationOptions", "get_treatments_by_flag_set_with_evaluation_options", "treatments_by_flag_set_with_evaluation_options", "treatmentsByFlagSetWithEvaluationOptions", "TreatmentsByFlagSetWithEvaluationOptions": + return TreatmentsByFlagSetWithEvaluationOptions, true + case "getTreatmentsByFlagSetsWithEvaluationOptions", "get_treatments_by_flag_sets_with_evaluation_options", "treatments_by_flag_sets_with_evaluation_options", "treatmentsByFlagSetsWithEvaluationOptions", "TreatmentsByFlagSetsWithEvaluationOptions": + return TreatmentsByFlagSetsWithEvaluationOptions, true + case "getTreatmentsWithConfigByFlagSetAndEvaluationOptions", "get_treatments_with_config_by_flag_set_and_evaluation_options", "treatments_with_config_by_flag_set_and_evaluation_options", "treatmentsWithConfigByFlagSetAndEvaluationOptions", "TreatmentsWithConfigByFlagSetAndEvaluationOptions": + return TreatmentsWithConfigByFlagSetAndEvaluationOptions, true + case "getTreatmentsWithConfigByFlagSetsAndEvaluationOptions", "get_treatments_with_config_by_flag_sets_and_evaluation_options", "treatments_with_config_by_flag_sets_and_evaluation_options", "treatmentsWithConfigByFlagSetsAndEvaluationOptions", "TreatmentsWithConfigByFlagSetsAndEvaluationOptions": + return TreatmentsWithConfigByFlagSetsAndEvaluationOptions, true case "track", "Track": return Track, true default: @@ -60,6 +92,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 diff --git a/telemetry/constants_test.go b/telemetry/constants_test.go index b7f37686..24155bb6 100644 --- a/telemetry/constants_test.go +++ b/telemetry/constants_test.go @@ -45,6 +45,46 @@ func TestMethodMapping(t *testing.T) { t.Error("expected `treatmentWithConfigByFlagSets`. Got: ", v) } } + for _, method := range []string{"getTreatmentWithEvaluationOptions", "get_treatment_with_evaluation_options", "treatmentWithEvaluationOptions", "TreatmentWithEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentWithEvaluationOptions { + t.Error("expected `TreatmentWithEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentsWithEvaluationOptions", "get_treatments_with_evaluation_options", "treatmentsWithEvaluationOptions", "TreatmentsWithEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithEvaluationOptions { + t.Error("expected `TreatmentsWithEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentWithConfigAndEvaluationOptions", "get_treatment_with_config_and_evaluation_options", "treatment_with_config_and_evaluation_options", "treatmentWithConfigAndEvaluationOptions", "TreatmentWithConfigAndEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentWithConfigAndEvaluationOptions { + t.Error("expected `TreatmentWithConfigAndEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentsWithConfigAndEvaluationOption", "get_treatments_with_config_and_evaluation_options", "treatments_with_config_and_evaluation_options", "treatmentsWithConfigAndEvaluationOptions", "TreatmentsWithConfigAndEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithConfigAndEvaluationOptions { + t.Error("expected `TreatmentsWithConfigAndEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentsByFlagSetWithEvaluationOptions", "get_treatments_by_flag_set_with_evaluation_options", "treatments_by_flag_set_with_evaluation_options", "treatmentsByFlagSetWithEvaluationOptions", "TreatmentsByFlagSetWithEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsByFlagSetWithEvaluationOptions { + t.Error("expected `TreatmentsByFlagSetWithEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentsByFlagSetsWithEvaluationOptions", "get_treatments_by_flag_sets_with_evaluation_options", "treatments_by_flag_sets_with_evaluation_options", "treatmentsByFlagSetsWithEvaluationOptions", "TreatmentsByFlagSetsWithEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsByFlagSetsWithEvaluationOptions { + t.Error("expected `TreatmentsByFlagSetsWithEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentsWithConfigByFlagSetAndEvaluationOptions", "get_treatments_with_config_by_flag_set_and_evaluation_options", "treatments_with_config_by_flag_set_and_evaluation_options", "treatmentsWithConfigByFlagSetAndEvaluationOptions", "TreatmentsWithConfigByFlagSetAndEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithConfigByFlagSetAndEvaluationOptions { + t.Error("expected `TreatmentsWithConfigByFlagSetAndEvaluationOptions`. Got: ", v) + } + } + for _, method := range []string{"getTreatmentsWithConfigByFlagSetsAndEvaluationOptions", "get_treatments_with_config_by_flag_sets_and_evaluation_options", "treatments_with_config_by_flag_sets_and_evaluation_options", "treatmentsWithConfigByFlagSetsAndEvaluationOptions", "TreatmentsWithConfigByFlagSetsAndEvaluationOptions"} { + if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithConfigByFlagSetsAndEvaluationOptions { + t.Error("expected `TreatmentsWithConfigByFlagSetsAndEvaluationOptions`. Got: ", v) + } + } for _, method := range []string{"track", "Track"} { if v, ok := ParseMethodFromRedisHash(method); !ok || v != Track { t.Error("expented `track`. Got: ", method) From 4e25329765a9a0b3b0aa2bb3c31ac839f492e41f Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 3 Dec 2025 10:16:26 -0300 Subject: [PATCH 08/12] Updated changes --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index 0457ccf8..29817ef1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +9.1.0 (Dec 3, 2025) +- Added impression properties. + 9.0.0 (Nov 21, 2025) - BREAKING CHANGE: - Changed new evaluator. From 05642e6c0f7731479d64d9243584184cc743dfd9 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 3 Dec 2025 10:18:55 -0300 Subject: [PATCH 09/12] Updated ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 824a59a6..1897d144 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 }} @@ -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 }} From 558205cace3b800cc1f1f2767da9edaabcf6dbc4 Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Fri, 5 Dec 2025 11:35:33 -0300 Subject: [PATCH 10/12] Updated constants --- CHANGES | 2 +- telemetry/constants.go | 32 ----------------------------- telemetry/constants_test.go | 40 ------------------------------------- 3 files changed, 1 insertion(+), 73 deletions(-) diff --git a/CHANGES b/CHANGES index 29817ef1..093a6d06 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,4 @@ -9.1.0 (Dec 3, 2025) +9.1.0 (Dec 5, 2025) - Added impression properties. 9.0.0 (Nov 21, 2025) diff --git a/telemetry/constants.go b/telemetry/constants.go index 00cb2ec7..3f76ff64 100644 --- a/telemetry/constants.go +++ b/telemetry/constants.go @@ -19,22 +19,6 @@ const ( TreatmentsWithConfigByFlagSet = "treatmentsWithConfigByFlagSet" // TreatmentsWithConfigByFlagSets getTreatmentsWithConfigByFlagSets TreatmentsWithConfigByFlagSets = "treatmentsWithConfigByFlagSets" - // TreatmentWithEvaluationOptions getTreatmentWithEvaluationOptions - TreatmentWithEvaluationOptions = "treatmentWithEvaluationOptions" - // TreatmentsWithEvaluationOptions getTreatmentsWithEvaluationOptions - TreatmentsWithEvaluationOptions = "treatmentsWithEvaluationOptions" - // TreatmentWithConfigAndEvaluationOptions getTreatmentWithConfigAndEvaluationOptions - TreatmentWithConfigAndEvaluationOptions = "treatmentWithConfigAndEvaluationOptions" - // TreatmentsWithConfigAndEvaluationOptions getTreatmentsWithConfigAndEvaluationOptions - TreatmentsWithConfigAndEvaluationOptions = "treatmentsWithConfigAndEvaluationOptions" - // TreatmentsByFlagSetWithEvaluationOptions getTreatmentsByFlagSetWithEvaluationOptions - TreatmentsByFlagSetWithEvaluationOptions = "treatmentsByFlagSetWithEvaluationOptions" - // TreatmentsByFlagSetsWithEvaluationOptions getTreatmentsByFlagSetsWithEvaluationOptions - TreatmentsByFlagSetsWithEvaluationOptions = "treatmentsByFlagSetsWithEvaluationOptions" - // TreatmentsWithConfigByFlagSetAndEvaluationOptions getTreatmentsWithConfigByFlagSetAndEvaluationOptions - TreatmentsWithConfigByFlagSetAndEvaluationOptions = "treatmentsWithConfigByFlagSetAndEvaluationOptions" - // TreatmentsWithConfigByFlagSetsAndEvaluationOptions getTreatmentsWithConfigByFlagSetsAndEvaluationOptions - TreatmentsWithConfigByFlagSetsAndEvaluationOptions = "treatmentsWithConfigByFlagSetsAndEvaluationOptions" // Track track Track = "track" ) @@ -58,22 +42,6 @@ func ParseMethodFromRedisHash(method string) (normalized string, ok bool) { return TreatmentsWithConfigByFlagSet, true case "getTreatmentsWithConfigByFlagSets", "get_treatments_with_config_by_flag_sets", "treatments_with_config_by_flag_sets", "treatmentsWithConfigByFlagSets", "TreatmentsWithConfigByFlagSets": return TreatmentsWithConfigByFlagSets, true - case "getTreatmentWithEvaluationOptions", "get_treatment_with_evaluation_options", "treatmentWithEvaluationOptions", "TreatmentWithEvaluationOptions": - return TreatmentWithEvaluationOptions, true - case "getTreatmentsWithEvaluationOptions", "get_treatments_with_evaluation_options", "treatmentsWithEvaluationOptions", "TreatmentsWithEvaluationOptions": - return TreatmentsWithEvaluationOptions, true - case "getTreatmentWithConfigAndEvaluationOptions", "get_treatment_with_config_and_evaluation_options", "treatment_with_config_and_evaluation_options", "treatmentWithConfigAndEvaluationOptions", "TreatmentWithConfigAndEvaluationOptions": - return TreatmentWithConfigAndEvaluationOptions, true - case "getTreatmentsWithConfigAndEvaluationOption", "get_treatments_with_config_and_evaluation_options", "treatments_with_config_and_evaluation_options", "treatmentsWithConfigAndEvaluationOptions", "TreatmentsWithConfigAndEvaluationOptions": - return TreatmentsWithConfigAndEvaluationOptions, true - case "getTreatmentsByFlagSetWithEvaluationOptions", "get_treatments_by_flag_set_with_evaluation_options", "treatments_by_flag_set_with_evaluation_options", "treatmentsByFlagSetWithEvaluationOptions", "TreatmentsByFlagSetWithEvaluationOptions": - return TreatmentsByFlagSetWithEvaluationOptions, true - case "getTreatmentsByFlagSetsWithEvaluationOptions", "get_treatments_by_flag_sets_with_evaluation_options", "treatments_by_flag_sets_with_evaluation_options", "treatmentsByFlagSetsWithEvaluationOptions", "TreatmentsByFlagSetsWithEvaluationOptions": - return TreatmentsByFlagSetsWithEvaluationOptions, true - case "getTreatmentsWithConfigByFlagSetAndEvaluationOptions", "get_treatments_with_config_by_flag_set_and_evaluation_options", "treatments_with_config_by_flag_set_and_evaluation_options", "treatmentsWithConfigByFlagSetAndEvaluationOptions", "TreatmentsWithConfigByFlagSetAndEvaluationOptions": - return TreatmentsWithConfigByFlagSetAndEvaluationOptions, true - case "getTreatmentsWithConfigByFlagSetsAndEvaluationOptions", "get_treatments_with_config_by_flag_sets_and_evaluation_options", "treatments_with_config_by_flag_sets_and_evaluation_options", "treatmentsWithConfigByFlagSetsAndEvaluationOptions", "TreatmentsWithConfigByFlagSetsAndEvaluationOptions": - return TreatmentsWithConfigByFlagSetsAndEvaluationOptions, true case "track", "Track": return Track, true default: diff --git a/telemetry/constants_test.go b/telemetry/constants_test.go index 24155bb6..b7f37686 100644 --- a/telemetry/constants_test.go +++ b/telemetry/constants_test.go @@ -45,46 +45,6 @@ func TestMethodMapping(t *testing.T) { t.Error("expected `treatmentWithConfigByFlagSets`. Got: ", v) } } - for _, method := range []string{"getTreatmentWithEvaluationOptions", "get_treatment_with_evaluation_options", "treatmentWithEvaluationOptions", "TreatmentWithEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentWithEvaluationOptions { - t.Error("expected `TreatmentWithEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentsWithEvaluationOptions", "get_treatments_with_evaluation_options", "treatmentsWithEvaluationOptions", "TreatmentsWithEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithEvaluationOptions { - t.Error("expected `TreatmentsWithEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentWithConfigAndEvaluationOptions", "get_treatment_with_config_and_evaluation_options", "treatment_with_config_and_evaluation_options", "treatmentWithConfigAndEvaluationOptions", "TreatmentWithConfigAndEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentWithConfigAndEvaluationOptions { - t.Error("expected `TreatmentWithConfigAndEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentsWithConfigAndEvaluationOption", "get_treatments_with_config_and_evaluation_options", "treatments_with_config_and_evaluation_options", "treatmentsWithConfigAndEvaluationOptions", "TreatmentsWithConfigAndEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithConfigAndEvaluationOptions { - t.Error("expected `TreatmentsWithConfigAndEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentsByFlagSetWithEvaluationOptions", "get_treatments_by_flag_set_with_evaluation_options", "treatments_by_flag_set_with_evaluation_options", "treatmentsByFlagSetWithEvaluationOptions", "TreatmentsByFlagSetWithEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsByFlagSetWithEvaluationOptions { - t.Error("expected `TreatmentsByFlagSetWithEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentsByFlagSetsWithEvaluationOptions", "get_treatments_by_flag_sets_with_evaluation_options", "treatments_by_flag_sets_with_evaluation_options", "treatmentsByFlagSetsWithEvaluationOptions", "TreatmentsByFlagSetsWithEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsByFlagSetsWithEvaluationOptions { - t.Error("expected `TreatmentsByFlagSetsWithEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentsWithConfigByFlagSetAndEvaluationOptions", "get_treatments_with_config_by_flag_set_and_evaluation_options", "treatments_with_config_by_flag_set_and_evaluation_options", "treatmentsWithConfigByFlagSetAndEvaluationOptions", "TreatmentsWithConfigByFlagSetAndEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithConfigByFlagSetAndEvaluationOptions { - t.Error("expected `TreatmentsWithConfigByFlagSetAndEvaluationOptions`. Got: ", v) - } - } - for _, method := range []string{"getTreatmentsWithConfigByFlagSetsAndEvaluationOptions", "get_treatments_with_config_by_flag_sets_and_evaluation_options", "treatments_with_config_by_flag_sets_and_evaluation_options", "treatmentsWithConfigByFlagSetsAndEvaluationOptions", "TreatmentsWithConfigByFlagSetsAndEvaluationOptions"} { - if v, ok := ParseMethodFromRedisHash(method); !ok || v != TreatmentsWithConfigByFlagSetsAndEvaluationOptions { - t.Error("expected `TreatmentsWithConfigByFlagSetsAndEvaluationOptions`. Got: ", v) - } - } for _, method := range []string{"track", "Track"} { if v, ok := ParseMethodFromRedisHash(method); !ok || v != Track { t.Error("expented `track`. Got: ", method) From 36cc589587df995b76e576418d34ad84af3e153f Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Thu, 11 Dec 2025 10:54:50 -0300 Subject: [PATCH 11/12] Updated single to map impressions --- synchronizer/worker/impression/single.go | 1 + synchronizer/worker/impression/single_test.go | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/synchronizer/worker/impression/single.go b/synchronizer/worker/impression/single.go index 12021f32..a63d7c12 100644 --- a/synchronizer/worker/impression/single.go +++ b/synchronizer/worker/impression/single.go @@ -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 { diff --git a/synchronizer/worker/impression/single_test.go b/synchronizer/worker/impression/single_test.go index fdea8c68..c4eb73b5 100644 --- a/synchronizer/worker/impression/single_test.go +++ b/synchronizer/worker/impression/single_test.go @@ -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) { @@ -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", } @@ -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", @@ -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") @@ -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", From 45fec27a01d0d61362866cb76b43375e84d32c2e Mon Sep 17 00:00:00 2001 From: Nadia Mayor Date: Wed, 17 Dec 2025 11:50:54 -0300 Subject: [PATCH 12/12] Updated changelogs --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 093a6d06..fa6d93cc 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,4 @@ -9.1.0 (Dec 5, 2025) +9.1.0 (Dec 17, 2025) - Added impression properties. 9.0.0 (Nov 21, 2025)