Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,32 @@ encourage your customers to perform an action.
# event. These attributes can be used in your triggers to control who should
# receive the triggered email. You can set any number of data values.

$customerio.track(5, "purchase", :type => "socks", :price => "13.99")
$customerio.track(5, "purchase", { :type => "socks", :price => "13.99" })
```

**Note:** If you want to track events which occurred in the past, you can include a `timestamp` attribute
**Note:** If you want to track events which occurred in the past, you can pass a `timestamp` keyword argument
(in seconds since the epoch), and we'll use that as the date the event occurred.

```ruby
$customerio.track(5, "purchase", :type => "socks", :price => "13.99", :timestamp => 1365436200)
$customerio.track(5, "purchase", { :type => "socks", :price => "13.99" }, timestamp: 1365436200)
```

#### Deduplicating events

You can provide a [ULID](https://github.com/ulid/spec) `id` to deduplicate events. If two events have the same `id`, Customer.io won't process the event a second time.

```ruby
$customerio.track(5, "purchase", { :type => "socks" }, id: "01BX5ZZKBKACTAV9WEVGEMMVRY")

$customerio.track_anonymous("anon-id", "purchase", { :type => "socks" }, id: "01BX5ZZKBKACTAV9WEVGEMMVRY")
```

You can also pass `timestamp` as a keyword argument (epoch seconds) instead of including it in the attributes hash:

```ruby
$customerio.track(5, "purchase", { :type => "socks" }, timestamp: 1561231234)

$customerio.track(5, "purchase", { :type => "socks" }, id: "01BX5ZZKBKACTAV9WEVGEMMVRY", timestamp: 1561231234)
```

### Tracking anonymous events
Expand All @@ -211,7 +229,7 @@ Anonymous events cannot trigger campaigns by themselves. To trigger a campaign,
# name (required) - the name of the event you want to track.
# attributes (optional) - related information you want to attach to the event.

$customerio.track_anonymous(anonymous_id, "product_view", :type => "socks" )
$customerio.track_anonymous(anonymous_id, "product_view", { :type => "socks" })
```

Use the `recipient` attribute to specify the email address to send the messages to. [See our documentation on how to use anonymous events for more details](https://customer.io/docs/invite-emails/).
Expand All @@ -221,7 +239,7 @@ Use the `recipient` attribute to specify the email address to send the messages
If you previously sent [invite events](https://customer.io/docs/anonymous-invite-emails/), you can achieve the same functionality by sending an anonymous event with `nil` for the anonymous identifier. To send anonymous invites, your event *must* include a `recipient` attribute.

```ruby
$customerio.track_anonymous(nil, "invite", :recipient => "new.person@example.com" )
$customerio.track_anonymous(nil, "invite", { :recipient => "new.person@example.com" })
```

### Adding a mobile device
Expand Down
26 changes: 16 additions & 10 deletions lib/customerio/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ def unsuppress(customer_id)
@client.request_and_verify_response(:post, unsuppress_path(customer_id))
end

def track(customer_id, event_name, attributes = {})
def track(customer_id, event_name, attributes = {}, id: nil, timestamp: nil)
raise ParamError, "customer_id must be a non-empty string" if empty?(customer_id)
raise ParamError, "event_name must be a non-empty string" if empty?(event_name)

create_customer_event(customer_id, event_name, attributes)
create_customer_event(customer_id, event_name, attributes, id: id, timestamp: timestamp)
end

def pageview(customer_id, page, attributes = {})
Expand All @@ -66,10 +66,10 @@ def pageview(customer_id, page, attributes = {})
create_pageview_event(customer_id, page, attributes)
end

def track_anonymous(anonymous_id, event_name, attributes = {})
def track_anonymous(anonymous_id, event_name, attributes = {}, id: nil, timestamp: nil)
raise ParamError, "event_name must be a non-empty string" if empty?(event_name)

create_anonymous_event(anonymous_id, event_name, attributes)
create_anonymous_event(anonymous_id, event_name, attributes, id: id, timestamp: timestamp)
end

def add_device(customer_id, device_id, platform, data = {})
Expand Down Expand Up @@ -190,20 +190,24 @@ def create_or_update(attributes = {})
@client.request_and_verify_response(:put, url, attributes)
end

def create_customer_event(customer_id, event_name, attributes = {})
def create_customer_event(customer_id, event_name, attributes = {}, id: nil, timestamp: nil)
create_event(
url: "#{customer_path(customer_id)}/events",
event_name: event_name,
attributes: attributes
attributes: attributes,
id: id,
timestamp: timestamp
)
end

def create_anonymous_event(anonymous_id, event_name, attributes = {})
def create_anonymous_event(anonymous_id, event_name, attributes = {}, id: nil, timestamp: nil)
create_event(
url: "/api/v1/events",
event_name: event_name,
anonymous_id: anonymous_id,
attributes: attributes
attributes: attributes,
id: id,
timestamp: timestamp
)
end

Expand All @@ -216,11 +220,13 @@ def create_pageview_event(customer_id, page, attributes = {})
)
end

def create_event(url:, event_name:, anonymous_id: nil, event_type: nil, attributes: {})
def create_event(url:, event_name:, anonymous_id: nil, event_type: nil, attributes: {}, id: nil, timestamp: nil) # rubocop:disable Metrics/ParameterLists
body = { name: event_name, data: attributes }
body[:timestamp] = attributes[:timestamp] if valid_timestamp?(attributes[:timestamp])
body[:timestamp] = timestamp if valid_timestamp?(timestamp)
body[:timestamp] = attributes[:timestamp] if body[:timestamp].nil? && valid_timestamp?(attributes[:timestamp])
body[:anonymous_id] = anonymous_id unless empty?(anonymous_id)
body[:type] = event_type unless empty?(event_type)
body[:id] = id unless empty?(id)

@client.request_and_verify_response(:post, url, body)
end
Expand Down
110 changes: 91 additions & 19 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def json(data)
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", type: "socks", price: "13.99")
client.track(5, "purchase", {type: "socks", price: "13.99"})
end

it "copes with arrays" do
Expand All @@ -373,7 +373,7 @@ def json(data)
}).
to_return(status: 200, body: "", headers: {})

client.track(5, "event", things: ["a", "b", "c"])
client.track(5, "event", {things: ["a", "b", "c"]})
end

it "copes with hashes" do
Expand All @@ -386,7 +386,7 @@ def json(data)
}).
to_return(status: 200, body: "", headers: {})

client.track(5, "event", stuff: { a: "b" })
client.track(5, "event", {stuff: { a: "b" }})
end

it "sends a POST request as json using json headers" do
Expand All @@ -402,7 +402,22 @@ def json(data)
client.track(5, "purchase", data)
end

it "allows sending of a timestamp" do
it "allows sending of a timestamp via keyword arg" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(body: json({
name: "purchase",
data: {
type: "socks",
price: "13.99"
},
timestamp: 1561231234
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", {type: "socks", price: "13.99"}, timestamp: 1561231234)
end

it "allows sending of a timestamp via attributes for backwards compat" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(body: json({
name: "purchase",
Expand All @@ -415,7 +430,7 @@ def json(data)
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", type: "socks", price: "13.99", timestamp: 1561231234)
client.track(5, "purchase", {type: "socks", price: "13.99", timestamp: 1561231234})
end

it "doesn't send timestamp if timestamp is in milliseconds" do
Expand All @@ -424,13 +439,12 @@ def json(data)
name: "purchase",
data: {
type: "socks",
price: "13.99",
timestamp: 1561231234000
price: "13.99"
}
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", type: "socks", price: "13.99", timestamp: 1561231234000)
client.track(5, "purchase", {type: "socks", price: "13.99"}, timestamp: 1561231234000)
end

it "doesn't send timestamp if timestamp is a date" do
Expand All @@ -441,13 +455,12 @@ def json(data)
name: "purchase",
data: {
type: "socks",
price: "13.99",
timestamp: Time.now.to_s
price: "13.99"
}
}).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", type: "socks", price: "13.99", timestamp: date)
client.track(5, "purchase", {type: "socks", price: "13.99"}, timestamp: date)
end

it "doesn't send timestamp if timestamp isn't an integer" do
Expand All @@ -456,14 +469,61 @@ def json(data)
name: "purchase",
data: {
type: "socks",
price: "13.99",
timestamp: "Hello world"
price: "13.99"
}
})).

to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", type: "socks", price: "13.99", timestamp: "Hello world")
client.track(5, "purchase", {type: "socks", price: "13.99"}, timestamp: "Hello world")
end

it "sends an event id for deduplication when provided" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(body: json({
name: "purchase",
data: { type: "socks" },
id: "01HB4HBDKTFWYZCK01DMRSWRFD"
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", { type: "socks" }, id: "01HB4HBDKTFWYZCK01DMRSWRFD")
end

it "doesn't send id when not provided" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(body: json({
name: "purchase",
data: { type: "socks" }
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", { type: "socks" })
end

it "sends a timestamp as a top-level field when provided as keyword arg" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(body: json({
name: "purchase",
data: { type: "socks" },
timestamp: 1561231234
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", { type: "socks" }, timestamp: 1561231234)
end

it "supports both id and timestamp keyword args together" do
stub_request(:post, api_uri('/api/v1/customers/5/events')).
with(body: json({
name: "purchase",
data: { type: "socks" },
timestamp: 1561231234,
id: "01HB4HBDKTFWYZCK01DMRSWRFD"
})).
to_return(status: 200, body: "", headers: {})

client.track(5, "purchase", { type: "socks" }, id: "01HB4HBDKTFWYZCK01DMRSWRFD", timestamp: 1561231234)
end

context "tracking an anonymous event" do
Expand All @@ -489,24 +549,23 @@ def json(data)
}).
to_return(status: 200, body: "", headers: {})

client.track_anonymous(anon_id, "purchase", type: "socks", price: "13.99")
client.track_anonymous(anon_id, "purchase", { type: "socks", price: "13.99" })
end

it "allows sending of a timestamp" do
it "allows sending of a timestamp via keyword arg" do
stub_request(:post, api_uri('/api/v1/events')).
with(body: {
anonymous_id: anon_id,
name: "purchase",
data: {
type: "socks",
price: "13.99",
timestamp: 1561231234
price: "13.99"
},
timestamp: 1561231234
}).
to_return(status: 200, body: "", headers: {})

client.track_anonymous(anon_id, "purchase", type: "socks", price: "13.99", timestamp: 1561231234)
client.track_anonymous(anon_id, "purchase", { type: "socks", price: "13.99" }, timestamp: 1561231234)
end

it "raises an error if POST doesn't return a 2xx response code" do
Expand Down Expand Up @@ -540,6 +599,19 @@ def json(data)

lambda { client.track_anonymous(anon_id, "") }.should raise_error(Customerio::Client::ParamError)
end

it "sends an event id for deduplication when provided" do
stub_request(:post, api_uri('/api/v1/events')).
with(body: {
anonymous_id: anon_id,
name: "purchase",
data: {},
id: "01HB4HBDKTFWYZCK01DMRSWRFD"
}).
to_return(status: 200, body: "", headers: {})

client.track_anonymous(anon_id, "purchase", {}, id: "01HB4HBDKTFWYZCK01DMRSWRFD")
end
end
end

Expand Down