Skip to content

[FSSDK-11178] Update impression event for CMAB #404

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
170 changes: 134 additions & 36 deletions pkg/decision/cmab_client.go → pkg/cmab/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License. *
***************************************************************************/

// Package decision provides CMAB client implementation
package decision
// Package cmab provides CMAB client implementation
package cmab

import (
"bytes"
Expand All @@ -27,6 +27,9 @@ import (
"net/http"
"time"

"github.com/optimizely/go-sdk/v2/pkg/config"
"github.com/optimizely/go-sdk/v2/pkg/entities"
"github.com/optimizely/go-sdk/v2/pkg/event"
"github.com/optimizely/go-sdk/v2/pkg/logging"
)

Expand All @@ -44,34 +47,34 @@ const (
DefaultBackoffMultiplier = 2.0
)

// CMABAttribute represents an attribute in a CMAB request
type CMABAttribute struct {
// Attribute represents an attribute in a CMAB request
type Attribute struct {
ID string `json:"id"`
Value interface{} `json:"value"`
Type string `json:"type"`
}

// CMABInstance represents an instance in a CMAB request
type CMABInstance struct {
VisitorID string `json:"visitorId"`
ExperimentID string `json:"experimentId"`
Attributes []CMABAttribute `json:"attributes"`
CmabUUID string `json:"cmabUUID"`
// Instance represents an instance in a CMAB request
type Instance struct {
VisitorID string `json:"visitorId"`
ExperimentID string `json:"experimentId"`
Attributes []Attribute `json:"attributes"`
CmabUUID string `json:"cmabUUID"`
}

// CMABRequest represents a request to the CMAB API
type CMABRequest struct {
Instances []CMABInstance `json:"instances"`
// Request represents a request to the CMAB API
type Request struct {
Instances []Instance `json:"instances"`
}

// CMABPrediction represents a prediction in a CMAB response
type CMABPrediction struct {
// Prediction represents a prediction in a CMAB response
type Prediction struct {
VariationID string `json:"variation_id"`
}

// CMABResponse represents a response from the CMAB API
type CMABResponse struct {
Predictions []CMABPrediction `json:"predictions"`
// Response represents a response from the CMAB API
type Response struct {
Predictions []Prediction `json:"predictions"`
}

// RetryConfig defines configuration for retry behavior
Expand All @@ -88,20 +91,24 @@ type RetryConfig struct {

// DefaultCmabClient implements the CmabClient interface
type DefaultCmabClient struct {
httpClient *http.Client
retryConfig *RetryConfig
logger logging.OptimizelyLogProducer
httpClient *http.Client
retryConfig *RetryConfig
logger logging.OptimizelyLogProducer
eventProcessor event.Processor
projectConfig config.ProjectConfig
}

// CmabClientOptions defines options for creating a CMAB client
type CmabClientOptions struct {
HTTPClient *http.Client
RetryConfig *RetryConfig
Logger logging.OptimizelyLogProducer
// ClientOptions defines options for creating a CMAB client
type ClientOptions struct {
HTTPClient *http.Client
RetryConfig *RetryConfig
Logger logging.OptimizelyLogProducer
EventProcessor event.Processor
ProjectConfig config.ProjectConfig
}

// NewDefaultCmabClient creates a new instance of DefaultCmabClient
func NewDefaultCmabClient(options CmabClientOptions) *DefaultCmabClient {
func NewDefaultCmabClient(options ClientOptions) *DefaultCmabClient {
httpClient := options.HTTPClient
if httpClient == nil {
httpClient = &http.Client{
Expand All @@ -119,9 +126,11 @@ func NewDefaultCmabClient(options CmabClientOptions) *DefaultCmabClient {
}

return &DefaultCmabClient{
httpClient: httpClient,
retryConfig: retryConfig,
logger: logger,
httpClient: httpClient,
retryConfig: retryConfig,
logger: logger,
eventProcessor: options.EventProcessor,
projectConfig: options.ProjectConfig,
}
}

Expand All @@ -142,18 +151,18 @@ func (c *DefaultCmabClient) FetchDecision(
url := fmt.Sprintf(CMABPredictionEndpoint, ruleID)

// Convert attributes to CMAB format
cmabAttributes := make([]CMABAttribute, 0, len(attributes))
cmabAttributes := make([]Attribute, 0, len(attributes))
for key, value := range attributes {
cmabAttributes = append(cmabAttributes, CMABAttribute{
cmabAttributes = append(cmabAttributes, Attribute{
ID: key,
Value: value,
Type: "custom_attribute",
})
}

// Create the request body
requestBody := CMABRequest{
Instances: []CMABInstance{
requestBody := Request{
Instances: []Instance{
{
VisitorID: userID,
ExperimentID: ruleID,
Expand Down Expand Up @@ -248,7 +257,7 @@ func (c *DefaultCmabClient) doFetch(ctx context.Context, url string, bodyBytes [
}

// Parse response
var cmabResponse CMABResponse
var cmabResponse Response
if err := json.Unmarshal(respBody, &cmabResponse); err != nil {
return "", fmt.Errorf("failed to unmarshal CMAB response: %w", err)
}
Expand All @@ -263,6 +272,95 @@ func (c *DefaultCmabClient) doFetch(ctx context.Context, url string, bodyBytes [
}

// validateResponse validates the CMAB response
func (c *DefaultCmabClient) validateResponse(response CMABResponse) bool {
func (c *DefaultCmabClient) validateResponse(response Response) bool {
return len(response.Predictions) > 0 && response.Predictions[0].VariationID != ""
}

// EventProcessor is an interface for processing events
type EventProcessor interface {
Process(userEvent interface{}) bool
}

// LogImpression logs a CMAB impression event
func (c *DefaultCmabClient) LogImpression(
ctx context.Context,
eventProcessor EventProcessor,
projectConfig config.ProjectConfig,
ruleID string,
userID string,
variationKey string,
variationID string,
attributes map[string]interface{},
cmabUUID string,
) error {
// Instead of directly creating an event, we'll delegate this to the client
// that has access to the event package
return fmt.Errorf("CMAB impression logging not implemented")
Comment on lines +296 to +298
Copy link
Preview

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogImpression is currently unimplemented and always errors; implement the logic or remove this stub to avoid runtime failures.

Suggested change
// Instead of directly creating an event, we'll delegate this to the client
// that has access to the event package
return fmt.Errorf("CMAB impression logging not implemented")
// Get experiment from project config
experiment, err := projectConfig.GetExperimentByID(ruleID)
if err != nil {
return fmt.Errorf("error getting experiment: %w", err)
}
// Create variation object
variation := entities.Variation{
ID: variationID,
Key: variationKey,
}
// Create user context
userContext := entities.UserContext{
ID: userID,
Attributes: attributes,
}
// Look for associated feature flag (if any)
flagKey := ""
featureList := projectConfig.GetFeatureList()
for _, feature := range featureList {
for _, featureExperiment := range feature.FeatureExperiments {
if featureExperiment.ID == ruleID {
flagKey = feature.Key
break
}
}
if flagKey != "" {
break
}
}
// Create user event with CMAB impression
userEvent, shouldDispatch := event.CreateCMABImpressionUserEvent(
projectConfig,
experiment,
&variation,
userContext,
flagKey,
experiment.Key, // ruleKey
"experiment", // ruleType
true,
cmabUUID,
)
// Process the event if it should be dispatched
if shouldDispatch {
if !eventProcessor.Process(userEvent) {
return fmt.Errorf("failed to process CMAB impression event")
}
}
return nil

Copilot uses AI. Check for mistakes.

}

// TrackCMABDecision tracks a CMAB decision event
func (c *DefaultCmabClient) TrackCMABDecision(
ruleID string,
userID string,
variationID string,
variationKey string,
attributes map[string]interface{},
cmabUUID string,
) {
if c.eventProcessor == nil || c.projectConfig == nil {
c.logger.Debug("Event processor or project config not available, not tracking impression")
return
}

// Get experiment from project config
experiment, err := c.projectConfig.GetExperimentByID(ruleID)
if err != nil {
c.logger.Error("Error getting experiment", err)
return
}

// Create variation object
variation := entities.Variation{
ID: variationID,
Key: variationKey,
}

// Create user context
userContext := entities.UserContext{
ID: userID,
Attributes: attributes,
}

// Look for associated feature flag (if any)
flagKey := ""
featureList := c.projectConfig.GetFeatureList()
for _, feature := range featureList {
for _, featureExperiment := range feature.FeatureExperiments {
if featureExperiment.ID == ruleID {
flagKey = feature.Key
break
}
}
if flagKey != "" {
break
}
}

// Create user event with CMAB impression
userEvent, shouldDispatch := event.CreateCMABImpressionUserEvent(
c.projectConfig,
experiment,
&variation,
userContext,
flagKey,
experiment.Key, // ruleKey
"experiment", // ruleType
true,
cmabUUID,
)

// Process the event if it should be dispatched
if shouldDispatch {
c.eventProcessor.ProcessEvent(userEvent)
Copy link
Preview

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EventProcessor interface defines Process(...), not ProcessEvent; update the call or interface to match.

Suggested change
c.eventProcessor.ProcessEvent(userEvent)
c.eventProcessor.Process(userEvent)

Copilot uses AI. Check for mistakes.

}
}
Loading