Skip to content

feat!: v5 #151

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

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft

feat!: v5 #151

wants to merge 38 commits into from

Conversation

rolodato
Copy link
Member

@rolodato rolodato commented Mar 22, 2025

Breaking changes

Removed support for transient traits and transient evaluations

Traits are key-value pairs, so they are most naturally represented by a map[string]any. However, because each individual transient can be transient or not, we have to introduce a new Trait type to account for this, so we lose the simplicity and interoperability of using plain maps. The same applies for identifiers, which could be represented by a native string but instead we have to use an Identity struct or awkwardly pass around booleans.

To me, this tradeoff is not worth it, because this SDK will almost always be used with local evaluation, i.e. all evaluation is transient anyway. Transient identities can still be used with an empty identifier.

More thoughts on transient traits

I think the concept of transient traits and identities is flawed and should be replaced with something simpler. I also think this is the reason why supporting transient traits in SDKs causes the code to be so cumbersome and awkward.

An evaluation context is some data that should know nothing about when or how it's being evaluated. The fact that Flagsmith can sometimes persist context data as a side effect of remote evaluation does not belong as part of the evaluation context. These are orthogonal concerns that should be kept separate.

For example, the /identities API could accept an allowlist and denylist of trait keys to persist, separate from marking each individual trait as transient or not. This avoids tying trait data and whether to store this data as part of the same entity.

Simplified evaluation context types

NewEvaluationContext and NewTransientEvaluationContext are now the only ways to obtain EvaluationContext instances. All other related types such as Identity, IdentityEvaluationContext, TraitEvaluationContext andEnvironmentEvaluationContext are now removed.

TraitKey and TraitValue are renamed to Key and Value respectively in the Trait struct. Using map[string]interface{} to represent traits is preferred.

Removed WithOfflineHandler in favour of WithOfflineEnvironment

Previously, Client required two options for using offline mode: WithOfflineMode and WithOfflineEnvironment, which restricted with other SDK options could be used. Also, the idea of an offline handler was misleading, because the handler was only ever called once during SDK initialisation, and never again during the SDK lifecycle.

Now, we've added WithOfflineEnvironment which accepts a constructed EnvironmentModel instead. NewLocalFileHandler was removed in favour of ReadEnvironmentFile, which reads a file into an EnvironmentModel.

We also added a SetEnvironment method which lets consumers of the SDK update their offline/local environment whenever they want, instead of whenever the SDK decides to.

Simplified error types

Removed FlagsmithClientError since it only wraps string messages and is not used in any error handlers.

Renamed FlagmithAPIError to APIError. Removed Msg, ResponseStatusCode and ResponseStatus. Added APIError.Response, which should be used instead.

NewClient no longer panics

NewClient can now return an error and does not panic. Added MustNewClient, which can panic.

Removed custom logger interface

WithLogger now accepts a *slog.Logger instead of a custom logger implementation.

New features

The SDK now uses slog for all its logging. All network requests, responses, durations, failures and errors can now be debug logged. Many logs were improved with useful context and more helpful messages.

WithDefaultFlagHandler now automatically sets Flag.IsDefault and Flag.FeatureName. Previously, handlers would have to do this manually, which is unnecssary.

When using the SDK offline, the local environment can be now updated at any time by calling Client.SetEnvironment instead of only once after NewClient is called.

Bug fixes

Fixed environments being unnecessarily fetched when the realtime service responds immediately with the latest environment timestamp after a connection starts.

Other changes

GetEnvironmentFlags and GetIdentityFlags are no longer deprecated. These are useful aliases for the most common use cases across all SDKs.

The current environment state is now stored using a mutex and a sync.Map instead of atomic.Value references stored in context values. This makes concurrency bugs less likely and is more idiomatic.

EvaluationContext is now entirely defined in this package, and not code-generated from a common schema.

All tests now print debug logs by default.

@rolodato rolodato force-pushed the feat/event-channel branch from a050a54 to e79acfd Compare March 22, 2025 21:56
@rolodato rolodato force-pushed the feat/event-channel branch from 33aefe0 to bcc3e1c Compare March 23, 2025 00:47
@rolodato rolodato force-pushed the feat/event-channel branch from 6394d25 to cf672ff Compare March 23, 2025 01:55
@rolodato rolodato marked this pull request as ready for review March 23, 2025 02:13
@rolodato rolodato changed the title feat! v5 feat!: v5 Mar 23, 2025
@rolodato rolodato force-pushed the feat/event-channel branch from b38875c to fb512ba Compare March 23, 2025 02:55
@rolodato rolodato force-pushed the feat/event-channel branch from fb512ba to d0e37a1 Compare March 23, 2025 03:02
Copy link
Member

@khvn26 khvn26 left a comment

Choose a reason for hiding this comment

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

I like the overall approach and didn't find any immediate issues having read through the code. That said, I expect the ability to provide transient traits/identities to be restored in a separate PR, preferably ahead of next release — we can discuss the exact approach offline.

Copy link
Member

@gagantrivedi gagantrivedi left a comment

Choose a reason for hiding this comment

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

I have gone through some of the code, but not all of it yet.

}

type EnvironmentEvaluationContext struct {
Copy link
Member

Choose a reason for hiding this comment

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

Why did we get rid of this?

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO it doesn't provide any value to the SDK, and we should encourage customers to create evaluation contexts using our functions such as NewEvaluationContext. I can't see any reason why it should have been here in the first place. I'm happy to add it back if we can find a good reason to.

Copy link
Member

Choose a reason for hiding this comment

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

It was used to dynamically pass environment key to get flags etc

Copy link
Member Author

Choose a reason for hiding this comment

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

That can and should be passed as the argument to GetFlags. I really would like to not have the environment be mutated by SDK users.

In general, the evaluation context for server-side SDKs should not be mutable, since there's almost surely going to be a single instance in the whole application performing all the evaluations. I think this is the same reason why OpenFeature server-side SDKs don't provide a way to mutate the context, and instead require that each flag evaluation have an explicit context. I agree with this approach.

@rolodato rolodato requested a review from gagantrivedi March 25, 2025 13:57
// Initialise analytics processor
if c.config.enableAnalytics {
c.analyticsProcessor = NewAnalyticsProcessor(c.ctxAnalytics, c.client, c.config.baseURL, nil, c.log)
if c.ctxAnalytics != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Can we add the variable back? I'd prefer not to do if else on context

Copy link
Member

Choose a reason for hiding this comment

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

Can we also a test here to make sure if analytics processor is enabled, it is initialised properly?

if ok {
envCtx := ec.Environment
if envCtx != nil {
req.SetHeader(EnvironmentKeyHeader, envCtx.APIKey)
Copy link
Member

Choose a reason for hiding this comment

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

@khvn26 do you remember why we did this? As far I can remember, it was because a customer wanted it?

Copy link
Member

Choose a reason for hiding this comment

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

I think it was just general support for overriding the evaluation context

@@ -205,92 +210,100 @@ func (c *Client) BulkIdentify(ctx context.Context, batch []*IdentityTraits) erro
SetBody(&body).
SetContext(ctx).
ForceContentType("application/json").
Post(c.config.baseURL + "bulk-identities/")
if resp.StatusCode() == 404 {
Copy link
Member

Choose a reason for hiding this comment

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

Why did we get rid of this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see any value in handling this error. A 404 could be returned by an intermediate proxy so the suggestion to make sure Edge is being used is not always correct.

The better solution here would be to implement the /bulk-identities endpoint on the Core API, but have it return 404 and a relevant error message.

@@ -7,34 +7,26 @@ import (
"github.com/Flagsmith/flagsmith-go-client/v4/flagengine/environments"
)

type OfflineHandler interface {
GetEnvironment() *environments.EnvironmentModel
type Environment interface {
Copy link
Member

Choose a reason for hiding this comment

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

This is just confusing—now we have an interface with the same name as the method. I don't really think we're gaining anything by breaking the old interface?

Copy link
Member Author

Choose a reason for hiding this comment

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

The name "Handler" is misleading. This type was not being used as a handler because it's not called in response to any event; the offline environment was only being initialised when the client is constructed and never again. For example, http.Handler is named correctly because it's called in response to a request, but this was not the case for OfflineHandler.

I'll rename the method in this interface back to GetEnvironment

return nil
}

func (cs *environmentState) SetEnvironment(env *environments.EnvironmentModel) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we have any tests for functions defined in the file?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not directly, I'll add them later

}
return f
}
if resp.StatusCode() != 200 {
Copy link
Member

Choose a reason for hiding this comment

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

Why did we get rid of this check? Anything other than 200 should be error for us?

Copy link
Member Author

Choose a reason for hiding this comment

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

This wasn't removed, just changed to !resp.IsSuccess. The diff is confusing because this method was moved in the file. The check is still there: https://github.com/Flagsmith/flagsmith-go-client/pull/151/files/7dcb4a00171dc3666b2a770a7e573730f9fe9a80#diff-4b667feae66c9d46b21b9ecc19e8958cf4472d162ce0a47ac3e8386af8bbd8cfR177


analyticsProcessor *AnalyticsProcessor
baseURL string
timeout time.Duration
Copy link
Member

Choose a reason for hiding this comment

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

Why did we move timeout and proxy out of options and set them directly on the client? Are we planning to use them somewhere else?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just convenience, I didn't feel it was worth having a container type to hold the different settings so I've added them directly to the client. I can revert if you feel strongly about it but I don't think it makes a difference either way.

client_test.go Outdated

flags, err := client.GetFlags(ctx, nil)
flags, err := client.GetEnvironmentFlags(context.Background())
Copy link
Member

Choose a reason for hiding this comment

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

TestGetFlags is testing GetEnvironmentFlags here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@rolodato rolodato force-pushed the feat/event-channel branch from c9e8ea2 to 7f3bd06 Compare March 26, 2025 12:13
@rolodato rolodato marked this pull request as draft March 27, 2025 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants