Skip to content

Commit 74474c9

Browse files
authored
feat(event-manager): Adds implementation for ODPEventManager (#354)
This PR adds support for ODPEventManager.
1 parent c99e8dd commit 74474c9

21 files changed

+1426
-215
lines changed

pkg/config/polling_manager.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020,2022 Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -199,8 +199,8 @@ func (cm *PollingProjectConfigManager) Start(ctx context.Context) {
199199

200200
func (cm *PollingProjectConfigManager) setAuthHeaderIfDatafileAccessTokenPresent() {
201201
if cm.datafileAccessToken != "" {
202-
headers := []utils.Header{{Name: "Content-Type", Value: "application/json"}, {Name: "Accept", Value: "application/json"}}
203-
headers = append(headers, utils.Header{Name: "Authorization", Value: "Bearer " + cm.datafileAccessToken})
202+
headers := []utils.Header{{Name: utils.HeaderContentType, Value: utils.ContentTypeJSON}, {Name: utils.HeaderAccept, Value: utils.ContentTypeJSON}}
203+
headers = append(headers, utils.Header{Name: utils.HeaderAuthorization, Value: "Bearer " + cm.datafileAccessToken})
204204
cm.requester = utils.NewHTTPRequester(logging.GetLogger(cm.sdkKey, "HTTPRequester"), utils.Headers(headers...))
205205
}
206206
}

pkg/event/factory.go

Lines changed: 1 addition & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ package event
1919

2020
import (
2121
"errors"
22-
"fmt"
23-
"math"
24-
"reflect"
2522
"strings"
2623
"time"
2724

@@ -246,7 +243,7 @@ func getEventAttributes(projectConfig config.ProjectConfig, attributes map[strin
246243
var eventAttributes = []VisitorAttribute{}
247244

248245
for key, value := range attributes {
249-
if !isValidAttribute(value) {
246+
if !utils.IsValidAttribute(value) {
250247
continue
251248
}
252249
visitorAttribute := VisitorAttribute{}
@@ -294,77 +291,3 @@ func getTagValue(eventTags map[string]interface{}) (float64, error) {
294291

295292
return 0, errors.New("no event tag found for value")
296293
}
297-
298-
// Validates if the type of provided value is numeric.
299-
func isNumericType(value interface{}) (float64, error) {
300-
switch i := value.(type) {
301-
case int:
302-
return float64(i), nil
303-
case int8:
304-
return float64(i), nil
305-
case int16:
306-
return float64(i), nil
307-
case int32:
308-
return float64(i), nil
309-
case int64:
310-
return float64(i), nil
311-
case uint:
312-
return float64(i), nil
313-
case uint8:
314-
return float64(i), nil
315-
case uint16:
316-
return float64(i), nil
317-
case uint32:
318-
return float64(i), nil
319-
case uint64:
320-
return float64(i), nil
321-
case uintptr:
322-
return float64(i), nil
323-
case float32:
324-
return float64(i), nil
325-
case float64:
326-
return i, nil
327-
default:
328-
v := reflect.ValueOf(value)
329-
v = reflect.Indirect(v)
330-
return math.NaN(), fmt.Errorf("can't convert %v to float64", v.Type())
331-
}
332-
}
333-
334-
// Validates if the provided value is a valid numeric value.
335-
func isValidNumericValue(value interface{}) bool {
336-
if floatValue, err := isNumericType(value); err == nil {
337-
if math.IsNaN(floatValue) {
338-
return false
339-
}
340-
if math.IsInf(floatValue, 1) {
341-
return false
342-
}
343-
if math.IsInf(floatValue, -1) {
344-
return false
345-
}
346-
if math.IsInf(floatValue, 0) {
347-
return false
348-
}
349-
if math.Abs(floatValue) > math.Pow(2, 53) {
350-
return false
351-
}
352-
return true
353-
}
354-
return false
355-
}
356-
357-
// check if attribute value is valid
358-
func isValidAttribute(value interface{}) bool {
359-
if value == nil {
360-
return false
361-
}
362-
363-
switch value.(type) {
364-
// https://go.dev/tour/basics/11
365-
case bool, string:
366-
return true
367-
default:
368-
return isValidNumericValue(value)
369-
}
370-
}

pkg/event/factory_test.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package event
1919

2020
import (
2121
"context"
22-
"math"
2322
"math/rand"
2423
"testing"
2524
"time"
@@ -242,43 +241,3 @@ func TestCreateImpressionUserEvent(t *testing.T) {
242241
}
243242
}
244243
}
245-
246-
func TestIsValidAttribute(t *testing.T) {
247-
assert.False(t, isValidAttribute(nil))
248-
assert.False(t, isValidAttribute(map[string]interface{}{}))
249-
assert.False(t, isValidAttribute([]string{}))
250-
assert.False(t, isValidAttribute([]interface{}{}))
251-
assert.False(t, isValidAttribute(make(chan int)))
252-
assert.False(t, isValidAttribute(complex64(1234.1231)))
253-
assert.False(t, isValidAttribute(complex128(123446.123123)))
254-
assert.False(t, isValidAttribute(math.Pow(2, 54)))
255-
256-
assert.False(t, isValidAttribute(math.NaN()))
257-
posInf := math.Inf(1)
258-
assert.False(t, isValidAttribute(posInf))
259-
posInf += 12.2 // infinite value will still propagate after add operation
260-
assert.False(t, isValidAttribute(posInf))
261-
negInf := math.Inf(-1)
262-
assert.False(t, isValidAttribute(negInf))
263-
negInf /= negInf // will turn infinite into NaN
264-
assert.False(t, isValidAttribute(negInf))
265-
assert.False(t, isValidAttribute(math.Inf(0)))
266-
267-
assert.True(t, isValidAttribute(bool(true)))
268-
assert.True(t, isValidAttribute(string("abcd")))
269-
assert.True(t, isValidAttribute(int(1)))
270-
assert.True(t, isValidAttribute(int8(-12)))
271-
assert.True(t, isValidAttribute(int16(-123)))
272-
assert.True(t, isValidAttribute(int32(1234)))
273-
assert.True(t, isValidAttribute(int64(123446)))
274-
assert.True(t, isValidAttribute(uint(1)))
275-
assert.True(t, isValidAttribute(uint8(12)))
276-
assert.True(t, isValidAttribute(uint16(123)))
277-
assert.True(t, isValidAttribute(uint32(1234)))
278-
assert.True(t, isValidAttribute(uint64(123446)))
279-
assert.True(t, isValidAttribute(uintptr(1)))
280-
assert.True(t, isValidAttribute(float32(12.11)))
281-
assert.True(t, isValidAttribute(float64(123.1231)))
282-
assert.True(t, isValidAttribute(byte(134)))
283-
assert.True(t, isValidAttribute(rune(123446)))
284-
}

pkg/odp/utils/utils.go renamed to pkg/odp/audience.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,16 @@
1414
* limitations under the License. *
1515
***************************************************************************/
1616

17-
// Package utils //
18-
package utils
17+
// Package odp //
18+
package odp
1919

20-
// Equal determines if two string slices are equal
21-
func Equal(a, b []string) bool {
22-
if len(a) != len(b) {
23-
return false
24-
}
25-
for i, v := range a {
26-
if v != b[i] {
27-
return false
28-
}
29-
}
30-
return true
20+
// Audience represents an ODP Audience
21+
type Audience struct {
22+
Name string `json:"name"`
23+
State string `json:"state"`
24+
Description string `json:"description,omitempty"`
3125
}
3226

33-
// MakeCacheKey creates and returns cacheKey
34-
func MakeCacheKey(userKey, userValue string) string {
35-
return userKey + "-$-" + userValue
27+
func (s Audience) isQualified() bool {
28+
return s.State == "qualified"
3629
}

pkg/odp/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package odp
2020
import (
2121
"sync"
2222

23-
"github.com/optimizely/go-sdk/pkg/odp/utils"
23+
"github.com/optimizely/go-sdk/pkg/utils"
2424
)
2525

2626
// Config is used to represent odp config
@@ -53,7 +53,7 @@ func (s *DefaultConfig) Update(apiKey, apiHost string, segmentsToCheck []string)
5353
s.lock.Lock()
5454
defer s.lock.Unlock()
5555

56-
if s.apiKey == apiKey && s.apiHost == apiHost && utils.Equal(s.segmentsToCheck, segmentsToCheck) {
56+
if s.apiKey == apiKey && s.apiHost == apiHost && utils.CompareSlices(s.segmentsToCheck, segmentsToCheck) {
5757
return false
5858
}
5959
s.apiKey = apiKey

pkg/odp/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ package odp
1919

2020
const invalidSegmentIdentifier = "audience segments fetch failed (invalid identifier)"
2121
const fetchSegmentsFailedError = "audience segments fetch failed (%s)"
22+
const odpEventFailed = "ODP event send failed (%s)"
23+
const odpInvalidData = "ODP data is not valid"

pkg/odp/event.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/****************************************************************************
2+
* Copyright 2022, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package odp //
18+
package odp
19+
20+
// Event represents a event to be sent and stored in the Optimizely Data Platform
21+
type Event struct {
22+
Type string `json:"type"`
23+
Action string `json:"action"`
24+
Identifiers map[string]string `json:"identifiers"`
25+
Data map[string]interface{} `json:"data"`
26+
}

pkg/odp/event_api_manager.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/****************************************************************************
2+
* Copyright 2022, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package odp //
18+
package odp
19+
20+
import (
21+
"fmt"
22+
"net/url"
23+
24+
"github.com/optimizely/go-sdk/pkg/logging"
25+
"github.com/optimizely/go-sdk/pkg/utils"
26+
)
27+
28+
const eventsAPIEndpointPath = "/v3/events"
29+
30+
// EventAPIManager represents the event API manager.
31+
type EventAPIManager interface {
32+
SendODPEvents(config Config, events []Event) (canRetry bool, err error)
33+
}
34+
35+
// ODP REST Events API
36+
// - https://api.zaius.com/v3/events
37+
// - test ODP public API key = "W4WzcEs-ABgXorzY7h1LCQ"
38+
/*
39+
[Event Request]
40+
curl -i -H 'Content-Type: application/json' -H 'x-api-key: W4WzcEs-ABgXorzY7h1LCQ' -X POST -d '{"type":"fullstack","action":"identified","identifiers":{"fs_user_id": "abc"},"data":{"idempotence_id":"xyz","source":"go-sdk","data_source_type":"sdk","data_source_version":"1.8.3"}}' https://api.zaius.com/v3/events
41+
[Event Response]
42+
{"title":"Accepted","status":202,"timestamp":"2022-06-30T20:59:52.046Z"}
43+
*/
44+
45+
// DefaultEventAPIManager represents default implementation of Event API Manager
46+
type DefaultEventAPIManager struct {
47+
requester utils.Requester
48+
}
49+
50+
// NewEventAPIManager creates and returns a new instance of DefaultEventAPIManager.
51+
func NewEventAPIManager(sdkKey string, requester utils.Requester) *DefaultEventAPIManager {
52+
if requester == nil {
53+
requester = utils.NewHTTPRequester(logging.GetLogger(sdkKey, "EventAPIManager"))
54+
}
55+
return &DefaultEventAPIManager{requester: requester}
56+
}
57+
58+
// SendODPEvents sends events to ODP's RESTful API
59+
func (s *DefaultEventAPIManager) SendODPEvents(config Config, events []Event) (canRetry bool, err error) {
60+
61+
// Creating request
62+
apiEndpoint, err := url.ParseRequestURI(fmt.Sprintf("%s%s", config.GetAPIHost(), eventsAPIEndpointPath))
63+
if err != nil {
64+
return false, fmt.Errorf(odpEventFailed, err.Error())
65+
}
66+
headers := []utils.Header{{Name: utils.HeaderContentType, Value: utils.ContentTypeJSON}, {Name: ODPAPIKeyHeader, Value: config.GetAPIKey()}}
67+
68+
_, _, status, err := s.requester.Post(apiEndpoint.String(), events, headers...)
69+
// handling edge cases
70+
if err == nil {
71+
return false, nil
72+
}
73+
if status >= 400 && status < 500 { // no retry (client error)
74+
return false, fmt.Errorf(odpEventFailed, err.Error())
75+
}
76+
return true, fmt.Errorf(odpEventFailed, err.Error())
77+
}

0 commit comments

Comments
 (0)