Repository: Unleash/unleash-client-go
Pull request #191 (2025-03-05 09:39:15)

chore: update release steps
Adding an missing step to release steps.


Patch:
diff --git a/.github/workflows/build-prs.yml b/.github/workflows/build-prs.yml
index 69cb796..1ef7320 100644
--- a/.github/workflows/build-prs.yml
+++ b/.github/workflows/build-prs.yml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        version: [1.13, 1.15, 1.16, 1.17, 1.18, 1.19, "1.20", 1.21, 1.22, 1.23, 1.24] 
+        version: [1.21, 1.22, 1.23, 1.24]
     steps:
       - uses: actions/checkout@v2
         name: Checkout code
@@ -43,4 +43,4 @@ jobs:
     steps:
       - uses: shogo82148/actions-goveralls@v1
         with:
-          parallel-finished: true
\ No newline at end of file
+          parallel-finished: true
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dcf0e09..fe775ee 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        version: [1.13, 1.15, 1.16, 1.17, 1.18, 1.19, "1.20", 1.21, 1.22, 1.23, 1.24] 
+        version: [1.21, 1.22, 1.23, 1.24]
     steps:
       - uses: actions/checkout@v4
         name: Checkout code
@@ -31,8 +31,8 @@ jobs:
       - name: Run spec tests
         run: go test -v ./... -tags='norace'
       - name: Run all tests with race detection
-        timeout-minutes: 1
         run: go test -race -covermode atomic -coverprofile=profile.cov -v ./... -tags='!norace'
+        timeout-minutes: 1
       - name: Send coverage
         uses: shogo82148/actions-goveralls@v1
         with:
diff --git a/README.md b/README.md
index a15ece6..259564d 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Unleash Client for Go. Read more about the [Unleash project](https://github.com/
 
 ## Go Version
 
-The client is currently tested against Go 1.10.x and 1.13.x. These versions will be updated
+The client is currently tested against Go 1.21.x, 1.22.x, 1.23.x and 1.24.x. These versions will be updated
 as new versions of Go are released.
 
 The client may work on older versions of Go as well, but is not actively tested.
@@ -264,9 +264,34 @@ resolver := func(featureName string) *api.Feature {
 unleash.IsEnabled("someToggle", unleash.WithContext(ctx), unleash.WithResolver(resolver))
 ```
 
+### Impression data
+
+When impression data is enabled on a flag, the SDK will emit impression events during evaluation. You can hook into these with a custom listener to collect insights or integrate with analytics. You can [read more about impression data in Unleash's documentation](https://docs.getunleash.io/reference/impression-data).
+
+```go
+type MyListener struct {
+	unleash.DebugListener
+}
+
+func (l *MyListener) OnImpression(e unleash.ImpressionEvent) {
+	fmt.Printf("Custom Impression: %s = %v (%s)\n", e.FeatureName, e.Enabled, e.EventType)
+	if e.Context != nil {
+		fmt.Printf("Context: userId=%s sessionId=%s\n", e.Context.UserId, e.Context.SessionId)
+	}
+}
+
+func main() {
+	unleash.Initialize(
+		unleash.WithListener(&MyListener{}),
+		unleash.WithAppName("my-app"),
+		unleash.WithUrl("http://unleash.herokuapp.com/api/"),
+	)
+}
+```
+
 ## Development
 
-To override dependency on unleash-client-go github repository to a local development folder (for instance when building a local test-app for the SDK),  
+To override dependency on unleash-client-go github repository to a local development folder (for instance when building a local test-app for the SDK),
 you can add the following to your apps `go.mod`:
 
 ```mod
@@ -279,6 +304,7 @@ you can add the following to your apps `go.mod`:
 
 - Update the clientVersion in `client.go`
 - Tag the repository with the new tag
+- Create the release manually in Github
 
 ## Adding client specifications
 
diff --git a/client.go b/client.go
index 7ad7f57..1d22528 100644
--- a/client.go
+++ b/client.go
@@ -51,6 +51,7 @@ type Client struct {
 	errorListener      ErrorListener
 	metricsListener    MetricListener
 	repositoryListener RepositoryListener
+	impressionListener ImpressionListener
 	ready              chan bool
 	onReady            chan struct{}
 	close              chan struct{}
@@ -58,6 +59,7 @@ type Client struct {
 	count              chan metric
 	sent               chan MetricsData
 	registered         chan ClientData
+	impression         chan ImpressionEvent
 	staticContext      *context.Context
 }
 
@@ -109,6 +111,7 @@ func NewClient(options ...ConfigOption) (*Client, error) {
 		count:         make(chan metric),
 		sent:          make(chan MetricsData),
 		registered:    make(chan ClientData, 1),
+		impression:    make(chan ImpressionEvent, 10),
 		close:         make(chan struct{}),
 		closed:        make(chan struct{}),
 	}
@@ -135,6 +138,10 @@ func NewClient(options ...ConfigOption) (*Client, error) {
 	if mListener, ok := uc.options.listener.(MetricListener); ok {
 		uc.metricsListener = mListener
 	}
+	if iListener, ok := uc.options.listener.(ImpressionListener); ok {
+		uc.impressionListener = iListener
+	}
+
 	defer func() {
 		go uc.sync()
 	}()
@@ -251,6 +258,10 @@ func (uc *Client) sync() {
 			if uc.metricsListener != nil {
 				uc.metricsListener.OnRegistered(cd)
 			}
+		case ie := <-uc.impression:
+			if uc.impressionListener != nil {
+				uc.impressionListener.OnImpression(ie)
+			}
 		case <-uc.close:
 			close(uc.closed)
 			return
@@ -262,12 +273,32 @@ func (uc *Client) sync() {
 //
 // It is safe to call this method from multiple goroutines concurrently.
 func (uc *Client) IsEnabled(feature string, options ...FeatureOption) (enabled bool) {
+	result, f := uc.isEnabled(feature, options...)
+	enabled = result.Enabled
+
 	defer func() {
 		uc.metrics.count(feature, enabled)
+
+		if f != nil && f.ImpressionData && uc.impressionListener != nil {
+			var opts featureOption
+			for _, o := range options {
+				o(&opts)
+			}
+			ctx := uc.staticContext
+			if opts.ctx != nil {
+				ctx = ctx.Override(*opts.ctx)
+			}
+
+			uc.impression <- ImpressionEvent{
+				FeatureName: feature,
+				EventType:   ImpressionEventTypeIsEnabled,
+				Enabled:     enabled,
+				Context:     ctx,
+			}
+		}
 	}()
 
-	result, _ := uc.isEnabled(feature, options...)
-	return result.Enabled
+	return
 }
 
 // isEnabled abstracts away the details of checking if a toggle is turned on or off
@@ -400,12 +431,33 @@ func (uc *Client) isParentDependencySatisfied(feature *api.Feature, context cont
 // GetVariant queries a variant as the specified feature is enabled.
 //
 // It is safe to call this method from multiple goroutines concurrently.
-func (uc *Client) GetVariant(feature string, options ...VariantOption) *api.Variant {
-	variant := uc.getVariantWithoutMetrics(feature, options...)
+func (uc *Client) GetVariant(feature string, options ...VariantOption) (variant *api.Variant) {
+	variant = uc.getVariantWithoutMetrics(feature, options...)
+
 	defer func() {
 		uc.metrics.countVariants(feature, variant.FeatureEnabled, variant.Name)
+		
+		f := uc.repository.getToggle(feature)
+		if f != nil && f.ImpressionData && uc.impressionListener != nil {
+			var opts variantOption
+			for _, o := range options {
+				o(&opts)
+			}
+			ctx := uc.staticContext
+			if opts.ctx != nil {
+				ctx = ctx.Override(*opts.ctx)
+			}
+
+			uc.impression <- ImpressionEvent{
+				FeatureName: feature,
+				EventType:   ImpressionEventTypeGetVariant,
+				Enabled:     variant.FeatureEnabled,
+				Variant:     variant.Name,
+				Context:     ctx,
+			}
+		}
 	}()
-	return variant
+	return
 }
 
 // getVariantWithoutMetrics abstracts away the logic for resolving a variant without metrics
@@ -504,6 +556,11 @@ func (uc *Client) Registered() <-chan ClientData {
 	return uc.registered
 }
 
+// Impression returns the impression channel which gives an update when a feature with impression data has been evaluated.
+func (uc *Client) Impression() <-chan ImpressionEvent {
+	return uc.impression
+}
+
 // Sent returns the sent channel which receives data whenever the client has successfully sent metrics to
 // the metrics service.
 func (uc *Client) Sent() <-chan MetricsData {
diff --git a/debuglistener.go b/debuglistener.go
index 79526a9..f4a0f21 100644
--- a/debuglistener.go
+++ b/debuglistener.go
@@ -38,3 +38,8 @@ func (l DebugListener) OnSent(payload MetricsData) {
 func (l DebugListener) OnRegistered(payload ClientData) {
 	fmt.Printf("Registered: %+v\n", payload)
 }
+
+// OnImpression prints to the console for every user impression (IsEnabled / GetVariant)
+func (l DebugListener) OnImpression(event ImpressionEvent) {
+	fmt.Printf("Impression: %+v\n", event)
+}
diff --git a/go.mod b/go.mod
index 6c6deb3..abf58e7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,14 +1,19 @@
 module github.com/Unleash/unleash-client-go/v4
 
+go 1.21
+
 require (
-	github.com/Masterminds/semver/v3 v3.1.1
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.3.1
 	github.com/h2non/gock v1.2.0
 	github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/stretchr/objx v0.1.1 // indirect
-	github.com/stretchr/testify v1.2.2
+	github.com/stretchr/testify v1.10.0
 	github.com/twmb/murmur3 v1.1.8
 )
 
-go 1.13
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/objx v0.5.2 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
index f593884..8193bf3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
-github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
+github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
@@ -10,9 +10,13 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 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/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
 github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/impression.go b/impression.go
new file mode 100644
index 0000000..e69c262
--- /dev/null
+++ b/impression.go
@@ -0,0 +1,31 @@
+package unleash
+
+import (
+	"github.com/Unleash/unleash-client-go/v4/context"
+)
+
+type EventType string
+
+const (
+	ImpressionEventTypeIsEnabled EventType = "IsEnabled"
+	ImpressionEventTypeGetVariant EventType = "GetVariant"
+)
+
+// ImpressionEvent represents an event that occurs when a feature with impression data is evaluated.
+type ImpressionEvent struct {
+	// FeatureName is the name of the feature toggle that was evaluated.
+	FeatureName string
+
+	// EventType is the type of event that occurred (i.e., "IsEnabled" or "GetVariant").
+	EventType EventType
+
+	// Enabled indicates whether the feature was enabled or not.
+	Enabled bool
+
+	// Variant is the name of the variant that was resolved, if any.
+	Variant string
+
+	// Context is the evaluation context used.
+	Context *context.Context
+}
+
diff --git a/nooplistener.go b/nooplistener.go
index 64cf432..1c54a28 100644
--- a/nooplistener.go
+++ b/nooplistener.go
@@ -26,3 +26,7 @@ func (l NoopListener) OnSent(payload MetricsData) {
 // The client has registered.
 func (l NoopListener) OnRegistered(payload ClientData) {
 }
+
+// User impression (IsEnabled / GetVariant)
+func (l NoopListener) OnImpression(event ImpressionEvent) {
+}
diff --git a/unleash.go b/unleash.go
index c4c36eb..d0635c2 100644
--- a/unleash.go
+++ b/unleash.go
@@ -35,6 +35,13 @@ type RepositoryListener interface {
 	OnReady()
 }
 
+// ImpressionListener defines an interface that can be implemented in order to receive
+// impression events when a feature with impression data is evaluated.
+type ImpressionListener interface {
+	// OnImpression is called whenever a feature with impression data is evaluated.
+	OnImpression(ImpressionEvent)
+}
+
 // IsEnabled queries the default client whether or not the specified feature is enabled or not.
 func IsEnabled(feature string, options ...FeatureOption) bool {
 	if defaultClient == nil {
diff --git a/unleash_mock.go b/unleash_mock.go
index 76be4de..2d440e3 100644
--- a/unleash_mock.go
+++ b/unleash_mock.go
@@ -41,6 +41,10 @@ func (l *MockedListener) OnRegistered(payload ClientData) {
 	l.Called(payload)
 }
 
+func (l *MockedListener) OnImpression(event ImpressionEvent) {
+	l.Called(event)
+}
+
 func writeJSON(rw http.ResponseWriter, x interface{}) {
 	enc := json.NewEncoder(rw)
 	if err := enc.Encode(x); err != nil {


Test patch:
diff --git a/api/variant_test.go b/api/variant_test.go
index e834f70..ce669d1 100644
--- a/api/variant_test.go
+++ b/api/variant_test.go
@@ -331,3 +331,13 @@ func (suite *VariantTestSuite) TestGetVariant_OverrideOnEnvironment() {
 	suite.Equal(true, variantSetup.Enabled, "Should be equal")
 	suite.Equal(expectedPayload, variantSetup.Payload, "Should be equal")
 }
+
+func TestGetDefaultVariant(t *testing.T) {
+	defaultVariant := GetDefaultVariant()
+	if defaultVariant.Name != "disabled" {
+		t.Errorf("Expected default variant name to be 'disabled', got '%s'", defaultVariant.Name)
+	}
+	if defaultVariant.Enabled {
+		t.Error("Expected default variant to be disabled")
+	}
+}
diff --git a/benchmark_test.go b/benchmark_test.go
index 672b04d..da8f425 100644
--- a/benchmark_test.go
+++ b/benchmark_test.go
@@ -9,18 +9,9 @@ import (
 	"github.com/Unleash/unleash-client-go/v4"
 )
 
-type NoOpListener struct{}
-
-func (l *NoOpListener) OnReady()                                {}
-func (l *NoOpListener) OnError(err error)                       {}
-func (l *NoOpListener) OnWarning(warning error)                 {}
-func (l *NoOpListener) OnCount(name string, enabled bool)       {}
-func (l *NoOpListener) OnSent(payload unleash.MetricsData)      {}
-func (l *NoOpListener) OnRegistered(payload unleash.ClientData) {}
-
 func BenchmarkFeatureToggleEvaluation(b *testing.B) {
 	unleash.Initialize(
-		unleash.WithListener(&NoOpListener{}),
+		unleash.WithListener(&unleash.NoopListener{}),
 		unleash.WithAppName("go-benchmark"),
 		unleash.WithUrl("https://app.unleash-hosted.com/demo/api/"),
 		unleash.WithCustomHeaders(http.Header{"Authorization": {"Go-Benchmark:development.be6b5d318c8e77469efb58590022bb6416100261accf95a15046c04d"}}),
diff --git a/example_with_instance_test.go b/example_with_instance_test.go
index b75be77..1b114dc 100644
--- a/example_with_instance_test.go
+++ b/example_with_instance_test.go
@@ -24,6 +24,8 @@ func Sync(client *unleash.Client) {
 			fmt.Printf("SENT: %+v\n", md)
 		case cd := <-client.Registered():
 			fmt.Printf("REGISTERED: %+v\n", cd)
+		case ie := <-client.Impression():
+			fmt.Printf("IMPRESSION: %+v\n", ie)
 		case <-timer.C:
 			fmt.Printf("ISENABLED: %v\n", client.IsEnabled("eid.enabled"))
 			timer.Reset(1 * time.Second)
diff --git a/impression_test.go b/impression_test.go
new file mode 100644
index 0000000..addde6f
--- /dev/null
+++ b/impression_test.go
@@ -0,0 +1,442 @@
+package unleash
+
+import (
+	"sync"
+	"testing"
+
+	"github.com/Unleash/unleash-client-go/v4/api"
+	"github.com/Unleash/unleash-client-go/v4/context"
+	"github.com/h2non/gock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func setupClient(t *testing.T, listener *MockedListener) *Client {
+	client, err := NewClient(
+		WithUrl(mockerServer),
+		WithAppName(mockAppName),
+		WithInstanceId(mockInstanceId),
+		WithListener(listener),
+	)
+	assert.NoError(t, err)
+	client.WaitForReady()
+	return client
+}
+
+func TestImpression_Off(t *testing.T) {
+	defer gock.OffAll()
+	assert := assert.New(t)
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	feature := "impression-data-off"
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:    feature,
+					Enabled: true,
+					Strategies: []api.Strategy{
+						{
+							Id:   1,
+							Name: "flexibleRollout",
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+						},
+					},
+				},
+			},
+		})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+	mockListener.On("OnCount", feature, true).Return()
+	mockListener.On("OnImpression", mock.Anything).Maybe()
+
+	client := setupClient(t, mockListener)
+	client.IsEnabled(feature)
+
+	assert.NoError(client.Close())
+	mockListener.AssertNotCalled(t, "OnImpression", mock.Anything)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
+
+func TestImpression_IsEnabled(t *testing.T) {
+	defer gock.OffAll()
+	assert := assert.New(t)
+	var wg sync.WaitGroup
+	wg.Add(1)
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	feature := "impression-data-on"
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:           feature,
+					Enabled:        true,
+					ImpressionData: true,
+					Strategies: []api.Strategy{
+						{
+							Id:   1,
+							Name: "flexibleRollout",
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+						},
+					},
+				},
+			},
+		})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+	mockListener.On("OnCount", feature, true).Return()
+
+	mockListener.On("OnImpression", mock.MatchedBy(func(e ImpressionEvent) bool {
+		return e.FeatureName == feature &&
+			e.EventType == ImpressionEventTypeIsEnabled &&
+			e.Enabled == true
+	})).Run(func(args mock.Arguments) {
+		wg.Done()
+	}).Once()
+
+	client := setupClient(t, mockListener)
+	client.IsEnabled(feature)
+
+	wg.Wait()
+	assert.NoError(client.Close())
+	mockListener.AssertExpectations(t)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
+
+func TestImpression_GetVariant(t *testing.T) {
+	defer gock.OffAll()
+	assert := assert.New(t)
+	var wg sync.WaitGroup
+	wg.Add(1)
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	feature := "impression-data-on-variant"
+	variant := "variant-for-impression-data"
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:           feature,
+					Enabled:        true,
+					ImpressionData: true,
+					Strategies: []api.Strategy{
+						{
+							Id:   1,
+							Name: "flexibleRollout",
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+								"groupId":    variant,
+							},
+							Variants: []api.VariantInternal{
+								{
+									Variant: api.Variant{
+										Name: variant,
+										Payload: api.Payload{
+											Type:  "string",
+											Value: "strategyVariantValue",
+										},
+									},
+									Weight: 1000,
+								},
+							},
+						},
+					},
+				},
+			},
+		})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+	mockListener.On("OnCount", feature, true).Return()
+
+	mockListener.On("OnImpression", mock.MatchedBy(func(e ImpressionEvent) bool {
+		return e.FeatureName == feature &&
+			e.EventType == ImpressionEventTypeGetVariant &&
+			e.Enabled == true &&
+			e.Variant == variant
+	})).Run(func(args mock.Arguments) {
+		wg.Done()
+	}).Once()
+
+	client := setupClient(t, mockListener)
+	client.GetVariant(feature)
+
+	wg.Wait()
+	assert.NoError(client.Close())
+	mockListener.AssertExpectations(t)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
+
+func TestImpression_WithContext(t *testing.T) {
+	defer gock.OffAll()
+	assert := assert.New(t)
+	var wg sync.WaitGroup
+	wg.Add(1)
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	feature := "with-context"
+	ctxUserId := "123"
+	ctxSessionId := "abc"
+	ctxPropertyId := "impression-data-ctx-id"
+	ctxPropertyValue := "impression-data-ctx-value"
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:           feature,
+					Enabled:        true,
+					ImpressionData: true,
+					Strategies: []api.Strategy{
+						{
+							Id:   1,
+							Name: "flexibleRollout",
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+							Constraints: []api.Constraint{
+								{
+									ContextName: ctxPropertyId,
+									Operator:    api.OperatorIn,
+									Values:      []string{ctxPropertyValue},
+								},
+							},
+						},
+					},
+				},
+			},
+		})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+	mockListener.On("OnCount", feature, true).Return()
+
+	userCtx := context.Context{
+		UserId:    ctxUserId,
+		SessionId: ctxSessionId,
+		Properties: map[string]string{
+			ctxPropertyId: ctxPropertyValue,
+		},
+	}
+
+	mockListener.On("OnImpression", mock.MatchedBy(func(e ImpressionEvent) bool {
+		return e.FeatureName == feature &&
+			e.EventType == ImpressionEventTypeIsEnabled &&
+			e.Enabled == true &&
+			e.Context != nil &&
+			e.Context.UserId == ctxUserId &&
+			e.Context.SessionId == ctxSessionId &&
+			e.Context.Properties[ctxPropertyId] == ctxPropertyValue
+	})).Run(func(args mock.Arguments) {
+		wg.Done()
+	}).Once()
+
+	client := setupClient(t, mockListener)
+	client.IsEnabled(feature, WithContext(userCtx))
+
+	wg.Wait()
+	assert.NoError(client.Close())
+	mockListener.AssertExpectations(t)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
+
+func TestImpression_WithContextAndMultipleEvents(t *testing.T) {
+	defer gock.OffAll()
+	assert := assert.New(t)
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	feature := "context-multiple-evals"
+	ctxUserId := "123"
+	ctxSessionId := "abc"
+	ctxPropertyId := "ctx-key"
+	ctxPropertyValue := "ctx-val"
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:           feature,
+					Enabled:        true,
+					ImpressionData: true,
+					Strategies: []api.Strategy{
+						{
+							Id:   1,
+							Name: "flexibleRollout",
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+							Constraints: []api.Constraint{
+								{
+									ContextName: ctxPropertyId,
+									Operator:    api.OperatorIn,
+									Values:      []string{ctxPropertyValue},
+								},
+							},
+						},
+					},
+				},
+			},
+		})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+	mockListener.On("OnCount", feature, true).Maybe()
+	mockListener.On("OnCount", feature, false).Maybe()
+
+	userCtx := context.Context{
+		UserId:    ctxUserId,
+		SessionId: ctxSessionId,
+		Properties: map[string]string{
+			ctxPropertyId: ctxPropertyValue,
+		},
+	}
+
+	mockListener.On("OnImpression", mock.MatchedBy(func(e ImpressionEvent) bool {
+		return e.FeatureName == feature &&
+			e.EventType == ImpressionEventTypeIsEnabled &&
+			e.Enabled == true &&
+			e.Context != nil &&
+			e.Context.UserId == ctxUserId &&
+			e.Context.SessionId == ctxSessionId &&
+			e.Context.Properties[ctxPropertyId] == ctxPropertyValue
+	})).Run(func(args mock.Arguments) {
+		wg.Done()
+	}).Once()
+
+	mockListener.On("OnImpression", mock.MatchedBy(func(e ImpressionEvent) bool {
+		return e.FeatureName == feature &&
+			e.EventType == ImpressionEventTypeIsEnabled &&
+			e.Enabled == false &&
+			len(e.Context.Properties) == 0
+	})).Run(func(args mock.Arguments) {
+		wg.Done()
+	}).Once()
+
+	client := setupClient(t, mockListener)
+
+	resultWithCtx := client.IsEnabled(feature, WithContext(userCtx))
+	assert.True(resultWithCtx)
+
+	resultWithoutCtx := client.IsEnabled(feature)
+	assert.False(resultWithoutCtx)
+
+	wg.Wait()
+	assert.NoError(client.Close())
+	mockListener.AssertExpectations(t)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}
+
+func TestImpression_GetChannelMethod(t *testing.T) {
+	defer gock.OffAll()
+	assert := assert.New(t)
+	var wg sync.WaitGroup
+	wg.Add(1)
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		Reply(200)
+
+	feature := "impression-get-channel-method-test"
+
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:           feature,
+					Enabled:        true,
+					ImpressionData: true,
+					Strategies: []api.Strategy{
+						{
+							Id:   1,
+							Name: "flexibleRollout",
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+						},
+					},
+				},
+			},
+		})
+
+	mockListener := &MockedListener{}
+	mockListener.On("OnReady").Return()
+	mockListener.On("OnRegistered", mock.AnythingOfType("ClientData"))
+	mockListener.On("OnCount", feature, true).Return()
+	mockListener.On("OnImpression", mock.AnythingOfType("ImpressionEvent")).Maybe()
+	mockListener.On("OnError", mock.Anything).Maybe()
+
+	client := setupClient(t, mockListener)
+	impressionChannel := client.Impression()
+
+	ready := make(chan struct{})
+
+	go func() {
+		close(ready)
+		for impression := range impressionChannel {
+			if impression.FeatureName == feature &&
+				impression.EventType == ImpressionEventTypeIsEnabled &&
+				impression.Enabled {
+				wg.Done()
+				return
+			}
+		}
+	}()
+
+	<-ready
+	client.IsEnabled(feature)
+
+	wg.Wait()
+	assert.NoError(client.Close())
+	mockListener.AssertExpectations(t)
+	assert.True(gock.IsDone(), "there should be no more mocks")
+}