From eff8bae25541e73832a4d7569d4080720e712e54 Mon Sep 17 00:00:00 2001 From: Andreas Andersen Date: Mon, 29 Feb 2016 12:06:45 +0100 Subject: [PATCH 1/6] Converted structs to interfaces --- example_test.go | 26 ++++++---------------- mixpanel.go | 56 ++++++++++++++++++++++++++++++++++-------------- mixpanel_test.go | 5 ++--- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/example_test.go b/example_test.go index 7ee432c..d9ba087 100644 --- a/example_test.go +++ b/example_test.go @@ -4,27 +4,21 @@ import ( "time" ) -func ExampleNewMixpanel() { - NewMixpanel("mytoken") +func ExampleNew() { + New("mytoken", "") } -func ExampleMixpanel_Identify() { - client := NewMixpanel("mytoken") - - client.Identify("1") -} - -func ExampleTrack() { - client := NewMixpanel("mytoken") +func ExampleMixpanel() { + client := New("mytoken", "") client.Track("1", "Sign Up", map[string]interface{}{ "from": "email", }) } -func ExamplePeople_Update() { - var people *People - client := NewMixpanel("mytoken") +func ExamplePeople() { + var people People + client := New("mytoken", "") people = client.Identify("1") people.Update("$set", map[string]interface{}{ @@ -33,13 +27,7 @@ func ExamplePeople_Update() { "$created": time.Now().String(), "custom_field": "cool!", }) -} - -func ExamplePeople_Track() { - var people *People - client := NewMixpanel("mytoken") - people = client.Identify("1") people.Track("Sign Up", map[string]interface{}{ "from": "email", }) diff --git a/mixpanel.go b/mixpanel.go index 4bbf31b..4c0f6fa 100644 --- a/mixpanel.go +++ b/mixpanel.go @@ -7,14 +7,33 @@ import ( ) // The Mixapanel struct store the mixpanel endpoint and the project token -type Mixpanel struct { +type Mixpanel interface { + // Track create a events to current distinct id + Track(distinctId string, eventName string, properties map[string]interface{}) (*http.Response, error) + + // Identify call mixpanel 'engage' and returns People instance + Identify(id string) People +} + +// The Mixapanel struct store the mixpanel endpoint and the project token +type mixpanel struct { Token string ApiURL string } // People represents a consumer, and is used on People Analytics -type People struct { - m *Mixpanel +type People interface { + // Track create a events to current people + Track(eventName string, properties map[string]interface{}) (*http.Response, error) + + // Create a Update Operation to current people, see + // https://mixpanel.com/help/reference/http + Update(operation string, updateParams map[string]interface{}) (*http.Response, error) +} + +// People represents a consumer, and is used on People Analytics +type people struct { + m *mixpanel id string } @@ -24,7 +43,7 @@ type trackParams struct { } // Track create a events to current distinct id -func (m *Mixpanel) Track(distinctId string, eventName string, +func (m *mixpanel) Track(distinctId string, eventName string, properties map[string]interface{}) (*http.Response, error) { params := trackParams{Event: eventName} @@ -40,19 +59,20 @@ func (m *Mixpanel) Track(distinctId string, eventName string, } // Identify call mixpanel 'engage' and returns People instance -func (m *Mixpanel) Identify(id string) *People { +func (m *mixpanel) Identify(id string) People { params := map[string]interface{}{"$token": m.Token, "$distinct_id": id} m.send("engage", params) - return &People{m: m, id: id} + return &people{m: m, id: id} } // Track create a events to current people -func (p *People) Track(eventName string, properties map[string]interface{}) (*http.Response, error) { +func (p *people) Track(eventName string, properties map[string]interface{}) (*http.Response, error) { return p.m.Track(p.id, eventName, properties) } -// Create a Update Operation to current people, see https://mixpanel.com/help/reference/http -func (p *People) Update(operation string, updateParams map[string]interface{}) (*http.Response, error) { +// Create a Update Operation to current people, see +// https://mixpanel.com/help/reference/http +func (p *people) Update(operation string, updateParams map[string]interface{}) (*http.Response, error) { params := map[string]interface{}{ "$token": p.m.Token, "$distinct_id": p.id, @@ -61,13 +81,12 @@ func (p *People) Update(operation string, updateParams map[string]interface{}) ( return p.m.send("engage", params) } -func (m *Mixpanel) to64(data string) string { +func (m *mixpanel) to64(data string) string { bytes := []byte(data) return base64.StdEncoding.EncodeToString(bytes) } -func (m *Mixpanel) send(eventType string, params interface{}) (*http.Response, error) { - +func (m *mixpanel) send(eventType string, params interface{}) (*http.Response, error) { dataJSON, _ := json.Marshal(params) data := string(dataJSON) @@ -75,10 +94,15 @@ func (m *Mixpanel) send(eventType string, params interface{}) (*http.Response, e return http.Get(url) } -// NewMixpanel returns the client instance -func NewMixpanel(token string) *Mixpanel { - return &Mixpanel{ +// New returns the client instance. If apiURL is blank, the default will be used +// ("https://api.mixpanel.com"). +func New(token, apiURL string) Mixpanel { + if apiURL == "" { + apiURL = "https://api.mixpanel.com" + } + + return &mixpanel{ Token: token, - ApiURL: "https://api.mixpanel.com", + ApiURL: apiURL, } } diff --git a/mixpanel_test.go b/mixpanel_test.go index 16990ac..fb2e324 100644 --- a/mixpanel_test.go +++ b/mixpanel_test.go @@ -11,7 +11,7 @@ import ( var ( ts *httptest.Server - client *Mixpanel + client Mixpanel LastRequest *http.Request ) @@ -21,8 +21,7 @@ func setup() { LastRequest = r })) - client = NewMixpanel("e3bc4100330c35722740fb8c6f5abddc") - client.ApiURL = ts.URL + client = New("e3bc4100330c35722740fb8c6f5abddc", ts.URL) } func teardown() { From 6003453e766a283f86f6c5344296fe320de917ae Mon Sep 17 00:00:00 2001 From: Andreas Andersen Date: Mon, 29 Feb 2016 12:40:56 +0100 Subject: [PATCH 2/6] Using a single error for returns --- mixpanel.go | 37 ++++++++++++++++++++++++++++--------- mixpanel_test.go | 19 +++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/mixpanel.go b/mixpanel.go index 4c0f6fa..e7e5df3 100644 --- a/mixpanel.go +++ b/mixpanel.go @@ -3,13 +3,20 @@ package mixpanel import ( "encoding/base64" "encoding/json" + "errors" + "fmt" + "io/ioutil" "net/http" ) +var ( + ErrTrackFailed = errors.New("Mixpanel did not return 1 when tracking") +) + // The Mixapanel struct store the mixpanel endpoint and the project token type Mixpanel interface { // Track create a events to current distinct id - Track(distinctId string, eventName string, properties map[string]interface{}) (*http.Response, error) + Track(distinctId string, eventName string, properties map[string]interface{}) error // Identify call mixpanel 'engage' and returns People instance Identify(id string) People @@ -24,11 +31,11 @@ type mixpanel struct { // People represents a consumer, and is used on People Analytics type People interface { // Track create a events to current people - Track(eventName string, properties map[string]interface{}) (*http.Response, error) + Track(eventName string, properties map[string]interface{}) error // Create a Update Operation to current people, see // https://mixpanel.com/help/reference/http - Update(operation string, updateParams map[string]interface{}) (*http.Response, error) + Update(operation string, updateParams map[string]interface{}) error } // People represents a consumer, and is used on People Analytics @@ -43,8 +50,7 @@ type trackParams struct { } // Track create a events to current distinct id -func (m *mixpanel) Track(distinctId string, eventName string, - properties map[string]interface{}) (*http.Response, error) { +func (m *mixpanel) Track(distinctId string, eventName string, properties map[string]interface{}) error { params := trackParams{Event: eventName} params.Properties = make(map[string]interface{}, 0) @@ -66,13 +72,13 @@ func (m *mixpanel) Identify(id string) People { } // Track create a events to current people -func (p *people) Track(eventName string, properties map[string]interface{}) (*http.Response, error) { +func (p *people) Track(eventName string, properties map[string]interface{}) error { return p.m.Track(p.id, eventName, properties) } // Create a Update Operation to current people, see // https://mixpanel.com/help/reference/http -func (p *people) Update(operation string, updateParams map[string]interface{}) (*http.Response, error) { +func (p *people) Update(operation string, updateParams map[string]interface{}) error { params := map[string]interface{}{ "$token": p.m.Token, "$distinct_id": p.id, @@ -86,12 +92,25 @@ func (m *mixpanel) to64(data string) string { return base64.StdEncoding.EncodeToString(bytes) } -func (m *mixpanel) send(eventType string, params interface{}) (*http.Response, error) { +func (m *mixpanel) send(eventType string, params interface{}) error { dataJSON, _ := json.Marshal(params) data := string(dataJSON) url := m.ApiURL + "/" + eventType + "?data=" + m.to64(data) - return http.Get(url) + if resp, err := http.Get(url); err != nil { + return fmt.Errorf("mixpanel: %s", err.Error()) + } else { + defer resp.Body.Close() + body, bodyErr := ioutil.ReadAll(resp.Body) + if bodyErr != nil { + return fmt.Errorf("mixpanel: %s", bodyErr.Error()) + } + if string(body) != "1" && string(body) != "1\n" { + return ErrTrackFailed + } + } + + return nil } // New returns the client instance. If apiURL is blank, the default will be used diff --git a/mixpanel_test.go b/mixpanel_test.go index fb2e324..429bfac 100644 --- a/mixpanel_test.go +++ b/mixpanel_test.go @@ -18,6 +18,7 @@ var ( func setup() { ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) + w.Write([]byte("1\n")) LastRequest = r })) @@ -132,3 +133,21 @@ func TestPeopleTrack(t *testing.T) { path, want) } } + +func TestError(t *testing.T) { + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("0\n")) + LastRequest = r + })) + + client = New("e3bc4100330c35722740fb8c6f5abddc", ts.URL) + + if err := client.Track("1", "name", nil); err != ErrTrackFailed { + t.Error("Got bad error for track", err) + } + + if err := client.Identify("1").Track("name", nil); err != ErrTrackFailed { + t.Error("Got bad error for track", err) + } +} From ae2195682dd40b9388de864302e1afbb34b9c723 Mon Sep 17 00:00:00 2001 From: Andreas Andersen Date: Mon, 29 Feb 2016 12:41:23 +0100 Subject: [PATCH 3/6] Added mocked implementation --- mock.go | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ mock_test.go | 29 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 mock.go create mode 100644 mock_test.go diff --git a/mock.go b/mock.go new file mode 100644 index 0000000..f092b3c --- /dev/null +++ b/mock.go @@ -0,0 +1,87 @@ +package mixpanel + +import ( + "errors" + "fmt" +) + +// Mocked version of Mixpanel which can be used in unit tests. +type Mock struct { + // All People identified, mapped by distinctId + People map[string]*MockPeople +} + +func NewMock() *Mock { + return &Mock{ + People: map[string]*MockPeople{}, + } +} + +func (m *Mock) String() string { + str := "" + for id, p := range m.People { + str += id + ":\n" + p.String() + } + return str +} + +// Identifies a user. The user will be added to the People map. +func (m *Mock) Identify(distinctId string) People { + if p, ok := m.People[distinctId]; ok { + return p + } else { + p := &MockPeople{ + Properties: map[string]interface{}{}, + } + m.People[distinctId] = p + return p + } +} + +func (m *Mock) Track(distinctId string, eventName string, properties map[string]interface{}) error { + return m.Identify(distinctId).Track(eventName, properties) +} + +type MockPeople struct { + Properties map[string]interface{} + Events []MockEvent +} + +func (mp *MockPeople) String() string { + str := "" + str += " properties:\n" + for key, val := range mp.Properties { + str += fmt.Sprintf(" %s: %v\n", key, val) + } + str += " events:\n" + for _, event := range mp.Events { + str += " " + event.Name + ":\n" + for key, val := range event.Properties { + str += fmt.Sprintf(" %s: %v\n", key, val) + } + } + return str +} + +func (mp *MockPeople) Track(eventName string, properties map[string]interface{}) error { + mp.Events = append(mp.Events, MockEvent{eventName, properties}) + return nil +} + +func (mp *MockPeople) Update(operation string, updateParams map[string]interface{}) error { + switch operation { + case "$set": + for key, val := range updateParams { + mp.Properties[key] = val + } + default: + return errors.New("mixpanel.Mock only supports the $set operation") + } + + return nil +} + +type MockEvent struct { + Name string + Properties map[string]interface{} +} diff --git a/mock_test.go b/mock_test.go new file mode 100644 index 0000000..15fbb42 --- /dev/null +++ b/mock_test.go @@ -0,0 +1,29 @@ +package mixpanel + +import "fmt" + +var fullfillsInterface Mixpanel = &Mock{} + +func ExampleMock() { + var people People + client := NewMock() + + people = client.Identify("1") + people.Update("$set", map[string]interface{}{ + "custom_field": "cool!", + }) + + people.Track("Sign Up", map[string]interface{}{ + "from": "email", + }) + + fmt.Println(client) + + // Output: + // 1: + // properties: + // custom_field: cool! + // events: + // Sign Up: + // from: email +} From 774782fd8bb75e500c5248f14f58272126a388b7 Mon Sep 17 00:00:00 2001 From: Andreas Andersen Date: Mon, 29 Feb 2016 15:50:39 +0100 Subject: [PATCH 4/6] Updated examples --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1e632c4..0baaf42 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ import "github.com/dukex/mixpanel" Track ``` go -res, err := client.Track("13793", "Signed Up", map[string]interface{}{ - "Referred By": "Friend", +err := client.Track("13793", "Signed Up", map[string]interface{}{ + "Referred By": "Friend", }) ``` -- @@ -32,14 +32,14 @@ Identify and Update Operation ``` go people := client.Identify("13793") -res, err := people.Track(map[string]interface{}{ - "Buy": "133" +err := people.Track(map[string]interface{}{ + "Buy": "133" }) -res, err := people.Update("$set", map[string]interface{}{ - "Address": "1313 Mockingbird Lane", - "Birthday": "1948-01-01", - }) +err := people.Update("$set", map[string]interface{}{ + "Address": "1313 Mockingbird Lane", + "Birthday": "1948-01-01", +}) ``` ## License From a4b637b57394e56c8ea56ea386120f4c14b4880b Mon Sep 17 00:00:00 2001 From: Andreas Andersen Date: Wed, 2 Mar 2016 18:55:15 +0100 Subject: [PATCH 5/6] Added support for using a custom HTTP client --- mixpanel.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mixpanel.go b/mixpanel.go index e7e5df3..c28561e 100644 --- a/mixpanel.go +++ b/mixpanel.go @@ -24,6 +24,7 @@ type Mixpanel interface { // The Mixapanel struct store the mixpanel endpoint and the project token type mixpanel struct { + Client *http.Client Token string ApiURL string } @@ -97,7 +98,7 @@ func (m *mixpanel) send(eventType string, params interface{}) error { data := string(dataJSON) url := m.ApiURL + "/" + eventType + "?data=" + m.to64(data) - if resp, err := http.Get(url); err != nil { + if resp, err := m.Client.Get(url); err != nil { return fmt.Errorf("mixpanel: %s", err.Error()) } else { defer resp.Body.Close() @@ -116,11 +117,18 @@ func (m *mixpanel) send(eventType string, params interface{}) error { // New returns the client instance. If apiURL is blank, the default will be used // ("https://api.mixpanel.com"). func New(token, apiURL string) Mixpanel { + return NewFromClient(http.DefaultClient, token, apiURL) +} + +// Creates a client instance using the specified client instance. This is useful +// when using a proxy. +func NewFromClient(c *http.Client, token, apiURL string) Mixpanel { if apiURL == "" { apiURL = "https://api.mixpanel.com" } return &mixpanel{ + Client: c, Token: token, ApiURL: apiURL, } From cd949df8dfa55ecf3abe18479d9b79aa15da3450 Mon Sep 17 00:00:00 2001 From: Andreas Andersen Date: Fri, 4 Mar 2016 11:44:30 +0100 Subject: [PATCH 6/6] Removed the people object and added support for IP and timestamp --- example_test.go | 31 +++++++------ mixpanel.go | 111 ++++++++++++++++++++++++++++------------------- mixpanel_test.go | 49 +++++++-------------- mock.go | 63 +++++++++++++++++++-------- mock_test.go | 29 ++++++++++--- 5 files changed, 165 insertions(+), 118 deletions(-) diff --git a/example_test.go b/example_test.go index d9ba087..dafc882 100644 --- a/example_test.go +++ b/example_test.go @@ -1,8 +1,6 @@ package mixpanel -import ( - "time" -) +import "time" func ExampleNew() { New("mytoken", "") @@ -11,24 +9,29 @@ func ExampleNew() { func ExampleMixpanel() { client := New("mytoken", "") - client.Track("1", "Sign Up", map[string]interface{}{ - "from": "email", + client.Track("1", "Sign Up", &Event{ + Properties: map[string]interface{}{ + "from": "email", + }, }) } func ExamplePeople() { - var people People client := New("mytoken", "") - people = client.Identify("1") - people.Update("$set", map[string]interface{}{ - "$email": "user@email.com", - "$last_login": time.Now(), - "$created": time.Now().String(), - "custom_field": "cool!", + client.Update("1", &Update{ + Operation: "$set", + Properties: map[string]interface{}{ + "$email": "user@email.com", + "$last_login": time.Now(), + "$created": time.Now().String(), + "custom_field": "cool!", + }, }) - people.Track("Sign Up", map[string]interface{}{ - "from": "email", + client.Track("1", "Sign Up", &Event{ + Properties: map[string]interface{}{ + "from": "email", + }, }) } diff --git a/mixpanel.go b/mixpanel.go index c28561e..7784efe 100644 --- a/mixpanel.go +++ b/mixpanel.go @@ -7,19 +7,22 @@ import ( "fmt" "io/ioutil" "net/http" + "time" ) var ( ErrTrackFailed = errors.New("Mixpanel did not return 1 when tracking") + + IgnoreTime *time.Time = &time.Time{} ) // The Mixapanel struct store the mixpanel endpoint and the project token type Mixpanel interface { - // Track create a events to current distinct id - Track(distinctId string, eventName string, properties map[string]interface{}) error + // Create a mixpanel event + Track(distinctId, eventName string, e *Event) error - // Identify call mixpanel 'engage' and returns People instance - Identify(id string) People + // Set properties for a mixpanel user. + Update(distinctId string, u *Update) error } // The Mixapanel struct store the mixpanel endpoint and the project token @@ -29,63 +32,81 @@ type mixpanel struct { ApiURL string } -// People represents a consumer, and is used on People Analytics -type People interface { - // Track create a events to current people - Track(eventName string, properties map[string]interface{}) error +// A mixpanel event +type Event struct { + // IP-address of the user. Leave empty to use autodetect, or set to "0" to + // not specify an ip-address. + IP string - // Create a Update Operation to current people, see - // https://mixpanel.com/help/reference/http - Update(operation string, updateParams map[string]interface{}) error -} + // Timestamp. Set to nil to use the current time. + Timestamp *time.Time -// People represents a consumer, and is used on People Analytics -type people struct { - m *mixpanel - id string + // Custom properties. At least one must be specified. + Properties map[string]interface{} } -type trackParams struct { - Event string `json:"event"` - Properties map[string]interface{} `json:"properties"` +// An update of a user in mixpanel +type Update struct { + // IP-address of the user. Leave empty to use autodetect, or set to "0" to + // not specify an ip-address at all. + IP string + + // Timestamp. Set to nil to use the current time, or IgnoreTime to not use a + // timestamp. + Timestamp *time.Time + + // Update operation such as "$set", "$update" etc. + Operation string + + // Custom properties. At least one must be specified. + Properties map[string]interface{} } // Track create a events to current distinct id -func (m *mixpanel) Track(distinctId string, eventName string, properties map[string]interface{}) error { - params := trackParams{Event: eventName} +func (m *mixpanel) Track(distinctId, eventName string, e *Event) error { + props := map[string]interface{}{ + "token": m.Token, + "distinct_id": distinctId, + } + if e.IP != "" { + props["ip"] = e.IP + } + if e.Timestamp != nil { + props["time"] = e.Timestamp.Unix() + } - params.Properties = make(map[string]interface{}, 0) - params.Properties["token"] = m.Token - params.Properties["distinct_id"] = distinctId + for key, value := range e.Properties { + props[key] = value + } - for key, value := range properties { - params.Properties[key] = value + params := map[string]interface{}{ + "event": eventName, + "properties": props, } return m.send("track", params) } -// Identify call mixpanel 'engage' and returns People instance -func (m *mixpanel) Identify(id string) People { - params := map[string]interface{}{"$token": m.Token, "$distinct_id": id} - m.send("engage", params) - return &people{m: m, id: id} -} - -// Track create a events to current people -func (p *people) Track(eventName string, properties map[string]interface{}) error { - return p.m.Track(p.id, eventName, properties) -} - -// Create a Update Operation to current people, see -// https://mixpanel.com/help/reference/http -func (p *people) Update(operation string, updateParams map[string]interface{}) error { +// Updates a user in mixpanel. See +// https://mixpanel.com/help/reference/http#people-analytics-updates +func (m *mixpanel) Update(distinctId string, u *Update) error { params := map[string]interface{}{ - "$token": p.m.Token, - "$distinct_id": p.id, + "$token": m.Token, + "$distinct_id": distinctId, } - params[operation] = updateParams - return p.m.send("engage", params) + + if u.IP != "" { + params["$ip"] = u.IP + } + if u.Timestamp == IgnoreTime { + params["$ignore_time"] = true + } else if u.Timestamp != nil { + params["$time"] = u.Timestamp.Unix() + } + + params[u.Operation] = u.Properties + + return m.send("engage", params) } func (m *mixpanel) to64(data string) string { diff --git a/mixpanel_test.go b/mixpanel_test.go index 429bfac..09cfd46 100644 --- a/mixpanel_test.go +++ b/mixpanel_test.go @@ -41,8 +41,10 @@ func TestTrack(t *testing.T) { setup() defer teardown() - client.Track("13793", "Signed Up", map[string]interface{}{ - "Referred By": "Friend", + client.Track("13793", "Signed Up", &Event{ + Properties: map[string]interface{}{ + "Referred By": "Friend", + }, }) want := "{\"event\":\"Signed Up\",\"properties\":{\"Referred By\":\"Friend\",\"distinct_id\":\"13793\",\"token\":\"e3bc4100330c35722740fb8c6f5abddc\"}}" @@ -61,36 +63,16 @@ func TestTrack(t *testing.T) { } } -func TestIdentify(t *testing.T) { - setup() - defer teardown() - - client.Identify("13793") - - want := "{\"$distinct_id\":\"13793\",\"$token\":\"e3bc4100330c35722740fb8c6f5abddc\"}" - - if !reflect.DeepEqual(decodeURL(LastRequest.URL.String()), want) { - t.Errorf("LastRequest.URL returned %+v, want %+v", - decodeURL(LastRequest.URL.String()), want) - } - - want = "/engage" - path := LastRequest.URL.Path - - if !reflect.DeepEqual(path, want) { - t.Errorf("path returned %+v, want %+v", - path, want) - } -} - func TestPeopleOperations(t *testing.T) { setup() defer teardown() - people := client.Identify("13793") - people.Update("$set", map[string]interface{}{ - "Address": "1313 Mockingbird Lane", - "Birthday": "1948-01-01", + client.Update("13793", &Update{ + Operation: "$set", + Properties: map[string]interface{}{ + "Address": "1313 Mockingbird Lane", + "Birthday": "1948-01-01", + }, }) want := "{\"$distinct_id\":\"13793\",\"$set\":{\"Address\":\"1313 Mockingbird Lane\",\"Birthday\":\"1948-01-01\"},\"$token\":\"e3bc4100330c35722740fb8c6f5abddc\"}" @@ -113,9 +95,10 @@ func TestPeopleTrack(t *testing.T) { setup() defer teardown() - people := client.Identify("13793") - people.Track("Signed Up", map[string]interface{}{ - "Referred By": "Friend", + client.Track("13793", "Signed Up", &Event{ + Properties: map[string]interface{}{ + "Referred By": "Friend", + }, }) want := "{\"event\":\"Signed Up\",\"properties\":{\"Referred By\":\"Friend\",\"distinct_id\":\"13793\",\"token\":\"e3bc4100330c35722740fb8c6f5abddc\"}}" @@ -143,11 +126,11 @@ func TestError(t *testing.T) { client = New("e3bc4100330c35722740fb8c6f5abddc", ts.URL) - if err := client.Track("1", "name", nil); err != ErrTrackFailed { + if err := client.Update("1", &Update{}); err != ErrTrackFailed { t.Error("Got bad error for track", err) } - if err := client.Identify("1").Track("name", nil); err != ErrTrackFailed { + if err := client.Track("1", "name", &Event{}); err != ErrTrackFailed { t.Error("Got bad error for track", err) } } diff --git a/mock.go b/mock.go index f092b3c..cfd2005 100644 --- a/mock.go +++ b/mock.go @@ -3,6 +3,7 @@ package mixpanel import ( "errors" "fmt" + "time" ) // Mocked version of Mixpanel which can be used in unit tests. @@ -26,29 +27,41 @@ func (m *Mock) String() string { } // Identifies a user. The user will be added to the People map. -func (m *Mock) Identify(distinctId string) People { - if p, ok := m.People[distinctId]; ok { - return p - } else { - p := &MockPeople{ +func (m *Mock) people(distinctId string) *MockPeople { + p := m.People[distinctId] + if p == nil { + p = &MockPeople{ Properties: map[string]interface{}{}, } m.People[distinctId] = p - return p } + + return p } -func (m *Mock) Track(distinctId string, eventName string, properties map[string]interface{}) error { - return m.Identify(distinctId).Track(eventName, properties) +func (m *Mock) Track(distinctId, eventName string, e *Event) error { + p := m.people(distinctId) + p.Events = append(p.Events, MockEvent{ + Event: *e, + Name: eventName, + }) + return nil } type MockPeople struct { Properties map[string]interface{} + Time *time.Time + IP string Events []MockEvent } func (mp *MockPeople) String() string { - str := "" + timeStr := "" + if mp.Time != nil { + timeStr = mp.Time.Format(time.RFC3339) + } + + str := fmt.Sprintf(" ip: %s\n time: %s\n", mp.IP, timeStr) str += " properties:\n" for key, val := range mp.Properties { str += fmt.Sprintf(" %s: %v\n", key, val) @@ -56,6 +69,14 @@ func (mp *MockPeople) String() string { str += " events:\n" for _, event := range mp.Events { str += " " + event.Name + ":\n" + str += fmt.Sprintf(" IP: %s\n", event.IP) + if event.Timestamp != nil { + str += fmt.Sprintf( + " Timestamp: %s\n", event.Timestamp.Format(time.RFC3339), + ) + } else { + str += " Timestamp:\n" + } for key, val := range event.Properties { str += fmt.Sprintf(" %s: %v\n", key, val) } @@ -63,16 +84,20 @@ func (mp *MockPeople) String() string { return str } -func (mp *MockPeople) Track(eventName string, properties map[string]interface{}) error { - mp.Events = append(mp.Events, MockEvent{eventName, properties}) - return nil -} +func (m *Mock) Update(distinctId string, u *Update) error { + p := m.people(distinctId) + + if u.IP != "" { + p.IP = u.IP + } + if u.Timestamp != nil && u.Timestamp != IgnoreTime { + p.Time = u.Timestamp + } -func (mp *MockPeople) Update(operation string, updateParams map[string]interface{}) error { - switch operation { + switch u.Operation { case "$set": - for key, val := range updateParams { - mp.Properties[key] = val + for key, val := range u.Properties { + p.Properties[key] = val } default: return errors.New("mixpanel.Mock only supports the $set operation") @@ -82,6 +107,6 @@ func (mp *MockPeople) Update(operation string, updateParams map[string]interface } type MockEvent struct { - Name string - Properties map[string]interface{} + Event + Name string } diff --git a/mock_test.go b/mock_test.go index 15fbb42..7affbc3 100644 --- a/mock_test.go +++ b/mock_test.go @@ -1,29 +1,44 @@ package mixpanel -import "fmt" +import ( + "fmt" + "time" +) var fullfillsInterface Mixpanel = &Mock{} func ExampleMock() { - var people People client := NewMock() - people = client.Identify("1") - people.Update("$set", map[string]interface{}{ - "custom_field": "cool!", + t, _ := time.Parse(time.RFC3339, "2016-03-03T15:17:53+01:00") + + client.Update("1", &Update{ + Operation: "$set", + Timestamp: &t, + IP: "127.0.0.1", + Properties: map[string]interface{}{ + "custom_field": "cool!", + }, }) - people.Track("Sign Up", map[string]interface{}{ - "from": "email", + client.Track("1", "Sign Up", &Event{ + IP: "1.2.3.4", + Properties: map[string]interface{}{ + "from": "email", + }, }) fmt.Println(client) // Output: // 1: + // ip: 127.0.0.1 + // time: 2016-03-03T15:17:53+01:00 // properties: // custom_field: cool! // events: // Sign Up: + // IP: 1.2.3.4 + // Timestamp: // from: email }