diff --git a/config_fetcher.go b/config_fetcher.go index 88dc2bd..ac07074 100644 --- a/config_fetcher.go +++ b/config_fetcher.go @@ -4,13 +4,14 @@ import ( "context" "errors" "fmt" - "github.com/configcat/go-sdk/v9/configcatcache" "io" "net/http" "os" "sync" "sync/atomic" "time" + + "github.com/configcat/go-sdk/v9/configcatcache" ) const ( @@ -209,6 +210,12 @@ func (f *configFetcher) refreshIfOlder(ctx context.Context, before time.Time, wa func (f *configFetcher) fetcher(prevConfig *config) { defer f.wg.Done() config, newURL, err := f.fetchConfig(f.ctx, f.baseURL, prevConfig) + + // Call OnConfigDownloaded hook only for HTTP requests (not cache or local-only) + if !f.isOffline() && (f.overrides == nil || f.overrides.Behavior != LocalOnly) { + f.callOnConfigDownloaded(err) + } + f.mu.Lock() defer f.mu.Unlock() if err != nil { @@ -422,6 +429,13 @@ func (f *configFetcher) fetchHTTPWithoutRedirect(ctx context.Context, baseURL st return nil, &fetcherError{EventId: 1101, Err: fmt.Errorf("unexpected HTTP response was received while trying to fetch config JSON: %v", response.Status)} } +// callOnConfigDownloaded calls the OnConfigDownloaded hook if it's configured. +func (f *configFetcher) callOnConfigDownloaded(err error) { + if f.hooks != nil && f.hooks.OnConfigDownloaded != nil { + go f.hooks.OnConfigDownloaded(err) + } +} + func pollingModeToIdentifier(pollingMode PollingMode) string { switch pollingMode { case AutoPoll: diff --git a/configcat_client.go b/configcat_client.go index 03cb5a3..bdaf651 100644 --- a/configcat_client.go +++ b/configcat_client.go @@ -24,6 +24,9 @@ type Hooks struct { // OnConfigChanged is called, when a new config.json has downloaded. OnConfigChanged func() + + // OnConfigDownloaded is called every time a config download attempt is made. + OnConfigDownloaded func(err error) } // Config describes configuration options for the Client. diff --git a/configcat_client_test.go b/configcat_client_test.go index 2097b3d..94a1e56 100644 --- a/configcat_client_test.go +++ b/configcat_client_test.go @@ -1138,6 +1138,73 @@ func rootNodeWithKeyValueType(key string, value interface{}, t SettingType) *Con } } +func TestClient_Hooks_OnConfigDownloaded(t *testing.T) { + tests := []struct { + name string + expectError bool + refreshTwice bool + }{ + { + name: "successful_download", + expectError: false, + refreshTwice: false, + }, + { + name: "successful_download_with_refresh", + expectError: false, + refreshTwice: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := qt.New(t) + srv := newConfigServer(t) + srv.setResponse(configResponse{ + body: contentForIntegrationTestKey("PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"), + }) + + downloadedChan := make(chan error, 10) + cfg := srv.config() + cfg.Hooks = &Hooks{OnConfigDownloaded: func(err error) { + downloadedChan <- err + }} + client := NewCustomClient(cfg) + defer client.Close() + + // Wait for the initial config download + select { + case err := <-downloadedChan: + if test.expectError { + c.Assert(err, qt.Not(qt.IsNil)) + } else { + c.Assert(err, qt.IsNil) + } + case <-time.After(time.Second): + t.Fatalf("timed out waiting for OnConfigDownloaded hook") + } + + if test.refreshTwice { + // Trigger a refresh to get another download event + err := client.Refresh(context.Background()) + c.Assert(err, qt.IsNil) + + // Should get another download notification + select { + case err := <-downloadedChan: + if test.expectError { + c.Assert(err, qt.Not(qt.IsNil)) + } else { + c.Assert(err, qt.IsNil) + } + case <-time.After(time.Second): + t.Fatalf("timed out waiting for second OnConfigDownloaded hook") + } + } + }) + } +} + type mockHTTPTransport struct { requests []*http.Request responses []*http.Response