Skip to content
This repository was archived by the owner on Jan 27, 2026. It is now read-only.

Commit 3f6c53b

Browse files
authored
Merge branch 'master' into am/update-groups
2 parents 17bb77b + 75438a0 commit 3f6c53b

File tree

5 files changed

+129
-30
lines changed

5 files changed

+129
-30
lines changed

example_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ func ExampleNew() {
66
New("mytoken", "")
77
}
88

9+
func ExampleNewWithSecret() {
10+
NewWithSecret("mytoken", "myapisecret", "")
11+
}
12+
913
func ExampleMixpanel() {
1014
client := New("mytoken", "")
1115

@@ -17,7 +21,7 @@ func ExampleMixpanel() {
1721
}
1822

1923
func ExamplePeople() {
20-
client := New("mytoken", "")
24+
client := NewWithSecret("mytoken", "myapisecret", "")
2125

2226
client.UpdateUser("1", &Update{
2327
Operation: "$set",
@@ -34,4 +38,12 @@ func ExamplePeople() {
3438
"from": "email",
3539
},
3640
})
41+
42+
importTimestamp := time.Now().Add(-5 * 24 * time.Hour)
43+
client.Import("1", "Sign Up", &Event{
44+
Timestamp: &importTimestamp,
45+
Properties: map[string]interface{}{
46+
"subject": "topic",
47+
},
48+
})
3749
}

mixpanel.go

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,21 @@ func (err *MixpanelError) Error() string {
2525
}
2626

2727
type ErrTrackFailed struct {
28-
Body string
29-
Resp *http.Response
28+
Message string
3029
}
3130

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

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

40+
// Create a mixpanel event using the import api
41+
Import(distinctId, eventName string, e *Event) error
42+
4143
// Set properties for a mixpanel user.
4244
// Deprecated: Use UpdateUser instead
4345
Update(distinctId string, u *Update) error
@@ -48,13 +50,15 @@ type Mixpanel interface {
4850
// Set properties for a mixpanel group.
4951
UpdateGroup(groupKey, groupId string, u *Update) error
5052

53+
// Create an alias for an existing distinct id
5154
Alias(distinctId, newId string) error
5255
}
5356

5457
// The Mixapanel struct store the mixpanel endpoint and the project token
5558
type mixpanel struct {
5659
Client *http.Client
5760
Token string
61+
Secret string
5862
ApiURL string
5963
}
6064

@@ -88,7 +92,7 @@ type Update struct {
8892
Properties map[string]interface{}
8993
}
9094

91-
// Track create a events to current distinct id
95+
// Alias create an alias for an existing distinct id
9296
func (m *mixpanel) Alias(distinctId, newId string) error {
9397
props := map[string]interface{}{
9498
"token": m.Token,
@@ -104,7 +108,7 @@ func (m *mixpanel) Alias(distinctId, newId string) error {
104108
return m.send("track", params, false)
105109
}
106110

107-
// Track create a events to current distinct id
111+
// Track create an event for an existing distinct id
108112
func (m *mixpanel) Track(distinctId, eventName string, e *Event) error {
109113
props := map[string]interface{}{
110114
"token": m.Token,
@@ -131,6 +135,36 @@ func (m *mixpanel) Track(distinctId, eventName string, e *Event) error {
131135
return m.send("track", params, autoGeolocate)
132136
}
133137

138+
// Import create an event for an existing distinct id
139+
// See https://developer.mixpanel.com/docs/importing-old-events
140+
func (m *mixpanel) Import(distinctId, eventName string, e *Event) error {
141+
props := map[string]interface{}{
142+
"token": m.Token,
143+
"distinct_id": distinctId,
144+
}
145+
if e.IP != "" {
146+
props["ip"] = e.IP
147+
}
148+
if e.Timestamp != nil {
149+
props["time"] = e.Timestamp.Unix()
150+
}
151+
152+
for key, value := range e.Properties {
153+
props[key] = value
154+
}
155+
156+
params := map[string]interface{}{
157+
"event": eventName,
158+
"properties": props,
159+
}
160+
161+
autoGeolocate := e.IP == ""
162+
163+
return m.send("import", params, autoGeolocate)
164+
}
165+
166+
// Update updates a user in mixpanel. See
167+
// https://mixpanel.com/help/reference/http#people-analytics-updates
134168
// Deprecated: Use UpdateUser instead
135169
func (m *mixpanel) Update(distinctId string, u *Update) error {
136170
return m.UpdateUser(distinctId, u)
@@ -185,7 +219,7 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
185219
return err
186220
}
187221

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

190224
if autoGeolocate {
191225
url += "&ip=1"
@@ -195,7 +229,11 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
195229
return &MixpanelError{URL: url, Err: err}
196230
}
197231

198-
resp, err := m.Client.Get(url)
232+
req, _ := http.NewRequest("GET", url, nil)
233+
if m.Secret != "" {
234+
req.SetBasicAuth(m.Secret, "")
235+
}
236+
resp, err := m.Client.Do(req)
199237

200238
if err != nil {
201239
return wrapErr(err)
@@ -209,8 +247,17 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
209247
return wrapErr(bodyErr)
210248
}
211249

212-
if strBody := string(body); strBody != "1" && strBody != "1\n" {
213-
return wrapErr(&ErrTrackFailed{Body: strBody, Resp: resp})
250+
type verboseResponse struct {
251+
Error string `json:"error"`
252+
Status int `json:"status"`
253+
}
254+
255+
var jsonBody verboseResponse
256+
json.Unmarshal(body, &jsonBody)
257+
258+
if jsonBody.Status != 1 {
259+
errMsg := fmt.Sprintf("error=%s; status=%d; httpCode=%d", jsonBody.Error, jsonBody.Status, resp.StatusCode)
260+
return wrapErr(&ErrTrackFailed{Message: errMsg})
214261
}
215262

216263
return nil
@@ -222,16 +269,28 @@ func New(token, apiURL string) Mixpanel {
222269
return NewFromClient(http.DefaultClient, token, apiURL)
223270
}
224271

225-
// Creates a client instance using the specified client instance. This is useful
272+
// NewWithSecret returns the client instance using a secret.If apiURL is blank,
273+
// the default will be used ("https://api.mixpanel.com").
274+
func NewWithSecret(token, secret, apiURL string) Mixpanel {
275+
return NewFromClientWithSecret(http.DefaultClient, token, secret, apiURL)
276+
}
277+
278+
// NewFromClient creates a client instance using the specified client instance. This is useful
226279
// when using a proxy.
227280
func NewFromClient(c *http.Client, token, apiURL string) Mixpanel {
281+
return NewFromClientWithSecret(c, token, "", apiURL)
282+
}
283+
284+
// NewFromClientWithSecret creates a client instance using the specified client instance and secret.
285+
func NewFromClientWithSecret(c *http.Client, token, secret, apiURL string) Mixpanel {
228286
if apiURL == "" {
229287
apiURL = "https://api.mixpanel.com"
230288
}
231289

232290
return &mixpanel{
233291
Client: c,
234292
Token: token,
293+
Secret: secret,
235294
ApiURL: apiURL,
236295
}
237296
}

mixpanel_test.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package mixpanel
22

33
import (
44
"encoding/base64"
5+
"fmt"
56
"net/http"
67
"net/http/httptest"
78
"reflect"
89
"strings"
910
"testing"
11+
"time"
1012
)
1113

1214
var (
@@ -22,7 +24,7 @@ func setup() {
2224
LastRequest = r
2325
}))
2426

25-
client = New("e3bc4100330c35722740fb8c6f5abddc", ts.URL)
27+
client = NewWithSecret("e3bc4100330c35722740fb8c6f5abddc", "mysecret", ts.URL)
2628
}
2729

2830
func teardown() {
@@ -63,26 +65,27 @@ func TestTrack(t *testing.T) {
6365
}
6466
}
6567

66-
func TestPeopleOperations(t *testing.T) {
68+
func TestImport(t *testing.T) {
6769
setup()
6870
defer teardown()
6971

70-
client.Update("13793", &Update{
71-
Operation: "$set",
72+
importTime := time.Now().Add(-5 * 24 * time.Hour)
73+
74+
client.Import("13793", "Signed Up", &Event{
7275
Properties: map[string]interface{}{
73-
"Address": "1313 Mockingbird Lane",
74-
"Birthday": "1948-01-01",
76+
"Referred By": "Friend",
7577
},
78+
Timestamp: &importTime,
7679
})
7780

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

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

85-
want = "/engage"
88+
want = "/import"
8689
path := LastRequest.URL.Path
8790

8891
if !reflect.DeepEqual(path, want) {
@@ -119,24 +122,26 @@ func TestGroupOperations(t *testing.T) {
119122
}
120123
}
121124

122-
func TestPeopleTrack(t *testing.T) {
125+
func TestUpdate(t *testing.T) {
123126
setup()
124127
defer teardown()
125128

126-
client.Track("13793", "Signed Up", &Event{
129+
client.Update("13793", &Update{
130+
Operation: "$set",
127131
Properties: map[string]interface{}{
128-
"Referred By": "Friend",
132+
"Address": "1313 Mockingbird Lane",
133+
"Birthday": "1948-01-01",
129134
},
130135
})
131136

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

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

139-
want = "/track"
144+
want = "/engage"
140145
path := LastRequest.URL.Path
141146

142147
if !reflect.DeepEqual(path, want) {
@@ -148,7 +153,7 @@ func TestPeopleTrack(t *testing.T) {
148153
func TestError(t *testing.T) {
149154
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150155
w.WriteHeader(200)
151-
w.Write([]byte("0\n"))
156+
w.Write([]byte(`{"error": "some error", "status": 0}`))
152157
LastRequest = r
153158
}))
154159

@@ -167,13 +172,14 @@ func TestError(t *testing.T) {
167172
return
168173
}
169174

170-
if terr.Body != "0\n" {
171-
t.Errorf("Wrong body carried in the *ErrTrackFailed: %q", terr.Body)
175+
if terr.Message != "some error" {
176+
t.Errorf("Wrong body carried in the *ErrTrackFailed: %q", terr.Message)
172177
}
173178
}
174179

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

177182
assertErrTrackFailed(client.Update("1", &Update{}))
178183
assertErrTrackFailed(client.Track("1", "name", &Event{}))
184+
assertErrTrackFailed(client.Import("1", "name", &Event{}))
179185
}

mock.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ func (m *Mock) Track(distinctId, eventName string, e *Event) error {
4848
return nil
4949
}
5050

51+
func (m *Mock) Import(distinctId, eventName string, e *Event) error {
52+
p := m.people(distinctId)
53+
p.Events = append(p.Events, MockEvent{
54+
Event: *e,
55+
Name: eventName,
56+
})
57+
return nil
58+
}
59+
5160
type MockPeople struct {
5261
Properties map[string]interface{}
5362
Time *time.Time
@@ -99,12 +108,12 @@ func (m *Mock) UpdateUser(distinctId string, u *Update) error {
99108
}
100109

101110
switch u.Operation {
102-
case "$set":
111+
case "$set", "$set_once":
103112
for key, val := range u.Properties {
104113
p.Properties[key] = val
105114
}
106115
default:
107-
return errors.New("mixpanel.Mock only supports the $set operation")
116+
return errors.New("mixpanel.Mock only supports the $set and $set_once operations")
108117
}
109118

110119
return nil

mock_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ func ExampleMock() {
2828
},
2929
})
3030

31+
client.Import("1", "Sign Up", &Event{
32+
IP: "1.2.3.4",
33+
Timestamp: &t,
34+
Properties: map[string]interface{}{
35+
"imported": true,
36+
},
37+
})
38+
3139
fmt.Println(client)
3240

3341
// Output:
@@ -41,4 +49,9 @@ func ExampleMock() {
4149
// IP: 1.2.3.4
4250
// Timestamp:
4351
// from: email
52+
// Sign Up:
53+
// IP: 1.2.3.4
54+
// Timestamp: 2016-03-03T15:17:53+01:00
55+
// imported: true
56+
4457
}

0 commit comments

Comments
 (0)