From be924a7164b0cae6f86c7e0444016083cc4d842e Mon Sep 17 00:00:00 2001 From: TiganeteaRobert Date: Wed, 13 Apr 2022 10:17:01 +0300 Subject: [PATCH] Extend Get* methods to allow getting fields from self-describing data (close #9) --- analytics/transform.go | 82 ++++++++++++++++++++++++++++++++++--- analytics/transform_test.go | 38 ++++++++++++++++- analytics/vars_test.go | 9 ++-- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/analytics/transform.go b/analytics/transform.go index c361e0b..8666262 100644 --- a/analytics/transform.go +++ b/analytics/transform.go @@ -22,7 +22,10 @@ import ( "github.com/pkg/errors" ) -const eventLength int = 131 +const ( + eventLength int = 131 + EmptyFieldErr string = `Field is empty` +) type KeyVal struct { Key string @@ -179,10 +182,8 @@ func (event ParsedEvent) ToJsonWithGeo() ([]byte, error) { return jsonified, nil } -// GetValue returns the value for a provided atomic field, without processing the rest of the event. -// For unstruct_event, it returns a map of only the data for the unstruct event. -func (event ParsedEvent) GetValue(field string) (interface{}, error) { - +// getParsedValue gets a field's value from an event after parsing it with its specific ParseFunction +func (event ParsedEvent) getParsedValue(field string) ([]KeyVal, error) { if len(event) != eventLength { return nil, errors.New(fmt.Sprintf("Cannot get value - wrong number of fields provided: %v", len(event))) } @@ -191,12 +192,24 @@ func (event ParsedEvent) GetValue(field string) (interface{}, error) { return nil, errors.New(fmt.Sprintf("Key %s not a valid atomic field", field)) } if event[index] == "" { - return nil, errors.New(fmt.Sprintf("Field %s is empty", field)) + return nil, errors.New(EmptyFieldErr) } kvPairs, err := enrichedEventFieldTypes[index].ParseFunction(enrichedEventFieldTypes[index].Key, event[index]) if err != nil { return nil, err } + + return kvPairs, nil +} + +// GetValue returns the value for a provided atomic field, without processing the rest of the event. +// For unstruct_event, it returns a map of only the data for the unstruct event. +func (event ParsedEvent) GetValue(field string) (interface{}, error) { + kvPairs, err := event.getParsedValue(field) + if err != nil { + return nil, err + } + if field == "contexts" || field == "derived_contexts" || field == "unstruct_event" { // TODO: DRY HERE TOO? output := make(map[string]interface{}) @@ -205,8 +218,65 @@ func (event ParsedEvent) GetValue(field string) (interface{}, error) { } return output, nil } + return kvPairs[0].Value, nil +} + +// GetUnstructEventValue returns the value for a provided atomic field inside an event's unstruct_event field +func (event ParsedEvent) GetUnstructEventValue(path ...interface{}) (interface{}, error) { + fullPath := append([]interface{}{`data`, `data`}, path...) + + el := json.Get([]byte(event[indexMap["unstruct_event"]]), fullPath...) + return el.GetInterface(), el.LastError() +} + +// GetContextValue returns the value for a provided atomic field inside an event's contexts or derived_contexts +func (event ParsedEvent) GetContextValue(contextName string, path ...interface{}) (interface{}, error) { + contextNames := []string{`contexts`, `derived_contexts`} + var contexts []interface{} + for _, c := range contextNames { + kvPairs, err := event.getParsedValue(c) + if err != nil && err.Error() != EmptyFieldErr { + return nil, err + } + // extract the key/value pairs of the event path into a map + eventMap := make(map[string]interface{}) + for _, pair := range kvPairs { + eventMap[pair.Key] = pair.Value + } + contexts = append(contexts, eventMap) + } + + var output []interface{} + b := make([]interface{}, len(path)) + for idx := range path { + b[idx] = path[idx] + } + // iterate through all contextNames and extract the requested path to the output slice + for _, ctx := range contexts { + for key, contextSlice := range ctx.(map[string]interface{}) { + if key == contextName { + for _, ctxValues := range contextSlice.([]interface{}) { + ctxValuesMap := ctxValues.(map[string]interface{}) + // output whole context if path is not defined + if len(path) == 0 { + output = append(output, ctxValuesMap) + continue + } + j, err := json.Marshal(ctxValuesMap) + if err != nil { + return nil, err + } + el := json.Get(j, b...) + if el.LastError() == nil { + output = append(output, el.GetInterface()) + } + } + } + } + } + return output, nil } // GetSubsetMap returns a map of a subset of the event, containing only the atomic fields provided, without processing the rest of the event. diff --git a/analytics/transform_test.go b/analytics/transform_test.go index 0f645cb..60f5927 100644 --- a/analytics/transform_test.go +++ b/analytics/transform_test.go @@ -302,7 +302,7 @@ func TestGetValue(t *testing.T) { // correct value contexts contextsValue, err := fullEvent.GetValue("contexts") assert.Nil(err) - assert.Equal(contextsMap, contextsValue) + assert.Equal(multipleContextsMap, contextsValue) // incorrect field name failureValue, err := fullEvent.GetValue("not_a_field") @@ -315,6 +315,42 @@ func TestGetValue(t *testing.T) { assert.NotNil(err) } +func TestGetUnstructEventValue(t *testing.T) { + assert := assert.New(t) + + // correct value unstruct field + unstructValue, err := fullEvent.GetUnstructEventValue(`elementClasses`, 0) + assert.Nil(err) + assert.Equal(`foreground`, unstructValue) + + unstructValue, err = fullEvent.GetUnstructEventValue(`elementClassesBoo`, 0) + assert.NotNil(err) + assert.Nil(unstructValue) + + unstructValue, err = fullEvent.GetUnstructEventValue(`elementId`) + assert.Nil(err) + assert.Equal(`exampleLink`, unstructValue) +} + +func TestGetContextValue(t *testing.T) { + assert := assert.New(t) + + // correct value contexts + contextsValue, err := fullEvent.GetContextValue(`contexts_org_schema_web_page_1`, "breadcrumb", 0) + assert.Nil(err) + assert.Equal(contextsArray, contextsValue) + + // correct value contexts + contextsValue, err = fullEvent.GetContextValue(`contexts_org_schema_web_page_1`) + assert.Nil(err) + assert.Equal(wholeContextMap, contextsValue) + + // correct value contexts + contextsValue, err = fullEvent.GetContextValue(`contexts_org_schema_web_page_1`, "breadcrumb", 3) + assert.Nil(err) + assert.Equal([]interface{}(nil), contextsValue) +} + func BenchmarkGetValue(b *testing.B) { for i := 0; i < b.N; i++ { fullEvent.GetValue("app_id") diff --git a/analytics/vars_test.go b/analytics/vars_test.go index dd012df..8916465 100644 --- a/analytics/vars_test.go +++ b/analytics/vars_test.go @@ -285,10 +285,11 @@ var eventMapWithoutGeo = copyWithoutGeo(eventMapWithGeo) var unstructMap = map[string]interface{}{"unstruct_event_com_snowplowanalytics_snowplow_link_click_1": eventMapWithGeo["unstruct_event_com_snowplowanalytics_snowplow_link_click_1"]} -var contextsMap = map[string]interface{}{ - "contexts_org_w3_performance_timing_1": eventMapWithGeo["contexts_org_w3_performance_timing_1"], - "contexts_org_schema_web_page_1": eventMapWithGeo["contexts_org_schema_web_page_1"], -} +var contextsArray = []interface{}{"blog"} + +var multipleContextsMap = map[string]interface{}{"contexts_org_schema_web_page_1": []interface{}{map[string]interface{}{"author": "Fred Blundun", "breadcrumb": []interface{}{"blog", "releases"}, "datePublished": "2014-11-06T00:00:00Z", "genre": "blog", "inLanguage": "en-US", "keywords": []interface{}{"snowplow", "javascript", "tracker", "event"}}}, "contexts_org_w3_performance_timing_1": []interface{}{map[string]interface{}{"connectEnd": 1.415358090183e+12, "connectStart": 1.415358090103e+12, "domComplete": 0.0, "domContentLoadedEventEnd": 1.415358091309e+12, "domContentLoadedEventStart": 1.415358090968e+12, "domInteractive": 1.415358090886e+12, "domLoading": 1.41535809027e+12, "domainLookupEnd": 1.415358090102e+12, "domainLookupStart": 1.415358090102e+12, "fetchStart": 1.41535808987e+12, "loadEventEnd": 0.0, "loadEventStart": 0.0, "navigationStart": 1.415358089861e+12, "redirectEnd": 0.0, "redirectStart": 0.0, "requestStart": 1.415358090183e+12, "responseEnd": 1.415358090265e+12, "responseStart": 1.415358090265e+12, "unloadEventEnd": 1.415358090287e+12, "unloadEventStart": 1.41535809027e+12}}} + +var wholeContextMap = []interface{}{map[string]interface{}{"author": "Fred Blundun", "breadcrumb": []interface{}{"blog", "releases"}, "datePublished": "2014-11-06T00:00:00Z", "genre": "blog", "inLanguage": "en-US", "keywords": []interface{}{"snowplow", "javascript", "tracker", "event"}}} var subsetMap = map[string]interface{}{ "app_id": eventMapWithGeo["app_id"],