Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Import method to be able to create events older than 5 days #7

Merged
merged 14 commits into from
Apr 10, 2022
14 changes: 13 additions & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ func ExampleNew() {
New("mytoken", "")
}

func ExampleNewWithSecret() {
NewWithSecret("mytoken", "myapisecret", "")
}

func ExampleMixpanel() {
client := New("mytoken", "")

Expand All @@ -17,7 +21,7 @@ func ExampleMixpanel() {
}

func ExamplePeople() {
client := New("mytoken", "")
client := NewWithSecret("mytoken", "myapisecret", "")

client.Update("1", &Update{
Operation: "$set",
Expand All @@ -34,4 +38,12 @@ func ExamplePeople() {
"from": "email",
},
})

importTimestamp := time.Now().Add(-5 * 24 * time.Hour)
client.Import("1", "Sign Up", &Event{
Timestamp: &importTimestamp,
Properties: map[string]interface{}{
"subject": "topic",
},
})
}
81 changes: 69 additions & 12 deletions mixpanel.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,33 @@ func (err *MixpanelError) Error() string {
}

type ErrTrackFailed struct {
Body string
Resp *http.Response
Message string
}

func (err *ErrTrackFailed) Error() string {
return fmt.Sprintf("Mixpanel did not return 1 when tracking: %s", err.Body)
return fmt.Sprintf("mixpanel did not return 1 when tracking: %s", err.Message)
}

// The Mixapanel struct store the mixpanel endpoint and the project token
type Mixpanel interface {
// Create a mixpanel event
// Create a mixpanel event using the track api
Track(distinctId, eventName string, e *Event) error

// Create a mixpanel event using the import api
Import(distinctId, eventName string, e *Event) error

// Set properties for a mixpanel user.
Update(distinctId string, u *Update) error

// Create an alias for an existing distinct id
Alias(distinctId, newId string) error
}

// The Mixapanel struct store the mixpanel endpoint and the project token
type mixpanel struct {
Client *http.Client
Token string
Secret string
ApiURL string
}

Expand Down Expand Up @@ -81,7 +85,7 @@ type Update struct {
Properties map[string]interface{}
}

// Track create a events to current distinct id
// Alias create an alias for an existing distinct id
func (m *mixpanel) Alias(distinctId, newId string) error {
props := map[string]interface{}{
"token": m.Token,
Expand All @@ -97,7 +101,7 @@ func (m *mixpanel) Alias(distinctId, newId string) error {
return m.send("track", params, false)
}

// Track create a events to current distinct id
// Track create an event for an existing distinct id
func (m *mixpanel) Track(distinctId, eventName string, e *Event) error {
props := map[string]interface{}{
"token": m.Token,
Expand All @@ -124,7 +128,35 @@ func (m *mixpanel) Track(distinctId, eventName string, e *Event) error {
return m.send("track", params, autoGeolocate)
}

// Updates a user in mixpanel. See
// Import create an event for an existing distinct id
// See https://developer.mixpanel.com/docs/importing-old-events
func (m *mixpanel) Import(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()
}

for key, value := range e.Properties {
props[key] = value
}

params := map[string]interface{}{
"event": eventName,
"properties": props,
}

autoGeolocate := e.IP == ""

return m.send("import", params, autoGeolocate)
}

// Update 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{}{
Expand Down Expand Up @@ -159,7 +191,7 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
return err
}

url := m.ApiURL + "/" + eventType + "?data=" + m.to64(data)
url := m.ApiURL + "/" + eventType + "?data=" + m.to64(data) + "&verbose=1"

if autoGeolocate {
url += "&ip=1"
Expand All @@ -169,7 +201,11 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
return &MixpanelError{URL: url, Err: err}
}

resp, err := m.Client.Get(url)
req, _ := http.NewRequest("GET", url, nil)
if m.Secret != "" {
req.SetBasicAuth(m.Secret, "")
}
resp, err := m.Client.Do(req)

if err != nil {
return wrapErr(err)
Expand All @@ -183,8 +219,17 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
return wrapErr(bodyErr)
}

if strBody := string(body); strBody != "1" && strBody != "1\n" {
return wrapErr(&ErrTrackFailed{Body: strBody, Resp: resp})
type verboseResponse struct {
Error string `json:"error"`
Status int `json:"status"`
}

var jsonBody verboseResponse
json.Unmarshal(body, &jsonBody)

if jsonBody.Status != 1 {
errMsg := fmt.Sprintf("error=%s; status=%d; httpCode=%d", jsonBody.Error, jsonBody.Status, resp.StatusCode)
return wrapErr(&ErrTrackFailed{Message: errMsg})
}

return nil
Expand All @@ -196,16 +241,28 @@ func New(token, apiURL string) Mixpanel {
return NewFromClient(http.DefaultClient, token, apiURL)
}

// Creates a client instance using the specified client instance. This is useful
// NewWithSecret returns the client instance using a secret.If apiURL is blank,
// the default will be used ("https://api.mixpanel.com").
func NewWithSecret(token, secret, apiURL string) Mixpanel {
return NewFromClientWithSecret(http.DefaultClient, token, secret, apiURL)
}

// NewFromClient 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 {
return NewFromClientWithSecret(c, token, "", apiURL)
}

// NewFromClientWithSecret creates a client instance using the specified client instance and secret.
func NewFromClientWithSecret(c *http.Client, token, secret, apiURL string) Mixpanel {
if apiURL == "" {
apiURL = "https://api.mixpanel.com"
}

return &mixpanel{
Client: c,
Token: token,
Secret: secret,
ApiURL: apiURL,
}
}
38 changes: 22 additions & 16 deletions mixpanel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package mixpanel

import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
)

var (
Expand All @@ -22,7 +24,7 @@ func setup() {
LastRequest = r
}))

client = New("e3bc4100330c35722740fb8c6f5abddc", ts.URL)
client = NewWithSecret("e3bc4100330c35722740fb8c6f5abddc", "mysecret", ts.URL)
}

func teardown() {
Expand Down Expand Up @@ -63,26 +65,27 @@ func TestTrack(t *testing.T) {
}
}

func TestPeopleOperations(t *testing.T) {
func TestImport(t *testing.T) {
setup()
defer teardown()

client.Update("13793", &Update{
Operation: "$set",
importTime := time.Now().Add(-5 * 24 * time.Hour)

client.Import("13793", "Signed Up", &Event{
Properties: map[string]interface{}{
"Address": "1313 Mockingbird Lane",
"Birthday": "1948-01-01",
"Referred By": "Friend",
},
Timestamp: &importTime,
})

want := "{\"$distinct_id\":\"13793\",\"$set\":{\"Address\":\"1313 Mockingbird Lane\",\"Birthday\":\"1948-01-01\"},\"$token\":\"e3bc4100330c35722740fb8c6f5abddc\"}"
want := fmt.Sprintf("{\"event\":\"Signed Up\",\"properties\":{\"Referred By\":\"Friend\",\"distinct_id\":\"13793\",\"time\":%d,\"token\":\"e3bc4100330c35722740fb8c6f5abddc\"}}", importTime.Unix())

if !reflect.DeepEqual(decodeURL(LastRequest.URL.String()), want) {
t.Errorf("LastRequest.URL returned %+v, want %+v",
decodeURL(LastRequest.URL.String()), want)
}

want = "/engage"
want = "/import"
path := LastRequest.URL.Path

if !reflect.DeepEqual(path, want) {
Expand All @@ -91,24 +94,26 @@ func TestPeopleOperations(t *testing.T) {
}
}

func TestPeopleTrack(t *testing.T) {
func TestUpdate(t *testing.T) {
setup()
defer teardown()

client.Track("13793", "Signed Up", &Event{
client.Update("13793", &Update{
Operation: "$set",
Properties: map[string]interface{}{
"Referred By": "Friend",
"Address": "1313 Mockingbird Lane",
"Birthday": "1948-01-01",
},
})

want := "{\"event\":\"Signed Up\",\"properties\":{\"Referred By\":\"Friend\",\"distinct_id\":\"13793\",\"token\":\"e3bc4100330c35722740fb8c6f5abddc\"}}"
want := "{\"$distinct_id\":\"13793\",\"$set\":{\"Address\":\"1313 Mockingbird Lane\",\"Birthday\":\"1948-01-01\"},\"$token\":\"e3bc4100330c35722740fb8c6f5abddc\"}"

if !reflect.DeepEqual(decodeURL(LastRequest.URL.String()), want) {
t.Errorf("LastRequest.URL returned %+v, want %+v",
decodeURL(LastRequest.URL.String()), want)
}

want = "/track"
want = "/engage"
path := LastRequest.URL.Path

if !reflect.DeepEqual(path, want) {
Expand All @@ -120,7 +125,7 @@ func TestPeopleTrack(t *testing.T) {
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"))
w.Write([]byte(`{"error": "some error", "status": 0}`))
LastRequest = r
}))

Expand All @@ -139,13 +144,14 @@ func TestError(t *testing.T) {
return
}

if terr.Body != "0\n" {
t.Errorf("Wrong body carried in the *ErrTrackFailed: %q", terr.Body)
if terr.Message != "some error" {
t.Errorf("Wrong body carried in the *ErrTrackFailed: %q", terr.Message)
}
}

client = New("e3bc4100330c35722740fb8c6f5abddc", ts.URL)

assertErrTrackFailed(client.Update("1", &Update{}))
assertErrTrackFailed(client.Track("1", "name", &Event{}))
assertErrTrackFailed(client.Import("1", "name", &Event{}))
}
13 changes: 11 additions & 2 deletions mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ func (m *Mock) Track(distinctId, eventName string, e *Event) error {
return nil
}

func (m *Mock) Import(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
Expand Down Expand Up @@ -95,12 +104,12 @@ func (m *Mock) Update(distinctId string, u *Update) error {
}

switch u.Operation {
case "$set":
case "$set", "$set_once":
for key, val := range u.Properties {
p.Properties[key] = val
}
default:
return errors.New("mixpanel.Mock only supports the $set operation")
return errors.New("mixpanel.Mock only supports the $set and $set_once operations")
}

return nil
Expand Down
13 changes: 13 additions & 0 deletions mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ func ExampleMock() {
},
})

client.Import("1", "Sign Up", &Event{
IP: "1.2.3.4",
Timestamp: &t,
Properties: map[string]interface{}{
"imported": true,
},
})

fmt.Println(client)

// Output:
Expand All @@ -41,4 +49,9 @@ func ExampleMock() {
// IP: 1.2.3.4
// Timestamp:
// from: email
// Sign Up:
// IP: 1.2.3.4
// Timestamp: 2016-03-03T15:17:53+01:00
// imported: true

}