diff --git a/docs/sdks/server-sdks/go/_category_.json b/docs/sdks/server-sdks/go/_category_.json new file mode 100644 index 00000000..a1c419e4 --- /dev/null +++ b/docs/sdks/server-sdks/go/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Go", + "position": 4 +} \ No newline at end of file diff --git a/docs/sdks/server-sdks/go/assignments.mdx b/docs/sdks/server-sdks/go/assignments.mdx new file mode 100644 index 00000000..9dddc18e --- /dev/null +++ b/docs/sdks/server-sdks/go/assignments.mdx @@ -0,0 +1,331 @@ +--- +title: Assignments +sidebar_position: 4 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import ApiOptionRef from '@site/src/components/ApiOptionRef'; + +Assignments are the mechanism through which a given [Subject](/sdks/sdk-features/subjects) is assigned to a variation for a feature flag or experiment. + +The Eppo SDK supports the following assignment types: +- String +- Boolean +- JSON +- Integer +- Float + +## Assignment Types + +### String Assignments + +String assignments return a string value that is set as the variation. String flags are the most common type of flags and are useful for both A/B/n tests and advanced targeting use cases. + +```go +import ( + "github.com/eppo-exp/golang-sdk/v6/eppoclient" +) + +subjectAttributes := map[string]interface{}{ + "country": user.Country, + "age": 30, +} + +variation, err := client.GetStringAssignment( + "flag-key-123", + user.ID, + subjectAttributes, + "control", +) +if err != nil { + log.Printf("Error getting assignment: %v", err) + return +} + +switch variation { +case "version-a": + handleVersionA() +case "version-b": + handleVersionB() +default: + handleControl() +} +``` + +### Boolean Assignments + +Boolean flags support simple on/off toggles. They're useful for simple, binary feature switches like blue/green deployments or enabling/disabling a new feature. + +```go +enabled, err := client.GetBooleanAssignment( + "new-feature", + user.ID, + subjectAttributes, + false, // default value +) +if err != nil { + log.Printf("Error getting assignment: %v", err) + return +} + +if enabled { + handleFeatureEnabled() +} else { + handleFeatureDisabled() +} +``` + +### JSON Assignments + +JSON flags work best for advanced configuration use cases. The JSON flag can include structured information such as: +- Marketing copy for a promotional campaign +- Configuration parameters for a feature +- UI customization settings + +```go +defaultCampaign := map[string]interface{}{ + "hero": false, + "hero_image": "placeholder.png", + "hero_title": "Placeholder Hero Title", + "hero_description": "Placeholder Hero Description", +} + +campaignConfig, err := client.GetJSONAssignment( + "campaign-config", + user.ID, + subjectAttributes, + defaultCampaign, +) +if err != nil { + log.Printf("Error getting assignment: %v", err) + return +} + +// You can also get the raw JSON bytes +campaignBytes, err := client.GetJSONBytesAssignment( + "campaign-config", + user.ID, + subjectAttributes, + defaultCampaignBytes, +) +``` + +### Numeric Assignments + +The SDK provides both integer and floating-point numeric assignments. These are useful for testing different numeric values like: +- Price points +- Number of items to display +- Timeout durations + +```go +// Integer assignment +numItems, err := client.GetIntegerAssignment( + "items-to-show", + user.ID, + subjectAttributes, + 10, // default value +) + +// Float assignment +price, err := client.GetNumericAssignment( + "price-test", + user.ID, + subjectAttributes, + 9.99, // default value +) +``` + +## Assignment Logging + +### Assignment Logger Schema + +The SDK will invoke the `LogAssignment` method with an `AssignmentEvent` struct that contains the following fields: + + + +The time when the subject was assigned to the variation in ISO format. Example: `"2021-06-22T17:35:12.000Z"` + + + + +An Eppo feature flag key. Example: `"recommendation-algo"` + + + + +An Eppo allocation key. Example: `"allocation-17"` + + + + +An Eppo experiment key. Example: `"recommendation-algo-allocation-17"` + + + + +An identifier of the subject or user assigned to the experiment variation. Example: UUID + + + + +A map of metadata about the subject. These attributes are only logged if passed to the SDK assignment function. Example: `{"country": "US"}` + + + + +The experiment variation the subject was assigned to. Example: `"control"` + + +### Logging to Your Data Warehouse + +Eppo's architecture ensures that raw user data never leaves your system. Instead of pushing subject-level exposure events to Eppo's servers, Eppo's SDKs integrate with your existing logging system. + +Here are examples of implementing the `IAssignmentLogger` interface for different logging systems: + + + + +```go +type ConsoleLogger struct{} + +func (l *ConsoleLogger) LogAssignment(event eppoclient.AssignmentEvent) error { + log.Printf("Assignment: %+v", event) + return nil +} +``` + + + + +```go +import "gopkg.in/segmentio/analytics-go.v3" + +type SegmentLogger struct { + client analytics.Client +} + +func (l *SegmentLogger) LogAssignment(event eppoclient.AssignmentEvent) error { + return l.client.Enqueue(analytics.Track{ + UserId: event.Subject, + Event: "Eppo Randomization Event", + Properties: analytics.Properties(event), + }) +} +``` + + + + +```go +import "github.com/snowplow/snowplow-golang-tracker/v2/tracker" + +type SnowplowLogger struct { + tracker *tracker.Tracker +} + +func (l *SnowplowLogger) LogAssignment(event eppoclient.AssignmentEvent) error { + return l.tracker.TrackSelfDescribingEvent(tracker.SelfDescribingEvent{ + Schema: "iglu:com.example_company/eppo-event/jsonschema/1-0-2", + Data: map[string]interface{}{ + "userId": event.Subject, + "properties": event, + }, + }) +} +``` + + + + +### Deduplicating Logs + +To prevent duplicate assignment events, you can implement caching in your logger: + +```go +type CachingLogger struct { + wrapped eppoclient.IAssignmentLogger + cache *lru.Cache +} + +func NewCachingLogger(wrapped eppoclient.IAssignmentLogger) *CachingLogger { + cache, _ := lru.New(1024) + return &CachingLogger{ + wrapped: wrapped, + cache: cache, + } +} + +func (l *CachingLogger) LogAssignment(event eppoclient.AssignmentEvent) error { + cacheKey := fmt.Sprintf("%s-%s", event.Subject, event.FeatureFlag) + if _, exists := l.cache.Get(cacheKey); exists { + return nil + } + + err := l.wrapped.LogAssignment(event) + if err == nil { + l.cache.Add(cacheKey, true) + } + return err +} +``` + +## Debugging Assignments + +The SDK provides detailed assignment information to help debug why a specific variation was chosen: + +```go +evaluation, err := client.GetBooleanAssignmentDetails( + "kill-switch", + "test-subject", + map[string]interface{}{ + "country": "UK", + "age": 62, + }, + false, +) +if err != nil { + log.Printf("Error getting assignment details: %v", err) + return +} + +log.Printf("Assignment: %s", evaluation.Variation) +log.Printf("Details: %+v", evaluation.EvaluationDetails) +``` + +The evaluation details include: +- Flag and subject information +- Timestamp and configuration metadata +- Allocation evaluation results +- Rule matching details +- Split calculations + +For more information on debugging assignments, see [Debugging Flag Assignments](/sdks/sdk-features/debugging-flag-assignment/). \ No newline at end of file diff --git a/docs/sdks/server-sdks/go/bandits.mdx b/docs/sdks/server-sdks/go/bandits.mdx new file mode 100644 index 00000000..9b4ce298 --- /dev/null +++ b/docs/sdks/server-sdks/go/bandits.mdx @@ -0,0 +1,235 @@ +--- +title: Contextual Bandits +sidebar_position: 5 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import ApiOptionRef from '@site/src/components/ApiOptionRef'; + +Eppo's Go SDK supports contextual multi-armed bandits, which dynamically optimize assignments based on user context. A bandit balances exploration of new actions with exploitation of known successful actions to maximize a specified metric. + +## Bandit Setup + +To leverage Eppo's contextual bandits using the Go SDK, there are two additional steps over regular feature flags: +1. Add a bandit action logger to the assignment logger +2. Query the bandit for an action + +### Logging Bandit Actions + +In order for the bandit to learn an optimized policy, we need to capture and log the bandit's actions. +This requires implementing the `LogBanditAction` method in your logger: + +```go +type MyLogger struct{} + +func (l *MyLogger) LogAssignment(assignment eppoclient.AssignmentEvent) error { + // Your assignment logging implementation + return nil +} + +func (l *MyLogger) LogBanditAction(banditAction eppoclient.BanditAction) error { + // Your bandit action logging implementation + return nil +} +``` + +### Bandit Action Schema + +The SDK will invoke the `LogBanditAction` method with a `BanditAction` struct containing the following fields: + + + +The time when the action is taken in UTC. Example: `"2024-03-22T14:26:55.000Z"` + + + + +The key of the feature flag corresponding to the bandit. Example: `"bandit-test-allocation-4"` + + + + +The key (unique identifier) of the bandit. Example: `"ad-bandit-1"` + + + + +An identifier of the subject or user assigned to the experiment variation. Example: `"ed6f85019080"` + + + + +Metadata about numeric attributes of the subject. Example: `{"age": 30}` + + + + +Metadata about non-numeric attributes of the subject. Example: `{"loyalty_tier": "gold"}` + + + + +The action assigned by the bandit. Example: `"promo-20%-off"` + + + + +Metadata about numeric attributes of the assigned action. Example: `{"discount": 0.2}` + + + + +Metadata about non-numeric attributes of the assigned action. Example: `{"promoTextColor": "white"}` + + + + +The weight between 0 and 1 the bandit valued the assigned action. Example: `0.25` + + + + +Unique identifier for the version of the bandit parameters used. Example: `"v123"` + + +## Querying the Bandit + +To query the bandit for an action, use the `GetBanditAction` method: + +```go +attributes := eppoclient.Attributes{ + NumericAttributes: map[string]float64{ + "age": float64(user.Age), + }, + CategoricalAttributes: map[string]string{ + "country": user.Country, + }, +} + +actions := map[string]eppoclient.Attributes{ + "nike": { + NumericAttributes: map[string]float64{ + "brand_affinity": 2.3, + }, + CategoricalAttributes: map[string]string{ + "previously_purchased": "true", + }, + }, + "adidas": { + NumericAttributes: map[string]float64{ + "brand_affinity": 0.2, + }, + CategoricalAttributes: map[string]string{ + "previously_purchased": "false", + }, + }, +} + +result, err := client.GetBanditAction( + "shoe-bandit", + user.ID, + attributes, + actions, + "control", +) +if err != nil { + log.Printf("Error getting bandit action: %v", err) + return +} + +if result.Action != "" { + showShoeAd(result.Action) +} else { + showDefaultAd() +} +``` + +### Subject Context + +The subject context contains contextual information about the subject that is independent of bandit actions. +For example, the subject's age or country. + +The subject context has type `Attributes` which has two fields: +- `NumericAttributes` (map[string]float64): A map of numeric attributes (such as "age") +- `CategoricalAttributes` (map[string]string): A map of categorical attributes (such as "country") + +:::note +The `CategoricalAttributes` are also used for targeting rules for the feature flag similar to how `subject_attributes` are used with regular feature flags. +::: + +### Action Contexts + +Next, supply a map with actions and their attributes: `actions: map[string]ContextAttributes`. +If the user is assigned to the bandit, the bandit selects one of the actions supplied here. +All actions supplied are considered to be valid; if an action should not be shown to a user, do not include it in this map. + +The action attributes are similar to the `subject_attributes` but hold action-specific information. +You can use `ContextAttributes{}` to create an empty attribute context. + +Note that action contexts can contain two kinds of information: +- Action-specific context: e.g., the image aspect ratio of the image corresponding to this action +- User-action interaction context: e.g., there could be a "brand-affinity" model that computes brand affinities of users to brands, and scores of this model can be added to the action context to provide additional context for the bandit. + +### Result + +The `result` is an instance of `BanditResult`, which has two fields: +- `Variation` (string): The variation that was assigned to the subject +- `Action` (string): The action that was assigned to the subject (empty string if no action was assigned) + +The variation returns the feature flag variation, this can be the bandit itself, or the "status quo" variation if the user is not assigned to the bandit. +If we are unable to generate a variation, for example when the flag is turned off, then the `default` variation is returned. +In both of those cases, the `Action` is an empty string, and you should use the status-quo algorithm to select an action. + +When `Action` is not empty, the bandit has selected that action to be shown to the user. + +### Status Quo Algorithm + +In order to accurately measure the performance of the bandit, we need to compare it to the status quo algorithm using an experiment. +This status quo algorithm could be a complicated algorithm that selects an action according to a different model, or a simple baseline such as selecting a fixed or random action. + +When you create an analysis allocation for the bandit and the `Action` in `BanditResult` is empty, implement the desired status quo algorithm based on the `Variation` value. diff --git a/docs/sdks/server-sdks/go/initialization.mdx b/docs/sdks/server-sdks/go/initialization.mdx new file mode 100644 index 00000000..96fb1c96 --- /dev/null +++ b/docs/sdks/server-sdks/go/initialization.mdx @@ -0,0 +1,204 @@ +--- +title: Initialization +sidebar_position: 3 +--- + +import ApiOptionRef from '@site/src/components/ApiOptionRef'; + +The Eppo Go SDK is easy to initialize while offering robust customization options, making it adaptable to various use cases such as offline mode, custom caching requirements, and ultra-low-latency initialization. + +## Initialize the SDK + +To complete basic initialization, you only need to provide an SDK key. [Create an SDK key](/sdks/sdk-keys) if you don't already have one. + +```go +import ( + "github.com/eppo-exp/golang-sdk/v6/eppoclient" +) + +config := eppoclient.Config{ + SdkKey: "", +} +client, err := eppoclient.InitClient(config) +if err != nil { + log.Fatal("Failed to initialize Eppo client:", err) +} +``` + +## Waiting for Initialization + +The SDK provides a channel to wait for initialization to complete. This is useful in server applications where you want to ensure the SDK is ready before handling requests: + +```go +select { +case <-client.Initialized(): + log.Println("Eppo SDK initialized successfully") +case <-time.After(2 * time.Second): + log.Warn("Timed out waiting for Eppo SDK to initialize") +} +``` + +## Advanced Configuration + +Basic initialization is great for most use cases, but the SDK provides options that you can use during initialization to customize the behavior of the SDK. + +### Configuration Options + +The `Config` struct accepts the following options: + + + +Your SDK key from the Eppo dashboard. Required. + + + + +A callback that sends each assignment to your data warehouse. Required only for experiment analysis. + + + + +The base URL for the Eppo API. + + + + +The interval at which the SDK polls for configuration updates. + + + + +Maximum time to wait for initial configuration fetch. + + +For example, to configure custom polling intervals and timeouts: + +```go +config := eppoclient.Config{ + SdkKey: "", + AssignmentLogger: &MyLogger{}, + PollInterval: 60 * time.Second, + InitialWaitTime: 5 * time.Second, +} +``` + +### Configuration Caching + +The SDK can cache previously loaded configurations for use in future sessions. This makes the SDK initialize faster and provides resilience against network issues. + +#### Advanced Configuration Control + +The Go SDK exposes an API to allow manual control over configuration: + +```go +// Get current configuration +config := client.GetConfiguration() + +// Access specific parts of the configuration +flagsConfig := config.GetFlagsConfiguration() +flagKeys := config.GetFlagKeys() +banditKeys := config.GetBanditKeys() +``` + +This API can be used for debugging or advanced optimizations like: +- Caching configuration +- Faster client-side initialization from server configuration +- Debugging flag assignments + +### Usage in Serverless Environments + +The default periodic polling setup is suitable for most cases but may not be efficient in short-lived serverless environments like AWS Lambda, where a new configuration is fetched on every function call. + +For serverless environments, you can: + +1. Configure shorter initialization timeouts: +```go +config := eppoclient.Config{ + SdkKey: "", + InitialWaitTime: 500 * time.Millisecond, +} +``` + +2. Manually control configuration updates: +```go +// Initialize with a cached configuration +cachedConfig := getCachedConfiguration() // Your caching logic +config := eppoclient.Config{ + SdkKey: "", + InitialConfiguration: cachedConfig, +} +client, err := eppoclient.InitClient(config) + +// Later, update configuration manually if needed +newConfig := fetchNewConfiguration() // Your update logic +client.SetConfiguration(newConfig) +``` + +### Example Configurations + +Here are some common configuration patterns based on different needs: + +#### Prioritize Flag Value Freshness +If you want to always use the latest flag values: + +```go +config := eppoclient.Config{ + SdkKey: "", + PollInterval: 30 * time.Second, + InitialWaitTime: 2 * time.Second, +} +``` + +#### Prioritize Fast Initialization +If you want to optimize for quick initialization: + +```go +cachedConfig := getCachedConfiguration() // Your caching logic +config := eppoclient.Config{ + SdkKey: "", + InitialConfiguration: cachedConfig, + PollInterval: 5 * time.Minute, +} +``` + +#### Offline Mode +For completely offline operation: + +```go +offlineConfig := getOfflineConfiguration() // Your configuration source +config := eppoclient.Config{ + SdkKey: "", + InitialConfiguration: offlineConfig, + BaseURL: "", // Disable API calls +} +``` + +## Best Practices + +1. **Initialize Once**: Create a single instance of the SDK at application startup and reuse it throughout your application. + +2. **Handle Errors**: Always implement error handling around initialization to make sure errors don't disrupt your application. + +3. **Configure Timeouts**: Set appropriate timeout values for your use case to prevent hanging operations. + +4. **Monitor Memory**: Be mindful of memory usage when setting polling intervals, as configurations are cached in memory. \ No newline at end of file diff --git a/docs/sdks/server-sdks/go/intro.mdx b/docs/sdks/server-sdks/go/intro.mdx new file mode 100644 index 00000000..8ae472e6 --- /dev/null +++ b/docs/sdks/server-sdks/go/intro.mdx @@ -0,0 +1,29 @@ +--- +title: SDK Guide +sidebar_position: 1 +--- +import FeatureCard from '/src/components/FeatureCard'; + +The Eppo Go SDK allows you to manage feature flags and experiments in your Go applications. + +
+ + + + +
\ No newline at end of file diff --git a/docs/sdks/server-sdks/go/quickstart.mdx b/docs/sdks/server-sdks/go/quickstart.mdx new file mode 100644 index 00000000..290c564b --- /dev/null +++ b/docs/sdks/server-sdks/go/quickstart.mdx @@ -0,0 +1,209 @@ +--- +title: Quickstart +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The Eppo Go SDK enables feature flags and experiments in your Go applications with only a few lines of code. + +The SDK handles all the complexity of feature flag evaluation and experiment assignment locally in your application, with no network calls required after initial setup. This guide will walk you through installing the SDK and implementing your first feature flag, experiment, and contextual bandit. + +## Installation + +First, install the SDK using go get: + +```bash +go get github.com/Eppo-exp/golang-sdk/v6 +``` + +## Feature Flags +Feature flags are a way to toggle features on and off without needing to deploy code. + +### Initialize the SDK + +[Create an SDK key](/sdks/sdk-keys) if you don't already have one. + +First, initialize the SDK using your SDK key: + +```go +import ( + "github.com/eppo-exp/eppo-go-sdk/eppo" +) + +config := eppo.NewConfig("") +client, err := eppo.Init(config) +if err != nil { + log.Fatal("Failed to initialize Eppo client:", err) +} +``` + +:::note +The SDK key is different from the project API key. You can find your SDK key in the [SDK Keys section of the Eppo interface](https://eppo.cloud/configuration/environments/keys). +::: + +### Assign a variant + +Once initialized, you can start making assignments: + +```go +subjectAttributes := map[string]interface{}{ + "country": user.Country, + "age": 30, +} + +variation, err := client.GetStringAssignment( + "show-new-feature", + user.ID, + subjectAttributes, + "control", +) +if err != nil { + log.Printf("Error getting assignment: %v", err) + return +} + +switch variation { +case "variant-a": + handleVariantA() +case "variant-b": + handleVariantB() +default: + handleControl() +} +``` + +### Assignment Types + +The SDK provides different assignment functions based on the type of value you need: + +| Function | Return Type | +|----------|-------------| +| `GetStringAssignment()` | string | +| `GetBoolAssignment()` | bool | +| `GetJSONAssignment()` | map[string]interface{} | +| `GetIntegerAssignment()` | int64 | +| `GetNumericAssignment()` | float64 | + +:::note +See more details about assignment functions in the [Assignments](/sdks/server-sdks/go/assignments) page. +::: + +## Experiments + +While feature flags are useful, they do not send you any information about how your users are interacting with the feature. Experiments provide a way to collect data about these interactions using your preferred event logging system. + +To log events through the SDK, you need to implement the `AssignmentLogger` interface: + +```go +type MyLogger struct{} + +func (l *MyLogger) LogAssignment(assignment *eppo.Assignment) error { + // Replace with your logging logic + log.Printf("Logging assignment: %+v", assignment) + return nil +} + +config := eppo.NewConfig( + "", + eppo.WithAssignmentLogger(&MyLogger{}), +) +client, err := eppo.Init(config) +``` + +:::note +In a production application, you would want to replace the log.Printf with an actual logging system. We have documentation on how to set up logging with multiple popular data warehouses and logging systems in the [Assignments page](/sdks/server-sdks/go/assignments/#logging-data-to-your-data-warehouse). +::: + +## Contextual Bandits + +Contextual Multi-Armed Bandits are a way to dynamically optimize assignments based on user context. A bandit balances exploration of new actions with exploitation of known successful actions to maximize a specified metric. + +### Bandit Setup + +Setting up a bandit requires implementing both an assignment logger and a bandit logger: + +```go +type MyLogger struct{} + +func (l *MyLogger) LogAssignment(assignment *eppo.Assignment) error { + log.Printf("Logging assignment: %+v", assignment) + return nil +} + +func (l *MyLogger) LogBanditAction(banditAction *eppo.BanditAction) error { + log.Printf("Logging bandit action: %+v", banditAction) + return nil +} + +config := eppo.NewConfig( + "", + eppo.WithAssignmentLogger(&MyLogger{}), +) +client, err := eppo.Init(config) +``` + +### Query the bandit for actions + +Instead of making simple assignments with a bandit, you query the bandit for actions: + +```go +attributes := eppo.Attributes{ + NumericAttributes: map[string]float64{ + "age": float64(user.Age), + }, + CategoricalAttributes: map[string]string{ + "country": user.Country, + }, +} + +actions := map[string]eppo.Attributes{ + "nike": { + NumericAttributes: map[string]float64{ + "brand_affinity": 2.3, + }, + CategoricalAttributes: map[string]string{ + "previously_purchased": "true", + }, + }, + "adidas": { + NumericAttributes: map[string]float64{ + "brand_affinity": 0.2, + }, + CategoricalAttributes: map[string]string{ + "previously_purchased": "false", + }, + }, +} + +result, err := client.GetBanditAction( + "shoe-bandit", + user.ID, + attributes, + actions, + "control", +) +if err != nil { + log.Printf("Error getting bandit action: %v", err) + return +} + +if result.Action != "" { + showShoeAd(result.Action) +} else { + showDefaultAd() +} +``` + +:::note +For full steps to create a bandit including UI steps, see the [bandit quickstart](/bandit-quickstart). +::: + +## Next Steps + +Now that you've seen how to make assignments with the Eppo Go SDK, we recommend familiarizing yourself with: + +- [High Level concepts for the server API](/sdks/server-sdks) +- [Initialization Configuration](/sdks/server-sdks/go/initialization) +- [Assignment details](/sdks/server-sdks/go/assignments) \ No newline at end of file