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

Commit 75438a0

Browse files
authored
Merge pull request #7 from PM-Connect/master
Add Import method to be able to create events older than 5 days
2 parents f8d5594 + 6f5a199 commit 75438a0

File tree

5 files changed

+128
-31
lines changed

5 files changed

+128
-31
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.Update("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: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,33 @@ 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
Update(distinctId string, u *Update) error
4345

46+
// Create an alias for an existing distinct id
4447
Alias(distinctId, newId string) error
4548
}
4649

4750
// The Mixapanel struct store the mixpanel endpoint and the project token
4851
type mixpanel struct {
4952
Client *http.Client
5053
Token string
54+
Secret string
5155
ApiURL string
5256
}
5357

@@ -81,7 +85,7 @@ type Update struct {
8185
Properties map[string]interface{}
8286
}
8387

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

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

127-
// Updates a user in mixpanel. See
131+
// Import create an event for an existing distinct id
132+
// See https://developer.mixpanel.com/docs/importing-old-events
133+
func (m *mixpanel) Import(distinctId, eventName string, e *Event) error {
134+
props := map[string]interface{}{
135+
"token": m.Token,
136+
"distinct_id": distinctId,
137+
}
138+
if e.IP != "" {
139+
props["ip"] = e.IP
140+
}
141+
if e.Timestamp != nil {
142+
props["time"] = e.Timestamp.Unix()
143+
}
144+
145+
for key, value := range e.Properties {
146+
props[key] = value
147+
}
148+
149+
params := map[string]interface{}{
150+
"event": eventName,
151+
"properties": props,
152+
}
153+
154+
autoGeolocate := e.IP == ""
155+
156+
return m.send("import", params, autoGeolocate)
157+
}
158+
159+
// Update updates a user in mixpanel. See
128160
// https://mixpanel.com/help/reference/http#people-analytics-updates
129161
func (m *mixpanel) Update(distinctId string, u *Update) error {
130162
params := map[string]interface{}{
@@ -159,7 +191,7 @@ func (m *mixpanel) send(eventType string, params interface{}, autoGeolocate bool
159191
return err
160192
}
161193

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

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

172-
resp, err := m.Client.Get(url)
204+
req, _ := http.NewRequest("GET", url, nil)
205+
if m.Secret != "" {
206+
req.SetBasicAuth(m.Secret, "")
207+
}
208+
resp, err := m.Client.Do(req)
173209

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

186-
if strBody := string(body); strBody != "1" && strBody != "1\n" {
187-
return wrapErr(&ErrTrackFailed{Body: strBody, Resp: resp})
222+
type verboseResponse struct {
223+
Error string `json:"error"`
224+
Status int `json:"status"`
225+
}
226+
227+
var jsonBody verboseResponse
228+
json.Unmarshal(body, &jsonBody)
229+
230+
if jsonBody.Status != 1 {
231+
errMsg := fmt.Sprintf("error=%s; status=%d; httpCode=%d", jsonBody.Error, jsonBody.Status, resp.StatusCode)
232+
return wrapErr(&ErrTrackFailed{Message: errMsg})
188233
}
189234

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

199-
// Creates a client instance using the specified client instance. This is useful
244+
// NewWithSecret returns the client instance using a secret.If apiURL is blank,
245+
// the default will be used ("https://api.mixpanel.com").
246+
func NewWithSecret(token, secret, apiURL string) Mixpanel {
247+
return NewFromClientWithSecret(http.DefaultClient, token, secret, apiURL)
248+
}
249+
250+
// NewFromClient creates a client instance using the specified client instance. This is useful
200251
// when using a proxy.
201252
func NewFromClient(c *http.Client, token, apiURL string) Mixpanel {
253+
return NewFromClientWithSecret(c, token, "", apiURL)
254+
}
255+
256+
// NewFromClientWithSecret creates a client instance using the specified client instance and secret.
257+
func NewFromClientWithSecret(c *http.Client, token, secret, apiURL string) Mixpanel {
202258
if apiURL == "" {
203259
apiURL = "https://api.mixpanel.com"
204260
}
205261

206262
return &mixpanel{
207263
Client: c,
208264
Token: token,
265+
Secret: secret,
209266
ApiURL: apiURL,
210267
}
211268
}

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) {
@@ -91,24 +94,26 @@ func TestPeopleOperations(t *testing.T) {
9194
}
9295
}
9396

94-
func TestPeopleTrack(t *testing.T) {
97+
func TestUpdate(t *testing.T) {
9598
setup()
9699
defer teardown()
97100

98-
client.Track("13793", "Signed Up", &Event{
101+
client.Update("13793", &Update{
102+
Operation: "$set",
99103
Properties: map[string]interface{}{
100-
"Referred By": "Friend",
104+
"Address": "1313 Mockingbird Lane",
105+
"Birthday": "1948-01-01",
101106
},
102107
})
103108

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

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

111-
want = "/track"
116+
want = "/engage"
112117
path := LastRequest.URL.Path
113118

114119
if !reflect.DeepEqual(path, want) {
@@ -120,7 +125,7 @@ func TestPeopleTrack(t *testing.T) {
120125
func TestError(t *testing.T) {
121126
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
122127
w.WriteHeader(200)
123-
w.Write([]byte("0\n"))
128+
w.Write([]byte(`{"error": "some error", "status": 0}`))
124129
LastRequest = r
125130
}))
126131

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

142-
if terr.Body != "0\n" {
143-
t.Errorf("Wrong body carried in the *ErrTrackFailed: %q", terr.Body)
147+
if terr.Message != "some error" {
148+
t.Errorf("Wrong body carried in the *ErrTrackFailed: %q", terr.Message)
144149
}
145150
}
146151

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

149154
assertErrTrackFailed(client.Update("1", &Update{}))
150155
assertErrTrackFailed(client.Track("1", "name", &Event{}))
156+
assertErrTrackFailed(client.Import("1", "name", &Event{}))
151157
}

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

97106
switch u.Operation {
98-
case "$set":
107+
case "$set", "$set_once":
99108
for key, val := range u.Properties {
100109
p.Properties[key] = val
101110
}
102111
default:
103-
return errors.New("mixpanel.Mock only supports the $set operation")
112+
return errors.New("mixpanel.Mock only supports the $set and $set_once operations")
104113
}
105114

106115
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)