1 package ldevents
2
3 import (
4 "encoding/json"
5 "testing"
6
7 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
8 "github.com/launchdarkly/go-sdk-common/v3/ldreason"
9 "github.com/launchdarkly/go-sdk-common/v3/lduser"
10 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
11
12 m "github.com/launchdarkly/go-test-helpers/v3/matchers"
13
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 )
17
18 var (
19 withoutReasons = NewEventFactory(false, fakeTimeFn)
20 withReasons = NewEventFactory(true, fakeTimeFn)
21 )
22
23 func withTestContextsAndConfigs(t *testing.T, action func(*testing.T, EventInputContext, EventsConfiguration)) {
24 singleCtx := Context(ldcontext.New("user-key"))
25 multiCtx := Context(ldcontext.NewMulti(ldcontext.New("user-key"), ldcontext.NewWithKind("org", "org-key")))
26
27 privateConfig := basicConfigWithoutPrivateAttrs()
28 privateConfig.AllAttributesPrivate = true
29
30 t.Run("single kind, no private attributes", func(t *testing.T) {
31 action(t, singleCtx, basicConfigWithoutPrivateAttrs())
32 })
33
34 t.Run("multi-kind, no private attributes", func(t *testing.T) {
35 action(t, multiCtx, basicConfigWithoutPrivateAttrs())
36 })
37
38 t.Run("single kind, with private attributes", func(t *testing.T) {
39 action(t, singleCtx, privateConfig)
40 })
41
42 t.Run("multi-kind, with private attributes", func(t *testing.T) {
43 action(t, multiCtx, privateConfig)
44 })
45 }
46
47 func TestEventOutputFullEvents(t *testing.T) {
48 withTestContextsAndConfigs(t, func(t *testing.T, context EventInputContext, config EventsConfiguration) {
49 flag := FlagEventProperties{Key: "flagkey", Version: 100}
50
51 formatter := eventOutputFormatter{
52 contextFormatter: newEventContextFormatter(config),
53 config: config,
54 }
55
56
57
58
59
60 contextJSON := contextJSON(context, config)
61 contextKeys := expectedContextKeys(context.context)
62
63 t.Run("feature", func(t *testing.T) {
64 event1 := withoutReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason),
65 false, ldvalue.String("dv"), "")
66 verifyEventOutput(t, formatter, event1,
67 m.JSONEqual(map[string]interface{}{
68 "kind": "feature",
69 "creationDate": fakeTime,
70 "key": flag.Key,
71 "version": flag.Version,
72 "contextKeys": contextKeys,
73 "variation": 1,
74 "value": "v",
75 "default": "dv",
76 }))
77
78 event1r := withReasons.NewEvaluationData(flag, context,
79 ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, ldreason.NewEvalReasonFallthrough()),
80 false, ldvalue.String("dv"), "")
81 verifyEventOutput(t, formatter, event1r,
82 m.JSONEqual(map[string]interface{}{
83 "kind": "feature",
84 "creationDate": fakeTime,
85 "key": flag.Key,
86 "version": flag.Version,
87 "contextKeys": contextKeys,
88 "variation": 1,
89 "value": "v",
90 "default": "dv",
91 "reason": json.RawMessage(`{"kind":"FALLTHROUGH"}`),
92 }))
93
94 event2 := withoutReasons.NewEvaluationData(flag, context, ldreason.EvaluationDetail{Value: ldvalue.String("v")},
95 false, ldvalue.String("dv"), "")
96 event2.Variation = ldvalue.OptionalInt{}
97 verifyEventOutput(t, formatter, event2,
98 m.JSONEqual(map[string]interface{}{
99 "kind": "feature",
100 "creationDate": fakeTime,
101 "key": flag.Key,
102 "version": flag.Version,
103 "contextKeys": contextKeys,
104 "value": "v",
105 "default": "dv",
106 }))
107
108 event3 := withoutReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason),
109 false, ldvalue.String("dv"), "pre")
110 verifyEventOutput(t, formatter, event3,
111 m.JSONEqual(map[string]interface{}{
112 "kind": "feature",
113 "creationDate": fakeTime,
114 "key": flag.Key,
115 "version": flag.Version,
116 "contextKeys": contextKeys,
117 "variation": 1,
118 "value": "v",
119 "default": "dv",
120 "prereqOf": "pre",
121 }))
122
123 event4 := withoutReasons.NewUnknownFlagEvaluationData("flagkey", context,
124 ldvalue.String("dv"), ldreason.EvaluationReason{})
125 verifyEventOutput(t, formatter, event4,
126 m.JSONEqual(map[string]interface{}{
127 "kind": "feature",
128 "creationDate": fakeTime,
129 "key": flag.Key,
130 "contextKeys": contextKeys,
131 "value": "dv",
132 "default": "dv",
133 }))
134 })
135
136 t.Run("debug", func(t *testing.T) {
137 event1 := withoutReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason),
138 false, ldvalue.String("dv"), "")
139 event1.debug = true
140 verifyEventOutput(t, formatter, event1,
141 m.JSONEqual(map[string]interface{}{
142 "kind": "debug",
143 "creationDate": fakeTime,
144 "key": flag.Key,
145 "version": flag.Version,
146 "context": contextJSON,
147 "variation": 1,
148 "value": "v",
149 "default": "dv",
150 }))
151 })
152
153 t.Run("identify", func(t *testing.T) {
154 event := withoutReasons.NewIdentifyEventData(context)
155 verifyEventOutput(t, formatter, event,
156 m.JSONEqual(map[string]interface{}{
157 "kind": "identify",
158 "creationDate": fakeTime,
159 "context": contextJSON,
160 }))
161 })
162
163 t.Run("custom", func(t *testing.T) {
164 event1 := withoutReasons.NewCustomEventData("eventkey", context, ldvalue.Null(), false, 0)
165 verifyEventOutput(t, formatter, event1,
166 m.JSONEqual(map[string]interface{}{
167 "kind": "custom",
168 "key": "eventkey",
169 "creationDate": fakeTime,
170 "contextKeys": contextKeys,
171 }))
172
173 event2 := withoutReasons.NewCustomEventData("eventkey", context, ldvalue.String("d"), false, 0)
174 verifyEventOutput(t, formatter, event2,
175 m.JSONEqual(map[string]interface{}{
176 "kind": "custom",
177 "key": "eventkey",
178 "creationDate": fakeTime,
179 "contextKeys": contextKeys,
180 "data": "d",
181 }))
182
183 event3 := withoutReasons.NewCustomEventData("eventkey", context, ldvalue.Null(), true, 2.5)
184 verifyEventOutput(t, formatter, event3,
185 m.JSONEqual(map[string]interface{}{
186 "kind": "custom",
187 "key": "eventkey",
188 "creationDate": fakeTime,
189 "contextKeys": contextKeys,
190 "metricValue": 2.5,
191 }))
192 })
193
194 t.Run("index", func(t *testing.T) {
195 event := indexEvent{BaseEvent: BaseEvent{CreationDate: fakeTime, Context: context}}
196 verifyEventOutput(t, formatter, event,
197 m.JSONEqual(map[string]interface{}{
198 "kind": "index",
199 "creationDate": fakeTime,
200 "context": contextJSON,
201 }))
202 })
203
204 t.Run("raw", func(t *testing.T) {
205 rawData := json.RawMessage(`{"kind":"alias","arbitrary":["we","don't","care","what's","in","here"]}`)
206 event := rawEvent{data: rawData}
207 verifyEventOutput(t, formatter, event, m.JSONEqual(rawData))
208 })
209 })
210 }
211
212 func TestEventOutputSummaryEvents(t *testing.T) {
213 user := Context(lduser.NewUser("u"))
214 flag1v1 := FlagEventProperties{Key: "flag1", Version: 100}
215 flag1v2 := FlagEventProperties{Key: "flag1", Version: 200}
216 flag1Default := ldvalue.String("default1")
217 flag2 := FlagEventProperties{Key: "flag2", Version: 1}
218 flag2Default := ldvalue.String("default2")
219
220 formatter := eventOutputFormatter{
221 contextFormatter: newEventContextFormatter(basicConfigWithoutPrivateAttrs()),
222 config: basicConfigWithoutPrivateAttrs(),
223 }
224
225 t.Run("summary - single flag, single counter", func(t *testing.T) {
226 es1 := newEventSummarizer()
227 event1 := withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason),
228 false, ldvalue.String("dv"), "")
229 es1.summarizeEvent(event1)
230 verifySummaryEventOutput(t, formatter, es1.snapshot(),
231 m.JSONEqual(map[string]interface{}{
232 "kind": "summary",
233 "startDate": fakeTime,
234 "endDate": fakeTime,
235 "features": map[string]interface{}{
236 "flag1": map[string]interface{}{
237 "counters": json.RawMessage(`[{"count":1,"value":"v","variation":1,"version":100}]`),
238 "contextKinds": []string{"user"},
239 "default": "dv",
240 },
241 },
242 }))
243
244 es2 := newEventSummarizer()
245 event2 := withoutReasons.NewEvaluationData(flag1v1, user, ldreason.EvaluationDetail{Value: ldvalue.String("dv")},
246 false, ldvalue.String("dv"), "")
247 event2.Variation = ldvalue.OptionalInt{}
248 es2.summarizeEvent(event2)
249 verifySummaryEventOutput(t, formatter, es2.snapshot(),
250 m.JSONEqual(map[string]interface{}{
251 "kind": "summary",
252 "startDate": fakeTime,
253 "endDate": fakeTime,
254 "features": map[string]interface{}{
255 "flag1": map[string]interface{}{
256 "counters": json.RawMessage(`[{"count":1,"value":"dv","version":100}]`),
257 "contextKinds": []string{"user"},
258 "default": "dv",
259 },
260 },
261 }))
262
263 es3 := newEventSummarizer()
264 event3 := withoutReasons.NewUnknownFlagEvaluationData("flagkey", user,
265 ldvalue.String("dv"), ldreason.EvaluationReason{})
266 es3.summarizeEvent(event3)
267 verifySummaryEventOutput(t, formatter, es3.snapshot(),
268 m.JSONEqual(map[string]interface{}{
269 "kind": "summary",
270 "startDate": fakeTime,
271 "endDate": fakeTime,
272 "features": map[string]interface{}{
273 "flagkey": map[string]interface{}{
274 "counters": json.RawMessage(`[{"count":1,"value":"dv","unknown":true}]`),
275 "contextKinds": []string{"user"},
276 "default": "dv",
277 },
278 },
279 }))
280 })
281
282 t.Run("summary - multiple counters", func(t *testing.T) {
283 es := newEventSummarizer()
284 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason),
285 false, flag1Default, ""))
286 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("b"), 2, noReason),
287 false, flag1Default, ""))
288 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason),
289 false, flag1Default, ""))
290 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v2, user, ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason),
291 false, flag1Default, ""))
292 es.summarizeEvent(withoutReasons.NewEvaluationData(flag2, user, ldreason.NewEvaluationDetail(ldvalue.String("c"), 3, noReason),
293 false, flag2Default, ""))
294
295 bytes, count := formatter.makeOutputEvents(nil, es.snapshot())
296 require.Equal(t, 1, count)
297
298
299
300 m.In(t).Assert(bytes, m.JSONArray().Should(m.Items(
301 m.MapOf(
302 m.KV("kind", m.Equal("summary")),
303 m.KV("startDate", m.Not(m.BeNil())),
304 m.KV("endDate", m.Not(m.BeNil())),
305 m.KV("features", m.MapOf(
306 m.KV("flag1", m.MapOf(
307 m.KV("default", m.JSONEqual(flag1Default)),
308 m.KV("counters", m.ItemsInAnyOrder(
309 m.JSONStrEqual(`{"version":100,"variation":1,"value":"a","count":2}`),
310 m.JSONStrEqual(`{"version":100,"variation":2,"value":"b","count":1}`),
311 m.JSONStrEqual(`{"version":200,"variation":1,"value":"a","count":1}`),
312 )),
313 m.KV("contextKinds", m.Items(m.Equal("user"))),
314 )),
315 m.KV("flag2", m.MapOf(
316 m.KV("default", m.JSONEqual(flag2Default)),
317 m.KV("counters", m.ItemsInAnyOrder(
318 m.JSONStrEqual(`{"version":1,"variation":3,"value":"c","count":1}`),
319 )),
320 m.KV("contextKinds", m.Items(m.Equal("user"))),
321 )),
322 )),
323 ),
324 )))
325 })
326
327 t.Run("summary with multiple context kinds", func(t *testing.T) {
328 context1, context2, context3 := ldcontext.New("userkey1"), ldcontext.New("userkey2"), ldcontext.NewWithKind("org", "orgkey")
329
330 es := newEventSummarizer()
331 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, Context(context1), ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason),
332 false, flag1Default, ""))
333 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, Context(context2), ldreason.NewEvaluationDetail(ldvalue.String("b"), 2, noReason),
334 false, flag1Default, ""))
335 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, Context(context3), ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason),
336 false, flag1Default, ""))
337 es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v2, Context(context1), ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason),
338 false, flag1Default, ""))
339 es.summarizeEvent(withoutReasons.NewEvaluationData(flag2, Context(context1), ldreason.NewEvaluationDetail(ldvalue.String("c"), 3, noReason),
340 false, flag2Default, ""))
341
342 bytes, count := formatter.makeOutputEvents(nil, es.snapshot())
343 require.Equal(t, 1, count)
344
345 m.In(t).Assert(bytes, m.JSONArray().Should(m.Items(
346 m.MapOf(
347 m.KV("kind", m.Equal("summary")),
348 m.KV("startDate", m.Not(m.BeNil())),
349 m.KV("endDate", m.Not(m.BeNil())),
350 m.KV("features", m.MapOf(
351 m.KV("flag1", m.MapOf(
352 m.KV("default", m.JSONEqual(flag1Default)),
353 m.KV("counters", m.Length().Should(m.Equal(3))),
354 m.KV("contextKinds", m.ItemsInAnyOrder(m.Equal("user"), m.Equal("org"))),
355 )),
356 m.KV("flag2", m.MapOf(
357 m.KV("default", m.JSONEqual(flag2Default)),
358 m.KV("counters", m.Length().Should(m.Equal(1))),
359 m.KV("contextKinds", m.Items(m.Equal("user"))),
360 )),
361 )),
362 ),
363 )))
364 })
365
366 t.Run("empty payload", func(t *testing.T) {
367 bytes, count := formatter.makeOutputEvents([]anyEventOutput{}, eventSummary{})
368 assert.Nil(t, bytes)
369 assert.Equal(t, 0, count)
370 })
371 }
372
373 func verifyEventOutput(t *testing.T, formatter eventOutputFormatter, event anyEventInput, jsonMatcher m.Matcher) {
374 t.Helper()
375 bytes, count := formatter.makeOutputEvents([]anyEventOutput{event}, eventSummary{})
376 require.Equal(t, 1, count)
377 m.In(t).Assert(bytes, m.JSONArray().Should(m.Items(jsonMatcher)))
378 }
379
380 func verifySummaryEventOutput(t *testing.T, formatter eventOutputFormatter, summary eventSummary, jsonMatcher m.Matcher) {
381 t.Helper()
382 bytes, count := formatter.makeOutputEvents(nil, summary)
383 require.Equal(t, 1, count)
384 m.In(t).Assert(bytes, m.JSONArray().Should(m.Items(jsonMatcher)))
385 }
386
View as plain text