...

Source file src/github.com/launchdarkly/go-server-sdk/v6/ldclient.go

Documentation: github.com/launchdarkly/go-server-sdk/v6

     1  package ldclient
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"reflect"
    10  	"time"
    11  
    12  	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
    13  	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
    14  	"github.com/launchdarkly/go-sdk-common/v3/ldreason"
    15  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    16  	ldevents "github.com/launchdarkly/go-sdk-events/v2"
    17  	ldeval "github.com/launchdarkly/go-server-sdk-evaluation/v2"
    18  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
    19  	"github.com/launchdarkly/go-server-sdk/v6/interfaces"
    20  	"github.com/launchdarkly/go-server-sdk/v6/interfaces/flagstate"
    21  	"github.com/launchdarkly/go-server-sdk/v6/internal"
    22  	"github.com/launchdarkly/go-server-sdk/v6/internal/bigsegments"
    23  	"github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
    24  	"github.com/launchdarkly/go-server-sdk/v6/internal/datasource"
    25  	"github.com/launchdarkly/go-server-sdk/v6/internal/datastore"
    26  	"github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
    27  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
    28  	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoreimpl"
    29  )
    30  
    31  // Version is the SDK version.
    32  const Version = internal.SDKVersion
    33  
    34  // LDClient is the LaunchDarkly client.
    35  //
    36  // This object evaluates feature flags, generates analytics events, and communicates with
    37  // LaunchDarkly services. Applications should instantiate a single instance for the lifetime
    38  // of their application and share it wherever feature flags need to be evaluated; all LDClient
    39  // methods are safe to be called concurrently from multiple goroutines.
    40  //
    41  // Some advanced client features are grouped together in API facades that are accessed through
    42  // an LDClient method, such as [LDClient.GetDataSourceStatusProvider].
    43  //
    44  // When an application is shutting down or no longer needs to use the LDClient instance, it
    45  // should call [LDClient.Close] to ensure that all of its connections and goroutines are shut down and
    46  // that any pending analytics events have been delivered.
    47  //
    48  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/server-side/go
    49  type LDClient struct {
    50  	sdkKey                           string
    51  	loggers                          ldlog.Loggers
    52  	eventProcessor                   ldevents.EventProcessor
    53  	dataSource                       subsystems.DataSource
    54  	store                            subsystems.DataStore
    55  	evaluator                        ldeval.Evaluator
    56  	dataSourceStatusBroadcaster      *internal.Broadcaster[interfaces.DataSourceStatus]
    57  	dataSourceStatusProvider         interfaces.DataSourceStatusProvider
    58  	dataStoreStatusBroadcaster       *internal.Broadcaster[interfaces.DataStoreStatus]
    59  	dataStoreStatusProvider          interfaces.DataStoreStatusProvider
    60  	flagChangeEventBroadcaster       *internal.Broadcaster[interfaces.FlagChangeEvent]
    61  	flagTracker                      interfaces.FlagTracker
    62  	bigSegmentStoreStatusBroadcaster *internal.Broadcaster[interfaces.BigSegmentStoreStatus]
    63  	bigSegmentStoreStatusProvider    interfaces.BigSegmentStoreStatusProvider
    64  	bigSegmentStoreWrapper           *ldstoreimpl.BigSegmentStoreWrapper
    65  	eventsDefault                    eventsScope
    66  	eventsWithReasons                eventsScope
    67  	withEventsDisabled               interfaces.LDClientInterface
    68  	logEvaluationErrors              bool
    69  	offline                          bool
    70  }
    71  
    72  // Initialization errors
    73  var (
    74  	// MakeClient and MakeCustomClient will return this error if the SDK was not able to establish a
    75  	// LaunchDarkly connection within the specified time interval. In this case, the LDClient will still
    76  	// continue trying to connect in the background.
    77  	ErrInitializationTimeout = errors.New("timeout encountered waiting for LaunchDarkly client initialization")
    78  
    79  	// MakeClient and MakeCustomClient will return this error if the SDK detected an error that makes it
    80  	// impossible for a LaunchDarkly connection to succeed. Currently, the only such condition is if the
    81  	// SDK key is invalid, since an invalid SDK key will never become valid.
    82  	ErrInitializationFailed = errors.New("LaunchDarkly client initialization failed")
    83  
    84  	// This error is returned by the Variation/VariationDetail methods if feature flags are not available
    85  	// because the client has not successfully initialized. In this case, the result value will be whatever
    86  	// default value was specified by the application.
    87  	ErrClientNotInitialized = errors.New("feature flag evaluation called before LaunchDarkly client initialization completed") //nolint:lll
    88  )
    89  
    90  // MakeClient creates a new client instance that connects to LaunchDarkly with the default configuration.
    91  //
    92  // For advanced configuration options, use [MakeCustomClient]. Calling MakeClient is exactly equivalent to
    93  // calling MakeCustomClient with the config parameter set to an empty value, ld.Config{}.
    94  //
    95  // The client will begin attempting to connect to LaunchDarkly as soon as you call this constructor. The
    96  // constructor will return when it successfully connects, or when the timeout set by the waitFor parameter
    97  // expires, whichever comes first.
    98  //
    99  // If the connection succeeded, the first return value is the client instance, and the error value is nil.
   100  //
   101  // If the timeout elapsed without a successful connection, it still returns a client instance-- in an
   102  // uninitialized state, where feature flags will return default values-- and the error value is
   103  // [ErrInitializationTimeout]. In this case, it will still continue trying to connect in the background.
   104  //
   105  // If there was an unrecoverable error such that it cannot succeed by retrying-- for instance, the SDK key is
   106  // invalid-- it will return a client instance in an uninitialized state, and the error value is
   107  // [ErrInitializationFailed].
   108  //
   109  // If you set waitFor to zero, the function will return immediately after creating the client instance, and
   110  // do any further initialization in the background.
   111  //
   112  // The only time it returns nil instead of a client instance is if the client cannot be created at all due to
   113  // an invalid configuration. This is rare, but could happen if for instance you specified a custom TLS
   114  // certificate file that did not contain a valid certificate.
   115  //
   116  // For more about the difference between an initialized and uninitialized client, and other ways to monitor
   117  // the client's status, see [LDClient.Initialized] and [LDClient.GetDataSourceStatusProvider].
   118  func MakeClient(sdkKey string, waitFor time.Duration) (*LDClient, error) {
   119  	// COVERAGE: this constructor cannot be called in unit tests because it uses the default base
   120  	// URI and will attempt to make a live connection to LaunchDarkly.
   121  	return MakeCustomClient(sdkKey, Config{}, waitFor)
   122  }
   123  
   124  // MakeCustomClient creates a new client instance that connects to LaunchDarkly with a custom configuration.
   125  //
   126  // The config parameter allows customization of all SDK properties; some of these are represented directly as
   127  // fields in Config, while others are set by builder methods on a more specific configuration object. See
   128  // [Config] for details.
   129  //
   130  // Unless it is configured to be offline with Config.Offline or [ldcomponents.ExternalUpdatesOnly], the client
   131  // will begin attempting to connect to LaunchDarkly as soon as you call this constructor. The constructor will
   132  // return when it successfully connects, or when the timeout set by the waitFor parameter expires, whichever
   133  // comes first.
   134  //
   135  // If the connection succeeded, the first return value is the client instance, and the error value is nil.
   136  //
   137  // If the timeout elapsed without a successful connection, it still returns a client instance-- in an
   138  // uninitialized state, where feature flags will return default values-- and the error value is
   139  // [ErrInitializationTimeout]. In this case, it will still continue trying to connect in the background.
   140  //
   141  // If there was an unrecoverable error such that it cannot succeed by retrying-- for instance, the SDK key is
   142  // invalid-- it will return a client instance in an uninitialized state, and the error value is
   143  // [ErrInitializationFailed].
   144  //
   145  // If you set waitFor to zero, the function will return immediately after creating the client instance, and
   146  // do any further initialization in the background.
   147  //
   148  // The only time it returns nil instead of a client instance is if the client cannot be created at all due to
   149  // an invalid configuration. This is rare, but could happen if for instance you specified a custom TLS
   150  // certificate file that did not contain a valid certificate.
   151  //
   152  // For more about the difference between an initialized and uninitialized client, and other ways to monitor
   153  // the client's status, see [LDClient.Initialized] and [LDClient.GetDataSourceStatusProvider].
   154  func MakeCustomClient(sdkKey string, config Config, waitFor time.Duration) (*LDClient, error) {
   155  	// Ensure that any intermediate components we create will be disposed of if we return an error
   156  	client := &LDClient{sdkKey: sdkKey}
   157  	clientValid := false
   158  	defer func() {
   159  		if !clientValid {
   160  			_ = client.Close()
   161  		}
   162  	}()
   163  
   164  	closeWhenReady := make(chan struct{})
   165  
   166  	eventProcessorFactory := getEventProcessorFactory(config)
   167  
   168  	clientContext, err := newClientContextFromConfig(sdkKey, config)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	// Do not create a diagnostics manager if diagnostics are disabled, or if we're not using the standard event processor.
   174  	if !config.DiagnosticOptOut {
   175  		if reflect.TypeOf(eventProcessorFactory) == reflect.TypeOf(ldcomponents.SendEvents()) {
   176  			clientContext.DiagnosticsManager = createDiagnosticsManager(clientContext, sdkKey, config, waitFor)
   177  		}
   178  	}
   179  
   180  	loggers := clientContext.GetLogging().Loggers
   181  	loggers.Infof("Starting LaunchDarkly client %s", Version)
   182  
   183  	client.loggers = loggers
   184  	client.logEvaluationErrors = clientContext.GetLogging().LogEvaluationErrors
   185  
   186  	client.offline = config.Offline
   187  
   188  	client.dataStoreStatusBroadcaster = internal.NewBroadcaster[interfaces.DataStoreStatus]()
   189  	dataStoreUpdateSink := datastore.NewDataStoreUpdateSinkImpl(client.dataStoreStatusBroadcaster)
   190  	storeFactory := config.DataStore
   191  	if storeFactory == nil {
   192  		storeFactory = ldcomponents.InMemoryDataStore()
   193  	}
   194  	clientContextWithDataStoreUpdateSink := clientContext
   195  	clientContextWithDataStoreUpdateSink.DataStoreUpdateSink = dataStoreUpdateSink
   196  	store, err := storeFactory.Build(clientContextWithDataStoreUpdateSink)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	client.store = store
   201  
   202  	bigSegments := config.BigSegments
   203  	if bigSegments == nil {
   204  		bigSegments = ldcomponents.BigSegments(nil)
   205  	}
   206  	bsConfig, err := bigSegments.Build(clientContext)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	bsStore := bsConfig.GetStore()
   211  	client.bigSegmentStoreStatusBroadcaster = internal.NewBroadcaster[interfaces.BigSegmentStoreStatus]()
   212  	if bsStore != nil {
   213  		client.bigSegmentStoreWrapper = ldstoreimpl.NewBigSegmentStoreWrapperWithConfig(
   214  			ldstoreimpl.BigSegmentsConfigurationProperties{
   215  				Store:              bsStore,
   216  				StartPolling:       true,
   217  				StatusPollInterval: bsConfig.GetStatusPollInterval(),
   218  				StaleAfter:         bsConfig.GetStaleAfter(),
   219  				ContextCacheSize:   bsConfig.GetContextCacheSize(),
   220  				ContextCacheTime:   bsConfig.GetContextCacheTime(),
   221  			},
   222  			client.bigSegmentStoreStatusBroadcaster.Broadcast,
   223  			loggers,
   224  		)
   225  		client.bigSegmentStoreStatusProvider = bigsegments.NewBigSegmentStoreStatusProviderImpl(
   226  			client.bigSegmentStoreWrapper.GetStatus,
   227  			client.bigSegmentStoreStatusBroadcaster,
   228  		)
   229  	} else {
   230  		client.bigSegmentStoreStatusProvider = bigsegments.NewBigSegmentStoreStatusProviderImpl(
   231  			nil, client.bigSegmentStoreStatusBroadcaster,
   232  		)
   233  	}
   234  
   235  	dataProvider := ldstoreimpl.NewDataStoreEvaluatorDataProvider(store, loggers)
   236  	evalOptions := []ldeval.EvaluatorOption{
   237  		ldeval.EvaluatorOptionErrorLogger(client.loggers.ForLevel(ldlog.Error)),
   238  	}
   239  	if client.bigSegmentStoreWrapper != nil {
   240  		evalOptions = append(evalOptions, ldeval.EvaluatorOptionBigSegmentProvider(client.bigSegmentStoreWrapper))
   241  	}
   242  	client.evaluator = ldeval.NewEvaluatorWithOptions(dataProvider, evalOptions...)
   243  
   244  	client.dataStoreStatusProvider = datastore.NewDataStoreStatusProviderImpl(store, dataStoreUpdateSink)
   245  
   246  	client.dataSourceStatusBroadcaster = internal.NewBroadcaster[interfaces.DataSourceStatus]()
   247  	client.flagChangeEventBroadcaster = internal.NewBroadcaster[interfaces.FlagChangeEvent]()
   248  	dataSourceUpdateSink := datasource.NewDataSourceUpdateSinkImpl(
   249  		store,
   250  		client.dataStoreStatusProvider,
   251  		client.dataSourceStatusBroadcaster,
   252  		client.flagChangeEventBroadcaster,
   253  		clientContext.GetLogging().LogDataSourceOutageAsErrorAfter,
   254  		loggers,
   255  	)
   256  
   257  	client.eventProcessor, err = eventProcessorFactory.Build(clientContext)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	if isNullEventProcessorFactory(eventProcessorFactory) {
   262  		client.eventsDefault = newDisabledEventsScope()
   263  		client.eventsWithReasons = newDisabledEventsScope()
   264  	} else {
   265  		client.eventsDefault = newEventsScope(client, false)
   266  		client.eventsWithReasons = newEventsScope(client, true)
   267  	}
   268  	// Pre-create the WithEventsDisabled object so that if an application ends up calling WithEventsDisabled
   269  	// frequently, it won't be causing an allocation each time.
   270  	client.withEventsDisabled = newClientEventsDisabledDecorator(client)
   271  
   272  	dataSource, err := createDataSource(config, clientContext, dataSourceUpdateSink)
   273  	client.dataSource = dataSource
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  	client.dataSourceStatusProvider = datasource.NewDataSourceStatusProviderImpl(
   278  		client.dataSourceStatusBroadcaster,
   279  		dataSourceUpdateSink,
   280  	)
   281  
   282  	client.flagTracker = internal.NewFlagTrackerImpl(
   283  		client.flagChangeEventBroadcaster,
   284  		func(flagKey string, context ldcontext.Context, defaultValue ldvalue.Value) ldvalue.Value {
   285  			value, _ := client.JSONVariation(flagKey, context, defaultValue)
   286  			return value
   287  		},
   288  	)
   289  
   290  	clientValid = true
   291  	client.dataSource.Start(closeWhenReady)
   292  	if waitFor > 0 && client.dataSource != datasource.NewNullDataSource() {
   293  		loggers.Infof("Waiting up to %d milliseconds for LaunchDarkly client to start...",
   294  			waitFor/time.Millisecond)
   295  		timeout := time.After(waitFor)
   296  		for {
   297  			select {
   298  			case <-closeWhenReady:
   299  				if !client.dataSource.IsInitialized() {
   300  					loggers.Warn("LaunchDarkly client initialization failed")
   301  					return client, ErrInitializationFailed
   302  				}
   303  
   304  				loggers.Info("Initialized LaunchDarkly client")
   305  				return client, nil
   306  			case <-timeout:
   307  				loggers.Warn("Timeout encountered waiting for LaunchDarkly client initialization")
   308  				go func() { <-closeWhenReady }() // Don't block the DataSource when not waiting
   309  				return client, ErrInitializationTimeout
   310  			}
   311  		}
   312  	}
   313  	go func() { <-closeWhenReady }() // Don't block the DataSource when not waiting
   314  	return client, nil
   315  }
   316  
   317  func createDataSource(
   318  	config Config,
   319  	context *internal.ClientContextImpl,
   320  	dataSourceUpdateSink subsystems.DataSourceUpdateSink,
   321  ) (subsystems.DataSource, error) {
   322  	if config.Offline {
   323  		context.GetLogging().Loggers.Info("Starting LaunchDarkly client in offline mode")
   324  		dataSourceUpdateSink.UpdateStatus(interfaces.DataSourceStateValid, interfaces.DataSourceErrorInfo{})
   325  		return datasource.NewNullDataSource(), nil
   326  	}
   327  	factory := config.DataSource
   328  	if factory == nil {
   329  		// COVERAGE: can't cause this condition in unit tests because it would try to connect to production LD
   330  		factory = ldcomponents.StreamingDataSource()
   331  	}
   332  	contextCopy := *context
   333  	contextCopy.BasicClientContext.DataSourceUpdateSink = dataSourceUpdateSink
   334  	return factory.Build(&contextCopy)
   335  }
   336  
   337  // Identify reports details about an evaluation context.
   338  //
   339  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/identify#go
   340  func (client *LDClient) Identify(context ldcontext.Context) error {
   341  	if client.eventsDefault.disabled {
   342  		return nil
   343  	}
   344  	if err := context.Err(); err != nil {
   345  		client.loggers.Warnf("Identify called with invalid context: %s", err)
   346  		return nil // Don't return an error value because we didn't in the past and it might confuse users
   347  	}
   348  	evt := client.eventsDefault.factory.NewIdentifyEventData(ldevents.Context(context))
   349  	client.eventProcessor.RecordIdentifyEvent(evt)
   350  	return nil
   351  }
   352  
   353  // TrackEvent reports an event associated with an evaluation context.
   354  //
   355  // The eventName parameter is defined by the application and will be shown in analytics reports;
   356  // it normally corresponds to the event name of a metric that you have created through the
   357  // LaunchDarkly dashboard. If you want to associate additional data with this event, use [TrackData]
   358  // or [TrackMetric].
   359  //
   360  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/events#go
   361  func (client *LDClient) TrackEvent(eventName string, context ldcontext.Context) error {
   362  	return client.TrackData(eventName, context, ldvalue.Null())
   363  }
   364  
   365  // TrackData reports an event associated with an evaluation context, and adds custom data.
   366  //
   367  // The eventName parameter is defined by the application and will be shown in analytics reports;
   368  // it normally corresponds to the event name of a metric that you have created through the
   369  // LaunchDarkly dashboard.
   370  //
   371  // The data parameter is a value of any JSON type, represented with the ldvalue.Value type, that
   372  // will be sent with the event. If no such value is needed, use [ldvalue.Null]() (or call [TrackEvent]
   373  // instead). To send a numeric value for experimentation, use [TrackMetric].
   374  //
   375  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/events#go
   376  func (client *LDClient) TrackData(eventName string, context ldcontext.Context, data ldvalue.Value) error {
   377  	if client.eventsDefault.disabled {
   378  		return nil
   379  	}
   380  	if err := context.Err(); err != nil {
   381  		client.loggers.Warnf("Track called with invalid context: %s", err)
   382  		return nil // Don't return an error value because we didn't in the past and it might confuse users
   383  	}
   384  	client.eventProcessor.RecordCustomEvent(
   385  		client.eventsDefault.factory.NewCustomEventData(
   386  			eventName,
   387  			ldevents.Context(context),
   388  			data,
   389  			false,
   390  			0,
   391  		))
   392  	return nil
   393  }
   394  
   395  // TrackMetric reports an event associated with an evaluation context, and adds a numeric value.
   396  // This value is used by the LaunchDarkly experimentation feature in numeric custom metrics, and will also
   397  // be returned as part of the custom event for Data Export.
   398  //
   399  // The eventName parameter is defined by the application and will be shown in analytics reports;
   400  // it normally corresponds to the event name of a metric that you have created through the
   401  // LaunchDarkly dashboard.
   402  //
   403  // The data parameter is a value of any JSON type, represented with the ldvalue.Value type, that
   404  // will be sent with the event. If no such value is needed, use [ldvalue.Null]().
   405  //
   406  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/events#go
   407  func (client *LDClient) TrackMetric(
   408  	eventName string,
   409  	context ldcontext.Context,
   410  	metricValue float64,
   411  	data ldvalue.Value,
   412  ) error {
   413  	if client.eventsDefault.disabled {
   414  		return nil
   415  	}
   416  	if err := context.Err(); err != nil {
   417  		client.loggers.Warnf("TrackMetric called with invalid context: %s", err)
   418  		return nil // Don't return an error value because we didn't in the past and it might confuse users
   419  	}
   420  	client.eventProcessor.RecordCustomEvent(
   421  		client.eventsDefault.factory.NewCustomEventData(
   422  			eventName,
   423  			ldevents.Context(context),
   424  			data,
   425  			true,
   426  			metricValue,
   427  		))
   428  	return nil
   429  }
   430  
   431  // IsOffline returns whether the LaunchDarkly client is in offline mode.
   432  //
   433  // This is only true if you explicitly set the Offline field to true in [Config], to force the client to
   434  // be offline. It does not mean that the client is having a problem connecting to LaunchDarkly. To detect
   435  // the status of a client that is configured to be online, use [LDClient.Initialized] or
   436  // [LDClient.GetDataSourceStatusProvider].
   437  //
   438  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/offline-mode#go
   439  func (client *LDClient) IsOffline() bool {
   440  	return client.offline
   441  }
   442  
   443  // SecureModeHash generates the secure mode hash value for an evaluation context.
   444  //
   445  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/secure-mode#go
   446  func (client *LDClient) SecureModeHash(context ldcontext.Context) string {
   447  	key := []byte(client.sdkKey)
   448  	h := hmac.New(sha256.New, key)
   449  	_, _ = h.Write([]byte(context.FullyQualifiedKey()))
   450  	return hex.EncodeToString(h.Sum(nil))
   451  }
   452  
   453  // Initialized returns whether the LaunchDarkly client is initialized.
   454  //
   455  // If this value is true, it means the client has succeeded at some point in connecting to LaunchDarkly and
   456  // has received feature flag data. It could still have encountered a connection problem after that point, so
   457  // this does not guarantee that the flags are up to date; if you need to know its status in more detail, use
   458  // [LDClient.GetDataSourceStatusProvider].
   459  //
   460  // If this value is false, it means the client has not yet connected to LaunchDarkly, or has permanently
   461  // failed. See [MakeClient] for the reasons that this could happen. In this state, feature flag evaluations
   462  // will always return default values-- unless you are using a database integration and feature flags had
   463  // already been stored in the database by a successfully connected SDK in the past. You can use
   464  // [LDClient.GetDataSourceStatusProvider] to get information on errors, or to wait for a successful retry.
   465  func (client *LDClient) Initialized() bool {
   466  	return client.dataSource.IsInitialized()
   467  }
   468  
   469  // Close shuts down the LaunchDarkly client. After calling this, the LaunchDarkly client
   470  // should no longer be used. The method will block until all pending analytics events (if any)
   471  // been sent.
   472  func (client *LDClient) Close() error {
   473  	client.loggers.Info("Closing LaunchDarkly client")
   474  
   475  	// Normally all of the following components exist; but they could be nil if we errored out
   476  	// partway through the MakeCustomClient constructor, in which case we want to close whatever
   477  	// did get created so far.
   478  	if client.eventProcessor != nil {
   479  		_ = client.eventProcessor.Close()
   480  	}
   481  	if client.dataSource != nil {
   482  		_ = client.dataSource.Close()
   483  	}
   484  	if client.store != nil {
   485  		_ = client.store.Close()
   486  	}
   487  	if client.dataSourceStatusBroadcaster != nil {
   488  		client.dataSourceStatusBroadcaster.Close()
   489  	}
   490  	if client.dataStoreStatusBroadcaster != nil {
   491  		client.dataStoreStatusBroadcaster.Close()
   492  	}
   493  	if client.flagChangeEventBroadcaster != nil {
   494  		client.flagChangeEventBroadcaster.Close()
   495  	}
   496  	if client.bigSegmentStoreStatusBroadcaster != nil {
   497  		client.bigSegmentStoreStatusBroadcaster.Close()
   498  	}
   499  	if client.bigSegmentStoreWrapper != nil {
   500  		client.bigSegmentStoreWrapper.Close()
   501  	}
   502  	return nil
   503  }
   504  
   505  // Flush tells the client that all pending analytics events (if any) should be delivered as soon
   506  // as possible. This flush is asynchronous, so this method will return before it is complete. To wait
   507  // for the flush to complete, use [LDClient.FlushAndWait] instead (or, if you are done with the SDK,
   508  // [LDClient.Close]).
   509  //
   510  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/flush#go
   511  func (client *LDClient) Flush() {
   512  	client.eventProcessor.Flush()
   513  }
   514  
   515  // FlushAndWait tells the client to deliver any pending analytics events synchronously now.
   516  //
   517  // Unlike [LDClient.Flush], this method waits for event delivery to finish. The timeout parameter, if
   518  // greater than zero, specifies the maximum amount of time to wait. If the timeout elapses before
   519  // delivery is finished, the method returns early and returns false; in this case, the SDK may still
   520  // continue trying to deliver the events in the background.
   521  //
   522  // If the timeout parameter is zero or negative, the method waits as long as necessary to deliver the
   523  // events. However, the SDK does not retry event delivery indefinitely; currently, any network error
   524  // or server error will cause the SDK to wait one second and retry one time, after which the events
   525  // will be discarded so that the SDK will not keep consuming more memory for events indefinitely.
   526  //
   527  // The method returns true if event delivery either succeeded, or definitively failed, before the
   528  // timeout elapsed. It returns false if the timeout elapsed.
   529  //
   530  // This method is also implicitly called if you call [LDClient.Close]. The difference is that
   531  // FlushAndWait does not shut down the SDK client.
   532  //
   533  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/flush#go
   534  func (client *LDClient) FlushAndWait(timeout time.Duration) bool {
   535  	return client.eventProcessor.FlushBlocking(timeout)
   536  }
   537  
   538  // AllFlagsState returns an object that encapsulates the state of all feature flags for a given evaluation.
   539  // context. This includes the flag values, and also metadata that can be used on the front end.
   540  //
   541  // The most common use case for this method is to bootstrap a set of client-side feature flags from a
   542  // back-end service.
   543  //
   544  // You may pass any combination of [flagstate.ClientSideOnly], [flagstate.WithReasons], and
   545  // [flagstate.DetailsOnlyForTrackedFlags] as optional parameters to control what data is included.
   546  //
   547  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/all-flags#go
   548  func (client *LDClient) AllFlagsState(context ldcontext.Context, options ...flagstate.Option) flagstate.AllFlags {
   549  	valid := true
   550  	if client.IsOffline() {
   551  		client.loggers.Warn("Called AllFlagsState in offline mode. Returning empty state")
   552  		valid = false
   553  	} else if !client.Initialized() {
   554  		if client.store.IsInitialized() {
   555  			client.loggers.Warn("Called AllFlagsState before client initialization; using last known values from data store")
   556  		} else {
   557  			client.loggers.Warn("Called AllFlagsState before client initialization. Data store not available; returning empty state") //nolint:lll
   558  			valid = false
   559  		}
   560  	}
   561  
   562  	if !valid {
   563  		return flagstate.AllFlags{}
   564  	}
   565  
   566  	items, err := client.store.GetAll(datakinds.Features)
   567  	if err != nil {
   568  		client.loggers.Warn("Unable to fetch flags from data store. Returning empty state. Error: " + err.Error())
   569  		return flagstate.AllFlags{}
   570  	}
   571  
   572  	clientSideOnly := false
   573  	for _, o := range options {
   574  		if o == flagstate.OptionClientSideOnly() {
   575  			clientSideOnly = true
   576  			break
   577  		}
   578  	}
   579  
   580  	state := flagstate.NewAllFlagsBuilder(options...)
   581  	for _, item := range items {
   582  		if item.Item.Item != nil {
   583  			if flag, ok := item.Item.Item.(*ldmodel.FeatureFlag); ok {
   584  				if clientSideOnly && !flag.ClientSideAvailability.UsingEnvironmentID {
   585  					continue
   586  				}
   587  
   588  				result := client.evaluator.Evaluate(flag, context, nil)
   589  
   590  				state.AddFlag(
   591  					item.Key,
   592  					flagstate.FlagState{
   593  						Value:                result.Detail.Value,
   594  						Variation:            result.Detail.VariationIndex,
   595  						Reason:               result.Detail.Reason,
   596  						Version:              flag.Version,
   597  						TrackEvents:          flag.TrackEvents || result.IsExperiment,
   598  						TrackReason:          result.IsExperiment,
   599  						DebugEventsUntilDate: flag.DebugEventsUntilDate,
   600  					},
   601  				)
   602  			}
   603  		}
   604  	}
   605  
   606  	return state.Build()
   607  }
   608  
   609  // BoolVariation returns the value of a boolean feature flag for a given evaluation context.
   610  //
   611  // Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
   612  // has no off variation.
   613  //
   614  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
   615  func (client *LDClient) BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error) {
   616  	detail, err := client.variation(key, context, ldvalue.Bool(defaultVal), true, client.eventsDefault)
   617  	return detail.Value.BoolValue(), err
   618  }
   619  
   620  // BoolVariationDetail is the same as [LDClient.BoolVariation], but also returns further information about how
   621  // the value was calculated. The "reason" data will also be included in analytics events.
   622  //
   623  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
   624  func (client *LDClient) BoolVariationDetail(
   625  	key string,
   626  	context ldcontext.Context,
   627  	defaultVal bool,
   628  ) (bool, ldreason.EvaluationDetail, error) {
   629  	detail, err := client.variation(key, context, ldvalue.Bool(defaultVal), true, client.eventsWithReasons)
   630  	return detail.Value.BoolValue(), detail, err
   631  }
   632  
   633  // IntVariation returns the value of a feature flag (whose variations are integers) for the given evaluation
   634  // context.
   635  //
   636  // Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
   637  // has no off variation.
   638  //
   639  // If the flag variation has a numeric value that is not an integer, it is rounded toward zero (truncated).
   640  //
   641  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
   642  func (client *LDClient) IntVariation(key string, context ldcontext.Context, defaultVal int) (int, error) {
   643  	detail, err := client.variation(key, context, ldvalue.Int(defaultVal), true, client.eventsDefault)
   644  	return detail.Value.IntValue(), err
   645  }
   646  
   647  // IntVariationDetail is the same as [LDClient.IntVariation], but also returns further information about how
   648  // the value was calculated. The "reason" data will also be included in analytics events.
   649  //
   650  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
   651  func (client *LDClient) IntVariationDetail(
   652  	key string,
   653  	context ldcontext.Context,
   654  	defaultVal int,
   655  ) (int, ldreason.EvaluationDetail, error) {
   656  	detail, err := client.variation(key, context, ldvalue.Int(defaultVal), true, client.eventsWithReasons)
   657  	return detail.Value.IntValue(), detail, err
   658  }
   659  
   660  // Float64Variation returns the value of a feature flag (whose variations are floats) for the given evaluation
   661  // context.
   662  //
   663  // Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and
   664  // has no off variation.
   665  //
   666  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
   667  func (client *LDClient) Float64Variation(key string, context ldcontext.Context, defaultVal float64) (float64, error) {
   668  	detail, err := client.variation(key, context, ldvalue.Float64(defaultVal), true, client.eventsDefault)
   669  	return detail.Value.Float64Value(), err
   670  }
   671  
   672  // Float64VariationDetail is the same as [LDClient.Float64Variation], but also returns further information about how
   673  // the value was calculated. The "reason" data will also be included in analytics events.
   674  //
   675  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
   676  func (client *LDClient) Float64VariationDetail(
   677  	key string,
   678  	context ldcontext.Context,
   679  	defaultVal float64,
   680  ) (float64, ldreason.EvaluationDetail, error) {
   681  	detail, err := client.variation(key, context, ldvalue.Float64(defaultVal), true, client.eventsWithReasons)
   682  	return detail.Value.Float64Value(), detail, err
   683  }
   684  
   685  // StringVariation returns the value of a feature flag (whose variations are strings) for the given evaluation
   686  // context.
   687  //
   688  // Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off and has
   689  // no off variation.
   690  //
   691  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
   692  func (client *LDClient) StringVariation(key string, context ldcontext.Context, defaultVal string) (string, error) {
   693  	detail, err := client.variation(key, context, ldvalue.String(defaultVal), true, client.eventsDefault)
   694  	return detail.Value.StringValue(), err
   695  }
   696  
   697  // StringVariationDetail is the same as [LDClient.StringVariation], but also returns further information about how
   698  // the value was calculated. The "reason" data will also be included in analytics events.
   699  //
   700  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
   701  func (client *LDClient) StringVariationDetail(
   702  	key string,
   703  	context ldcontext.Context,
   704  	defaultVal string,
   705  ) (string, ldreason.EvaluationDetail, error) {
   706  	detail, err := client.variation(key, context, ldvalue.String(defaultVal), true, client.eventsWithReasons)
   707  	return detail.Value.StringValue(), detail, err
   708  }
   709  
   710  // JSONVariation returns the value of a feature flag for the given evaluation context, allowing the value to
   711  // be of any JSON type.
   712  //
   713  // The value is returned as an [ldvalue.Value], which can be inspected or converted to other types using
   714  // methods such as [ldvalue.Value.GetType] and [ldvalue.Value.BoolValue]. The defaultVal parameter also uses this
   715  // type. For instance, if the values for this flag are JSON arrays:
   716  //
   717  //	defaultValAsArray := ldvalue.BuildArray().
   718  //	    Add(ldvalue.String("defaultFirstItem")).
   719  //	    Add(ldvalue.String("defaultSecondItem")).
   720  //	    Build()
   721  //	result, err := client.JSONVariation(flagKey, context, defaultValAsArray)
   722  //	firstItemAsString := result.GetByIndex(0).StringValue() // "defaultFirstItem", etc.
   723  //
   724  // You can also use unparsed json.RawMessage values:
   725  //
   726  //	defaultValAsRawJSON := ldvalue.Raw(json.RawMessage(`{"things":[1,2,3]}`))
   727  //	result, err := client.JSONVariation(flagKey, context, defaultValAsJSON
   728  //	resultAsRawJSON := result.AsRaw()
   729  //
   730  // Returns defaultVal if there is an error, if the flag doesn't exist, or the feature is turned off.
   731  //
   732  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go
   733  func (client *LDClient) JSONVariation(
   734  	key string,
   735  	context ldcontext.Context,
   736  	defaultVal ldvalue.Value,
   737  ) (ldvalue.Value, error) {
   738  	detail, err := client.variation(key, context, defaultVal, false, client.eventsDefault)
   739  	return detail.Value, err
   740  }
   741  
   742  // JSONVariationDetail is the same as [LDClient.JSONVariation], but also returns further information about how
   743  // the value was calculated. The "reason" data will also be included in analytics events.
   744  //
   745  // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluation-reasons#go
   746  func (client *LDClient) JSONVariationDetail(
   747  	key string,
   748  	context ldcontext.Context,
   749  	defaultVal ldvalue.Value,
   750  ) (ldvalue.Value, ldreason.EvaluationDetail, error) {
   751  	detail, err := client.variation(key, context, defaultVal, false, client.eventsWithReasons)
   752  	return detail.Value, detail, err
   753  }
   754  
   755  // GetDataSourceStatusProvider returns an interface for tracking the status of the data source.
   756  //
   757  // The data source is the mechanism that the SDK uses to get feature flag configurations, such as a
   758  // streaming connection (the default) or poll requests. The [interfaces.DataSourceStatusProvider] has methods
   759  // for checking whether the data source is (as far as the SDK knows) currently operational and tracking
   760  // changes in this status.
   761  //
   762  // See the DataSourceStatusProvider interface for more about this functionality.
   763  func (client *LDClient) GetDataSourceStatusProvider() interfaces.DataSourceStatusProvider {
   764  	return client.dataSourceStatusProvider
   765  }
   766  
   767  // GetDataStoreStatusProvider returns an interface for tracking the status of a persistent data store.
   768  //
   769  // The [interfaces.DataStoreStatusProvider] has methods for checking whether the data store is (as far as the SDK
   770  // knows) currently operational, tracking changes in this status, and getting cache statistics. These
   771  // are only relevant for a persistent data store; if you are using an in-memory data store, then this
   772  // method will always report that the store is operational.
   773  //
   774  // See the DataStoreStatusProvider interface for more about this functionality.
   775  func (client *LDClient) GetDataStoreStatusProvider() interfaces.DataStoreStatusProvider {
   776  	return client.dataStoreStatusProvider
   777  }
   778  
   779  // GetFlagTracker returns an interface for tracking changes in feature flag configurations.
   780  //
   781  // See [interfaces.FlagTracker] for more about this functionality.
   782  func (client *LDClient) GetFlagTracker() interfaces.FlagTracker {
   783  	return client.flagTracker
   784  }
   785  
   786  // GetBigSegmentStoreStatusProvider returns an interface for tracking the status of a Big
   787  // Segment store.
   788  //
   789  // The BigSegmentStoreStatusProvider has methods for checking whether the Big Segment store
   790  // is (as far as the SDK knows) currently operational and tracking changes in this status.
   791  //
   792  // See [interfaces.BigSegmentStoreStatusProvider] for more about this functionality.
   793  func (client *LDClient) GetBigSegmentStoreStatusProvider() interfaces.BigSegmentStoreStatusProvider {
   794  	return client.bigSegmentStoreStatusProvider
   795  }
   796  
   797  // WithEventsDisabled returns a decorator for the LDClient that implements the same basic operations
   798  // but will not generate any analytics events.
   799  //
   800  // If events were already disabled, this is just the same LDClient. Otherwise, it is an object whose
   801  // Variation methods use the same LDClient to evaluate feature flags, but without generating any
   802  // events, and whose Identify/Track/Custom methods do nothing. Neither evaluation counts nor context
   803  // properties will be sent to LaunchDarkly for any operations done with this object.
   804  //
   805  // You can use this to suppress events within some particular area of your code where you do not want
   806  // evaluations to affect your dashboard statistics, or do not want to incur the overhead of processing
   807  // the events.
   808  //
   809  // Note that if the original client configuration already had events disabled
   810  // (config.Events = ldcomponents.NoEvents()), you cannot re-enable them with this method. It is only
   811  // useful for temporarily disabling events on a client that had them enabled.
   812  func (client *LDClient) WithEventsDisabled(disabled bool) interfaces.LDClientInterface {
   813  	if !disabled || client.eventsDefault.disabled {
   814  		return client
   815  	}
   816  	return client.withEventsDisabled
   817  }
   818  
   819  // Generic method for evaluating a feature flag for a given evaluation context.
   820  func (client *LDClient) variation(
   821  	key string,
   822  	context ldcontext.Context,
   823  	defaultVal ldvalue.Value,
   824  	checkType bool,
   825  	eventsScope eventsScope,
   826  ) (ldreason.EvaluationDetail, error) {
   827  	if err := context.Err(); err != nil {
   828  		client.loggers.Warnf("Tried to evaluate a flag with an invalid context: %s", err)
   829  		return newEvaluationError(defaultVal, ldreason.EvalErrorUserNotSpecified), err
   830  	}
   831  	if client.IsOffline() {
   832  		return newEvaluationError(defaultVal, ldreason.EvalErrorClientNotReady), nil
   833  	}
   834  	result, flag, err := client.evaluateInternal(key, context, defaultVal, eventsScope)
   835  	if err != nil {
   836  		result.Detail.Value = defaultVal
   837  		result.Detail.VariationIndex = ldvalue.OptionalInt{}
   838  	} else if checkType && defaultVal.Type() != ldvalue.NullType && result.Detail.Value.Type() != defaultVal.Type() {
   839  		result.Detail = newEvaluationError(defaultVal, ldreason.EvalErrorWrongType)
   840  	}
   841  
   842  	if !eventsScope.disabled {
   843  		var eval ldevents.EvaluationData
   844  		if flag == nil {
   845  			eval = eventsScope.factory.NewUnknownFlagEvaluationData(
   846  				key,
   847  				ldevents.Context(context),
   848  				defaultVal,
   849  				result.Detail.Reason,
   850  			)
   851  		} else {
   852  			eval = eventsScope.factory.NewEvaluationData(
   853  				ldevents.FlagEventProperties{
   854  					Key:                  flag.Key,
   855  					Version:              flag.Version,
   856  					RequireFullEvent:     flag.TrackEvents,
   857  					DebugEventsUntilDate: flag.DebugEventsUntilDate,
   858  				},
   859  				ldevents.Context(context),
   860  				result.Detail,
   861  				result.IsExperiment,
   862  				defaultVal,
   863  				"",
   864  			)
   865  		}
   866  		client.eventProcessor.RecordEvaluation(eval)
   867  	}
   868  
   869  	return result.Detail, err
   870  }
   871  
   872  // Performs all the steps of evaluation except for sending the feature request event (the main one;
   873  // events for prerequisites will be sent).
   874  func (client *LDClient) evaluateInternal(
   875  	key string,
   876  	context ldcontext.Context,
   877  	defaultVal ldvalue.Value,
   878  	eventsScope eventsScope,
   879  ) (ldeval.Result, *ldmodel.FeatureFlag, error) {
   880  	// THIS IS A HIGH-TRAFFIC CODE PATH so performance tuning is important. Please see CONTRIBUTING.md for guidelines
   881  	// to keep in mind during any changes to the evaluation logic.
   882  
   883  	var feature *ldmodel.FeatureFlag
   884  	var storeErr error
   885  	var ok bool
   886  
   887  	evalErrorResult := func(
   888  		errKind ldreason.EvalErrorKind,
   889  		flag *ldmodel.FeatureFlag,
   890  		err error,
   891  	) (ldeval.Result, *ldmodel.FeatureFlag, error) {
   892  		detail := newEvaluationError(defaultVal, errKind)
   893  		if client.logEvaluationErrors {
   894  			client.loggers.Warn(err)
   895  		}
   896  		return ldeval.Result{Detail: detail}, flag, err
   897  	}
   898  
   899  	if !client.Initialized() {
   900  		if client.store.IsInitialized() {
   901  			client.loggers.Warn("Feature flag evaluation called before LaunchDarkly client initialization completed; using last known values from data store") //nolint:lll
   902  		} else {
   903  			return evalErrorResult(ldreason.EvalErrorClientNotReady, nil, ErrClientNotInitialized)
   904  		}
   905  	}
   906  
   907  	itemDesc, storeErr := client.store.Get(datakinds.Features, key)
   908  
   909  	if storeErr != nil {
   910  		client.loggers.Errorf("Encountered error fetching feature from store: %+v", storeErr)
   911  		detail := newEvaluationError(defaultVal, ldreason.EvalErrorException)
   912  		return ldeval.Result{Detail: detail}, nil, storeErr
   913  	}
   914  
   915  	if itemDesc.Item != nil {
   916  		feature, ok = itemDesc.Item.(*ldmodel.FeatureFlag)
   917  		if !ok {
   918  			return evalErrorResult(ldreason.EvalErrorException, nil,
   919  				fmt.Errorf(
   920  					"unexpected data type (%T) found in store for feature key: %s. Returning default value",
   921  					itemDesc.Item,
   922  					key,
   923  				))
   924  		}
   925  	} else {
   926  		return evalErrorResult(ldreason.EvalErrorFlagNotFound, nil,
   927  			fmt.Errorf("unknown feature key: %s. Verify that this feature key exists. Returning default value", key))
   928  	}
   929  
   930  	result := client.evaluator.Evaluate(feature, context, eventsScope.prerequisiteEventRecorder)
   931  	if result.Detail.Reason.GetKind() == ldreason.EvalReasonError && client.logEvaluationErrors {
   932  		client.loggers.Warnf("Flag evaluation for %s failed with error %s, default value was returned",
   933  			key, result.Detail.Reason.GetErrorKind())
   934  	}
   935  	if result.Detail.IsDefaultValue() {
   936  		result.Detail.Value = defaultVal
   937  	}
   938  	return result, feature, nil
   939  }
   940  
   941  func newEvaluationError(jsonValue ldvalue.Value, errorKind ldreason.EvalErrorKind) ldreason.EvaluationDetail {
   942  	return ldreason.EvaluationDetail{
   943  		Value:  jsonValue,
   944  		Reason: ldreason.NewEvalReasonError(errorKind),
   945  	}
   946  }
   947  

View as plain text