-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathclient.go
207 lines (182 loc) · 5.39 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package growthbook
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/url"
"github.com/growthbook/growthbook-golang/internal/value"
)
const defaultApiHost = "https://cdn.growthbook.io"
var (
ErrNoDecryptionKey = errors.New("No decryption key provided")
)
// Client is a GrowthBook SDK client.
type Client struct {
data *data
enabled bool
attributes value.ObjValue
url *url.URL
forcedVariations ForcedVariationsMap
qaMode bool
experimentCallback ExperimentCallback
featureUsageCallback FeatureUsageCallback
logger *slog.Logger
extraData any
}
// ForcedVariationsMap is a map that forces an Experiment to always assign a specific variation. Useful for QA.
type ForcedVariationsMap map[string]int
// ExperimentCallback function that is executed every time a user is included in an Experiment.
type ExperimentCallback func(context.Context, *Experiment, *ExperimentResult, any)
// FeatureUsageCallback funcion is executed every time feature is evaluated
type FeatureUsageCallback func(context.Context, string, *FeatureResult, any)
// NewApiClient creates simple client with API host and client key
func NewApiClient(apiHost string, clientKey string) (*Client, error) {
ctx := context.Background()
return NewClient(ctx, WithApiHost(apiHost), WithClientKey(clientKey))
}
// NewClient create a new GrowthBook SDK client.
func NewClient(ctx context.Context, opts ...ClientOption) (*Client, error) {
client := defaultClient()
for _, opt := range opts {
err := opt(client)
if err != nil {
return nil, err
}
}
if client.data.dataSource != nil {
go client.startDataSource(ctx)
}
return client, nil
}
// Close client's background goroutines
func (client *Client) Close() error {
ds := client.data.dataSource
if ds == nil || !client.data.getDsStarted() {
return nil
}
return ds.Close()
}
func defaultClient() *Client {
return &Client{
data: newData(),
enabled: true,
qaMode: false,
logger: slog.Default(),
attributes: value.ObjValue{},
}
}
// SetFeatures updates shared client features.
func (client *Client) SetFeatures(features FeatureMap) error {
client.data.withLock(func(d *data) error {
d.features = features
return nil
})
return nil
}
// SetJSONFeatures updates shared features from JSON
func (client *Client) SetJSONFeatures(featuresJSON string) error {
var features FeatureMap
err := json.Unmarshal([]byte(featuresJSON), &features)
if err != nil {
return err
}
return client.SetFeatures(features)
}
// SetEncryptedJSONFeatures updates shared features from encrypted JSON.
// Uses client's decryption key.
func (client *Client) SetEncryptedJSONFeatures(encryptedJSON string) error {
if client.data.decryptionKey == "" {
return ErrNoDecryptionKey
}
featuresJSON, err := decrypt(encryptedJSON, client.data.decryptionKey)
if err != nil {
return err
}
return client.SetJSONFeatures(featuresJSON)
}
// UpdateFromApiResponse updates shared data from Growthbook API response
func (client *Client) UpdateFromApiResponse(resp *FeatureApiResponse) error {
dataUpdated := client.data.getDateUpdated()
apiUpdated := resp.DateUpdated
if apiUpdated.Before(dataUpdated) {
client.logger.Warn("Api response is older then current data, refuse to update",
"dataUpdated", dataUpdated, "apiUdpated", apiUpdated)
return nil
}
var features FeatureMap
var err error
if resp.EncryptedFeatures != "" {
features, err = client.DecryptFeatures(resp.EncryptedFeatures)
if err != nil {
return err
}
} else {
features = resp.Features
}
client.data.withLock(func(d *data) error {
d.features = features
d.savedGroups = resp.SavedGroups
d.dateUpdated = resp.DateUpdated
return nil
})
return nil
}
func (client *Client) DecryptFeatures(encrypted string) (FeatureMap, error) {
var features FeatureMap
featuresJSON, err := client.data.decrypt(encrypted)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(featuresJSON), &features)
if err != nil {
return nil, err
}
return features, err
}
func (client *Client) UpdateFromApiResponseJSON(respJSON string) error {
var resp FeatureApiResponse
err := json.Unmarshal([]byte(respJSON), &resp)
if err != nil {
return err
}
return client.UpdateFromApiResponse(&resp)
}
// EvalFeature evaluates feature based on attributes and features map
func (client *Client) EvalFeature(ctx context.Context, key string) *FeatureResult {
e := client.evaluator()
res := e.evalFeature(key)
if client.featureUsageCallback != nil {
client.featureUsageCallback(ctx, key, res, client.extraData)
}
if client.experimentCallback != nil && res.InExperiment() {
client.experimentCallback(ctx, res.Experiment, res.ExperimentResult, client.extraData)
}
return res
}
func (client *Client) RunExperiment(ctx context.Context, exp *Experiment) *ExperimentResult {
e := client.evaluator()
res := e.runExperiment(exp, "")
if client.experimentCallback != nil && res.InExperiment {
client.experimentCallback(ctx, exp, res, client.extraData)
}
return res
}
func (client *Client) Features() FeatureMap {
return client.data.getFeatures()
}
// Internals
func (client *Client) evaluator() *evaluator {
client.data.mu.RLock()
e := evaluator{
features: client.data.features,
savedGroups: client.data.savedGroups,
client: client,
}
client.data.mu.RUnlock()
return &e
}
func (client *Client) clone() *Client {
c := *client
return &c
}