mixpal 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/Rakefile +1 -0
- data/lib/mixpal.rb +10 -0
- data/lib/mixpal/event.rb +26 -0
- data/lib/mixpal/integration.rb +34 -0
- data/lib/mixpal/tracker.rb +68 -0
- data/lib/mixpal/user.rb +43 -0
- data/lib/mixpal/util.rb +14 -0
- data/lib/mixpal/version.rb +3 -0
- data/mixpanel_assistant.gemspec +33 -0
- data/spec/lib/mixpal/event_spec.rb +48 -0
- data/spec/lib/mixpal/tracker_spec.rb +278 -0
- data/spec/lib/mixpal/user_spec.rb +73 -0
- data/spec/lib/mixpal/util_spec.rb +22 -0
- data/spec/lib/mixpal_spec.rb +4 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/matchers/element_matchers.rb +15 -0
- data/spec/support/mock_rails.rb +6 -0
- data/spec/support/mock_storage.rb +19 -0
- metadata +248 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 patbenatar
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# Mixpal
|
2
|
+
|
3
|
+
As the JavaScript library is Mixpanel's preferred method of usage,
|
4
|
+
Mixpal aims to make it easier to work with from your Rails backend.
|
5
|
+
Most notably it persists tracking data across redirects, perfect for handling
|
6
|
+
events like user sign ups or form submissions.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
### With Bundler
|
11
|
+
|
12
|
+
1. Add to Gemfile: `gem "mixpal"`
|
13
|
+
1. `$ bundle`
|
14
|
+
|
15
|
+
### Standalone
|
16
|
+
|
17
|
+
```bash
|
18
|
+
$ gem install mixpal
|
19
|
+
```
|
20
|
+
|
21
|
+
## Setup
|
22
|
+
|
23
|
+
### In your controller
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class ApplicationController < ActionController::Base
|
27
|
+
include Mixpal::Integration
|
28
|
+
mixpanel_identity :current_user, :email
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
`mixpanel_identity` tells Mixpal how to identify your users. This
|
33
|
+
is used to alias and identify with Mixpanel. The first arg should be a method
|
34
|
+
on this controller that returns an object to which we can send the second arg.
|
35
|
+
In this example, we'll identify our user by `current_user.email`.
|
36
|
+
|
37
|
+
### In your layout
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
<%= mixpanel.render() %>
|
41
|
+
```
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
Mixpal exposes its helpers to your controllers, views, and view helpers.
|
46
|
+
|
47
|
+
### Tracking Events
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
mixpanel.track "Event Name", property_1: "A string", property_2: true
|
51
|
+
```
|
52
|
+
|
53
|
+
### Registering New Users
|
54
|
+
|
55
|
+
When a new user signs up, you want to create their profile on Mixpanel as well
|
56
|
+
as alias all event data to their identifier. As per Mixpanel's docs, you should
|
57
|
+
only do this once per user.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
mixpanel.register_user user.attributes.slice("name", "email")
|
61
|
+
```
|
62
|
+
|
63
|
+
`register_user` will attempt to identify and convert the following properties to
|
64
|
+
Mixpanel "special properties": `name`, `email`, and `created_at`.
|
65
|
+
|
66
|
+
### Updating Existing Users
|
67
|
+
|
68
|
+
When a user changes their profile...
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
mixpanel.update_user email: "mynewemail@example.com"
|
72
|
+
```
|
73
|
+
|
74
|
+
As with `register_user`, this method will also identify "special properties".
|
75
|
+
|
76
|
+
### Persistance Across Redirects
|
77
|
+
|
78
|
+
Mixpal stores any tracked events or user data in `Rails.cache` when
|
79
|
+
it detects a redirect so it can output the appropriate Mixpanel JS integration
|
80
|
+
code to the client on the following render. This enables us to do cool things
|
81
|
+
like:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
class UsersController < ActionController::Base
|
85
|
+
def create
|
86
|
+
# ... do cool stuff ...
|
87
|
+
mixpanel.register_user name: @user.name, email: @user.email
|
88
|
+
redirect_to root_path
|
89
|
+
end
|
90
|
+
|
91
|
+
def update
|
92
|
+
# ... more cool stuff! ...
|
93
|
+
|
94
|
+
mixpanel.update_user name: @user.name
|
95
|
+
mixpanel.track "Profile Updated"
|
96
|
+
|
97
|
+
redirect_to root_path
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Customizing the storage adapter
|
103
|
+
|
104
|
+
You can specify a custom persistence storage adapter like so:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
Mixpal::Tracker.storage = MyCustomAdapter.new
|
108
|
+
```
|
109
|
+
|
110
|
+
Storage adapters must implement the following API: `write(key, value)`,
|
111
|
+
`read(key)`, and `delete(key)`.
|
112
|
+
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
1. Fork it
|
116
|
+
1. Create your feature branch (`git checkout -b feature/my-new-feature`)
|
117
|
+
1. Make your changes
|
118
|
+
1. Add accompanying tests
|
119
|
+
1. Commit your changes (`git commit -am 'Add some feature'`)
|
120
|
+
1. Push to the branch (`git push origin feature/my-new-feature`)
|
121
|
+
1. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/mixpal.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "mixpal/version"
|
2
|
+
require "active_support/core_ext"
|
3
|
+
|
4
|
+
module Mixpal
|
5
|
+
autoload :Util, "mixpal/util"
|
6
|
+
autoload :Tracker, "mixpal/tracker"
|
7
|
+
autoload :Event, "mixpal/event"
|
8
|
+
autoload :User, "mixpal/user"
|
9
|
+
autoload :Integration, "mixpal/integration"
|
10
|
+
end
|
data/lib/mixpal/event.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Mixpal
|
2
|
+
class Event
|
3
|
+
attr_reader :name, :properties
|
4
|
+
|
5
|
+
def initialize(name, properties)
|
6
|
+
@name = name
|
7
|
+
@properties = properties
|
8
|
+
end
|
9
|
+
|
10
|
+
def render
|
11
|
+
js_object = Mixpal::Util.hash_to_js_object_string(properties)
|
12
|
+
"mixpanel.track(\"#{name}\", #{js_object});".html_safe
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_store
|
16
|
+
{
|
17
|
+
name: name,
|
18
|
+
properties: properties,
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_store(data)
|
23
|
+
new(data[:name], data[:properties])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mixpal
|
2
|
+
module Integration
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
helper_method :mixpanel
|
7
|
+
after_filter :store_mixpanel_if_redirecting
|
8
|
+
|
9
|
+
class_attribute :mixpanel_identity_data
|
10
|
+
def self.mixpanel_identity(object_method, attribute_method)
|
11
|
+
self.mixpanel_identity_data = {
|
12
|
+
object_method: object_method,
|
13
|
+
attribute_method: attribute_method,
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def mixpanel
|
19
|
+
@mixpanel ||= begin
|
20
|
+
identity = if data = self.class.mixpanel_identity_data
|
21
|
+
send(data[:object_method]).try(data[:attribute_method])
|
22
|
+
end
|
23
|
+
|
24
|
+
Mixpal::Tracker.new(identity: identity)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def store_mixpanel_if_redirecting
|
31
|
+
mixpanel.store! if status == 302
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Mixpal
|
2
|
+
class Tracker
|
3
|
+
attr_reader :events, :user_updates, :identity, :alias_user
|
4
|
+
|
5
|
+
STORAGE_KEY = "mixpal"
|
6
|
+
class_attribute :storage
|
7
|
+
self.storage = Rails.cache
|
8
|
+
|
9
|
+
def initialize(args={})
|
10
|
+
@events = []
|
11
|
+
@user_updates = []
|
12
|
+
|
13
|
+
restore!
|
14
|
+
|
15
|
+
@identity = args[:identity]
|
16
|
+
end
|
17
|
+
|
18
|
+
def register_user(properties)
|
19
|
+
@alias_user = true
|
20
|
+
update_user(properties)
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_user(properties)
|
24
|
+
user_updates << Mixpal::User.new(properties)
|
25
|
+
end
|
26
|
+
|
27
|
+
def track(name, properties={})
|
28
|
+
events << Mixpal::Event.new(name, properties)
|
29
|
+
end
|
30
|
+
|
31
|
+
def render
|
32
|
+
"".tap do |html|
|
33
|
+
html << "<script type=\"text/javascript\">"
|
34
|
+
html << "mixpanel.alias(\"#{identity}\");" if alias_user
|
35
|
+
html << events.map(&:render).join("")
|
36
|
+
html << user_updates.map(&:render).join("")
|
37
|
+
html << "mixpanel.identify(\"#{identity}\");" if identity
|
38
|
+
html << "</script>"
|
39
|
+
end.html_safe
|
40
|
+
end
|
41
|
+
|
42
|
+
def store!
|
43
|
+
self.class.storage.write(STORAGE_KEY, to_store)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def restore!
|
49
|
+
data = self.class.storage.read(STORAGE_KEY) || {}
|
50
|
+
|
51
|
+
@alias_user = data[:alias_user]
|
52
|
+
@identity = data[:identity]
|
53
|
+
@events = data[:events].map { |e| Mixpal::Event.from_store(e) } if data[:events]
|
54
|
+
@user_updates = data[:user_updates].map { |u| Mixpal::User.from_store(u) } if data[:user_updates]
|
55
|
+
|
56
|
+
self.class.storage.delete(STORAGE_KEY)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_store
|
60
|
+
{
|
61
|
+
alias_user: @alias_user,
|
62
|
+
identity: identity,
|
63
|
+
events: events.map(&:to_store),
|
64
|
+
user_updates: user_updates.map(&:to_store),
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/mixpal/user.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Mixpal
|
2
|
+
class User
|
3
|
+
attr_reader :properties
|
4
|
+
|
5
|
+
def initialize(properties)
|
6
|
+
@properties = properties
|
7
|
+
end
|
8
|
+
|
9
|
+
def render
|
10
|
+
"mixpanel.people.set(#{properties_as_js_object_for_mixpanel});".html_safe
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_store
|
14
|
+
{
|
15
|
+
properties: properties,
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_store(data)
|
20
|
+
new(data[:properties])
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def properties_as_js_object_for_mixpanel
|
26
|
+
Mixpal::Util.hash_to_js_object_string(properties_for_mixpanel)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Isolate special properties and rename their keys to align with
|
30
|
+
# Mixpanel's naming.
|
31
|
+
def properties_for_mixpanel
|
32
|
+
Hash[properties.map {|k, v| [mixpanel_special_properties_map[k] || k, v] }]
|
33
|
+
end
|
34
|
+
|
35
|
+
def mixpanel_special_properties_map
|
36
|
+
{
|
37
|
+
name: "$name",
|
38
|
+
email: "$email",
|
39
|
+
created_at: "$created",
|
40
|
+
}.with_indifferent_access
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/mixpal/util.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Mixpal
|
2
|
+
module Util
|
3
|
+
class << self
|
4
|
+
def hash_to_js_object_string(hash)
|
5
|
+
contents = hash.map do |k,v|
|
6
|
+
js_value = v.is_a?(String) || v.is_a?(Time) ? "\"#{v}\"" : v
|
7
|
+
"\"#{k}\": #{js_value}"
|
8
|
+
end.join(",").html_safe
|
9
|
+
|
10
|
+
"{#{contents}}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mixpal/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mixpal"
|
8
|
+
spec.version = Mixpal::VERSION
|
9
|
+
spec.authors = ["patbenatar"]
|
10
|
+
spec.email = ["nick@gophilosophie.com"]
|
11
|
+
spec.description = "Use Mixpanel's JavaScript library from your backend with ease"
|
12
|
+
spec.summary = "As the JavaScript library is Mixpanel's preferred method of usage, Mixpal aims to make it easier to work with from your Rails backend. Most notably it persists tracking data across redirects, perfect for handling events like user sign ups or form submissions."
|
13
|
+
spec.homepage = "https://github.com/patbenatar/mixpal"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "pry"
|
24
|
+
spec.add_development_dependency "rspec", "~> 2.14.0"
|
25
|
+
spec.add_development_dependency "guard-rspec", "~> 3.0.3"
|
26
|
+
spec.add_development_dependency "rb-fsevent", "~> 0.9.3"
|
27
|
+
spec.add_development_dependency "awesome_print", "~> 1.1.0"
|
28
|
+
spec.add_development_dependency "nokogiri", "~> 1.6.0"
|
29
|
+
|
30
|
+
spec.add_development_dependency "actionpack", ">= 3.0"
|
31
|
+
|
32
|
+
spec.add_dependency "activesupport", ">= 3.0"
|
33
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Mixpal::Event do
|
4
|
+
let(:name) { "Event 1" }
|
5
|
+
let(:properties) { { title: "Awesome Product" } }
|
6
|
+
subject { described_class.new(name, properties) }
|
7
|
+
|
8
|
+
describe "#render" do
|
9
|
+
it "delegates to Util for js_object composition" do
|
10
|
+
Mixpal::Util.should_receive(:hash_to_js_object_string).with(properties)
|
11
|
+
subject.render
|
12
|
+
end
|
13
|
+
|
14
|
+
it "outputs a call to track" do
|
15
|
+
js_object = Mixpal::Util.hash_to_js_object_string(properties)
|
16
|
+
expect(subject.render).to eq "mixpanel.track(\"#{name}\", #{js_object});"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "outputs an html safe string" do
|
20
|
+
expect(subject.render).to be_html_safe
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#to_store" do
|
25
|
+
it "returns a hash with its data" do
|
26
|
+
expect(subject.to_store).to eq(
|
27
|
+
name: name,
|
28
|
+
properties: properties,
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ".from_store" do
|
34
|
+
let(:result) { described_class.from_store(name: name, properties: properties) }
|
35
|
+
|
36
|
+
it "instantiates a new instance" do
|
37
|
+
expect(result).to be_an_instance_of(described_class)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "sets its name from the data" do
|
41
|
+
expect(result.name).to eq name
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sets its properties from the data" do
|
45
|
+
expect(result.properties).to eq properties
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Mixpal::Tracker do
|
4
|
+
subject { Mixpal::Tracker.new }
|
5
|
+
let(:identity) { "nick" }
|
6
|
+
let(:subject_with_identity) { Mixpal::Tracker.new(identity: identity) }
|
7
|
+
|
8
|
+
describe "custom storage adapter" do
|
9
|
+
before do
|
10
|
+
@original_adapter = described_class.storage
|
11
|
+
|
12
|
+
MyCustomStorageAdapter = Class.new(MockStorage)
|
13
|
+
@adapter_instance = MyCustomStorageAdapter.new
|
14
|
+
|
15
|
+
described_class.storage = @adapter_instance
|
16
|
+
end
|
17
|
+
|
18
|
+
after { described_class.storage = @original_adapter }
|
19
|
+
|
20
|
+
it "uses the specified adapter" do
|
21
|
+
@adapter_instance.should_receive :write
|
22
|
+
subject.store!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#initialize" do
|
27
|
+
it "creates an empty set of events" do
|
28
|
+
expect(subject.events).to eq []
|
29
|
+
end
|
30
|
+
|
31
|
+
it "creates an empty set of user_updates" do
|
32
|
+
expect(subject.user_updates).to eq []
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with an :identity arg" do
|
36
|
+
subject { subject_with_identity }
|
37
|
+
|
38
|
+
it "sets the identity" do
|
39
|
+
expect(subject.identity).to eq "nick"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "when data exists in storage" do
|
44
|
+
let(:old_tracker) { Mixpal::Tracker.new(identity: identity) }
|
45
|
+
|
46
|
+
before do
|
47
|
+
old_tracker.track "Event 1"
|
48
|
+
old_tracker.register_user name: "Nick Giancola"
|
49
|
+
old_tracker.store!
|
50
|
+
end
|
51
|
+
|
52
|
+
it "restores the alias_user property" do
|
53
|
+
expect(subject.alias_user).to eq true
|
54
|
+
end
|
55
|
+
|
56
|
+
it "restores the events" do
|
57
|
+
expect(subject.events.size).to eq 1
|
58
|
+
end
|
59
|
+
|
60
|
+
it "delegates event restoration to the Event class" do
|
61
|
+
Mixpal::Event.should_receive(:from_store).
|
62
|
+
with(old_tracker.events.first.to_store)
|
63
|
+
|
64
|
+
subject
|
65
|
+
end
|
66
|
+
|
67
|
+
it "restores the events" do
|
68
|
+
expect(subject.events.size).to eq 1
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when initialized with an identity" do
|
72
|
+
subject { Mixpal::Tracker.new(identity: "Franky") }
|
73
|
+
|
74
|
+
it "overrides anything from storage" do
|
75
|
+
expect(subject.identity).to eq "Franky"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "#register_user" do
|
82
|
+
it "sets the alias_user flag so we render the alias call" do
|
83
|
+
subject.register_user(name: "Nick")
|
84
|
+
expect(subject.alias_user).to be_true
|
85
|
+
end
|
86
|
+
|
87
|
+
it "delegates to #update_user for tracking user properties" do
|
88
|
+
properties = { name: "Nick" }
|
89
|
+
subject.should_receive(:update_user).with(properties)
|
90
|
+
subject.register_user(properties)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#update_user" do
|
95
|
+
it "instantiates a new User object with properties" do
|
96
|
+
properties = { name: "Nick" }
|
97
|
+
Mixpal::User.should_receive(:new).with(properties)
|
98
|
+
subject.update_user(properties)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "adds the User to user_updates for rendering later" do
|
102
|
+
expect do
|
103
|
+
subject.update_user(name: "Nick")
|
104
|
+
end.to change(subject.user_updates, :size).by(1)
|
105
|
+
|
106
|
+
subject.user_updates.first.should be_an_instance_of(Mixpal::User)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#track" do
|
111
|
+
it "instantiates a new Event object with properties" do
|
112
|
+
name = "Clicked Button"
|
113
|
+
properties = { color: "Green" }
|
114
|
+
|
115
|
+
Mixpal::Event.should_receive(:new).with(name, properties)
|
116
|
+
subject.track(name, properties)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "adds the Event to events for rendering later" do
|
120
|
+
expect do
|
121
|
+
subject.track("Clicked Button", color: "Green")
|
122
|
+
end.to change(subject.events, :size).by(1)
|
123
|
+
|
124
|
+
subject.events.first.should be_an_instance_of(Mixpal::Event)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "#render" do
|
129
|
+
it "outputs script tag" do
|
130
|
+
expect(subject.render).to have_tag("script")
|
131
|
+
end
|
132
|
+
|
133
|
+
it "outputs an html safe string" do
|
134
|
+
expect(subject.render).to be_html_safe
|
135
|
+
end
|
136
|
+
|
137
|
+
context "with an identity" do
|
138
|
+
subject { subject_with_identity }
|
139
|
+
|
140
|
+
it "outputs call to identify" do
|
141
|
+
expect(subject.render).to include "mixpanel.identify(\"#{identity}\");"
|
142
|
+
end
|
143
|
+
|
144
|
+
context "when user is being registered" do
|
145
|
+
before { subject.register_user({ name: "Nick Giancola" }) }
|
146
|
+
|
147
|
+
it "outputs call to alias by identity" do
|
148
|
+
expect(subject.render).to include "mixpanel.alias(\"#{identity}\");"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "with no registered user" do
|
154
|
+
it "does not output call to alias" do
|
155
|
+
expect(subject.render).not_to include "mixpanel.alias"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
context "without an identity" do
|
160
|
+
it "does not output call to identify" do
|
161
|
+
expect(subject.render).not_to include "mixpanel.indentify"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "with tracked events" do
|
166
|
+
before do
|
167
|
+
subject.track("Event 1", { color: "Green" })
|
168
|
+
subject.track("Event 2", { title: "Something Awesome" })
|
169
|
+
end
|
170
|
+
|
171
|
+
it "delegates render to the events" do
|
172
|
+
subject.events.each { |event| event.should_receive :render }
|
173
|
+
subject.render
|
174
|
+
end
|
175
|
+
|
176
|
+
it "joins each rendered event" do
|
177
|
+
joined = subject.events[0].render + subject.events[1].render
|
178
|
+
expect(subject.render).to include joined
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context "with user properties" do
|
183
|
+
before do
|
184
|
+
subject.update_user({ name: "Hank" })
|
185
|
+
subject.update_user({ location: "Los Angeles" })
|
186
|
+
end
|
187
|
+
|
188
|
+
it "delegates render to the users" do
|
189
|
+
subject.user_updates.each { |user| user.should_receive :render }
|
190
|
+
subject.render
|
191
|
+
end
|
192
|
+
|
193
|
+
it "joins each rendered user" do
|
194
|
+
joined = subject.user_updates[0].render + subject.user_updates[1].render
|
195
|
+
expect(subject.render).to include joined
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "#store!" do
|
201
|
+
let(:storage) { described_class::storage }
|
202
|
+
|
203
|
+
after { MockRails.cache.delete(described_class::STORAGE_KEY) }
|
204
|
+
|
205
|
+
def storage_should_include(hash_fragment)
|
206
|
+
storage.should_receive(:write).with(
|
207
|
+
described_class::STORAGE_KEY,
|
208
|
+
hash_including(hash_fragment)
|
209
|
+
)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "writes to the storage adapter" do
|
213
|
+
storage.should_receive(:write)
|
214
|
+
subject.store!
|
215
|
+
end
|
216
|
+
|
217
|
+
context "when alias_user is set" do
|
218
|
+
before { subject.register_user({}) }
|
219
|
+
|
220
|
+
it "stores the alias_user property" do
|
221
|
+
storage_should_include(alias_user: true)
|
222
|
+
subject.store!
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
context "when identity is set" do
|
227
|
+
subject { subject_with_identity }
|
228
|
+
|
229
|
+
it "stores the identity" do
|
230
|
+
storage_should_include(identity: identity)
|
231
|
+
subject.store!
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context "when events have been tracked" do
|
236
|
+
before do
|
237
|
+
subject.track("Event 1", { color: "Green" })
|
238
|
+
subject.track("Event 2", { title: "Something Awesome" })
|
239
|
+
end
|
240
|
+
|
241
|
+
it "delegates composition to the events" do
|
242
|
+
subject.events.each { |event| event.should_receive :to_store }
|
243
|
+
subject.store!
|
244
|
+
end
|
245
|
+
|
246
|
+
it "stores the events' composed hashes in an array" do
|
247
|
+
storage_should_include(
|
248
|
+
events: [subject.events[0].to_store, subject.events[1].to_store]
|
249
|
+
)
|
250
|
+
|
251
|
+
subject.store!
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
context "when user properties have been updated" do
|
256
|
+
before do
|
257
|
+
subject.update_user({ name: "Hank" })
|
258
|
+
subject.update_user({ location: "Los Angeles" })
|
259
|
+
end
|
260
|
+
|
261
|
+
it "delegates composition to the users" do
|
262
|
+
subject.user_updates.each { |user| user.should_receive :to_store }
|
263
|
+
subject.store!
|
264
|
+
end
|
265
|
+
|
266
|
+
it "stores the users' composed hashes in an array" do
|
267
|
+
storage_should_include(
|
268
|
+
user_updates: [
|
269
|
+
subject.user_updates[0].to_store,
|
270
|
+
subject.user_updates[1].to_store
|
271
|
+
]
|
272
|
+
)
|
273
|
+
|
274
|
+
subject.store!
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Mixpal::User do
|
4
|
+
let(:properties) { { random_property: "Hansel", another_random_one: "So Hot Right Now" } }
|
5
|
+
let(:subject) { described_class.new(properties) }
|
6
|
+
|
7
|
+
describe "#render" do
|
8
|
+
it "delegates to Util for js_object composition" do
|
9
|
+
Mixpal::Util.should_receive(:hash_to_js_object_string).with(properties)
|
10
|
+
subject.render
|
11
|
+
end
|
12
|
+
|
13
|
+
it "outputs a call to people.set" do
|
14
|
+
js_object = Mixpal::Util.hash_to_js_object_string(properties)
|
15
|
+
expect(subject.render).to eq "mixpanel.people.set(#{js_object});"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "outputs an html safe string" do
|
19
|
+
expect(subject.render).to be_html_safe
|
20
|
+
end
|
21
|
+
|
22
|
+
context "with Mixpanel special properties" do
|
23
|
+
let(:properties) do
|
24
|
+
{
|
25
|
+
name: "Nick Giancola",
|
26
|
+
email: "nick@gophilosophie.com",
|
27
|
+
created_at: Time.now,
|
28
|
+
random_property: "Hansel",
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "converts name => $name" do
|
33
|
+
expect(subject.render).to include "\"$name\""
|
34
|
+
expect(subject.render).not_to include "\"name\""
|
35
|
+
end
|
36
|
+
|
37
|
+
it "converts email => $email" do
|
38
|
+
expect(subject.render).to include "\"$email\""
|
39
|
+
expect(subject.render).not_to include "\"email\""
|
40
|
+
end
|
41
|
+
|
42
|
+
it "converts created_at => $created" do
|
43
|
+
expect(subject.render).to include "\"$created\""
|
44
|
+
expect(subject.render).not_to include "\"created_at\""
|
45
|
+
end
|
46
|
+
|
47
|
+
it "leaves other properties untouched" do
|
48
|
+
expect(subject.render).to include "\"random_property\""
|
49
|
+
expect(subject.render).not_to include "\"$random_property\""
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#to_store" do
|
55
|
+
it "returns a hash with its data" do
|
56
|
+
expect(subject.to_store).to eq(
|
57
|
+
properties: properties,
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#from_store" do
|
63
|
+
let(:result) { described_class.from_store(properties: properties) }
|
64
|
+
|
65
|
+
it "instantiates a new instance" do
|
66
|
+
expect(result).to be_an_instance_of(described_class)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "sets its properties from the data" do
|
70
|
+
expect(result.properties).to eq properties
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Mixpal::Util do
|
4
|
+
subject { described_class }
|
5
|
+
|
6
|
+
describe ".hash_to_js_object_string" do
|
7
|
+
it "converts a ruby hash to a string representation of a javascript object" do
|
8
|
+
expect(subject.hash_to_js_object_string({key: "value", another: "more value"})).
|
9
|
+
to eq "{\"key\": \"value\",\"another\": \"more value\"}"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "leaves Booleans intact to be interpreted as JS Boolean" do
|
13
|
+
expect(subject.hash_to_js_object_string({ is_cool: true })).
|
14
|
+
to eq "{\"is_cool\": true}"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "leaves Fixnums intact to be interpreted as JS Numbers" do
|
18
|
+
expect(subject.hash_to_js_object_string({ age: 21 })).
|
19
|
+
to eq "{\"age\": 21}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler.require
|
3
|
+
|
4
|
+
require "mixpal"
|
5
|
+
|
6
|
+
Dir["./spec/support/**/*.rb"].each {|f| require f }
|
7
|
+
|
8
|
+
Rails = MockRails
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.filter_run :focus
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
|
3
|
+
RSpec::Matchers.define :have_tag do |tag|
|
4
|
+
match do |string|
|
5
|
+
document = Nokogiri::HTML(string.to_s)
|
6
|
+
!!document.at_css(tag)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec::Matchers.define :have_xpath do |tag|
|
11
|
+
match do |string|
|
12
|
+
document = Nokogiri::HTML(string.to_s)
|
13
|
+
!!document.at_xpath(tag)
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mixpal
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.4
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- patbenatar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-09-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
prerelease: false
|
16
|
+
name: bundler
|
17
|
+
type: :development
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '1.3'
|
23
|
+
none: false
|
24
|
+
requirement: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '1.3'
|
29
|
+
none: false
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
prerelease: false
|
32
|
+
name: rake
|
33
|
+
type: :development
|
34
|
+
version_requirements: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
none: false
|
40
|
+
requirement: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
none: false
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
prerelease: false
|
48
|
+
name: pry
|
49
|
+
type: :development
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
none: false
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
none: false
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
prerelease: false
|
64
|
+
name: rspec
|
65
|
+
type: :development
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ~>
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.14.0
|
71
|
+
none: false
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.14.0
|
77
|
+
none: false
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
prerelease: false
|
80
|
+
name: guard-rspec
|
81
|
+
type: :development
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ~>
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 3.0.3
|
87
|
+
none: false
|
88
|
+
requirement: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ~>
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 3.0.3
|
93
|
+
none: false
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
prerelease: false
|
96
|
+
name: rb-fsevent
|
97
|
+
type: :development
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ~>
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 0.9.3
|
103
|
+
none: false
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ~>
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 0.9.3
|
109
|
+
none: false
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
prerelease: false
|
112
|
+
name: awesome_print
|
113
|
+
type: :development
|
114
|
+
version_requirements: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ~>
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 1.1.0
|
119
|
+
none: false
|
120
|
+
requirement: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.1.0
|
125
|
+
none: false
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
prerelease: false
|
128
|
+
name: nokogiri
|
129
|
+
type: :development
|
130
|
+
version_requirements: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ~>
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: 1.6.0
|
135
|
+
none: false
|
136
|
+
requirement: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ~>
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: 1.6.0
|
141
|
+
none: false
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
prerelease: false
|
144
|
+
name: actionpack
|
145
|
+
type: :development
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '3.0'
|
151
|
+
none: false
|
152
|
+
requirement: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ! '>='
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '3.0'
|
157
|
+
none: false
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
prerelease: false
|
160
|
+
name: activesupport
|
161
|
+
type: :runtime
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '3.0'
|
167
|
+
none: false
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ! '>='
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '3.0'
|
173
|
+
none: false
|
174
|
+
description: Use Mixpanel's JavaScript library from your backend with ease
|
175
|
+
email:
|
176
|
+
- nick@gophilosophie.com
|
177
|
+
executables: []
|
178
|
+
extensions: []
|
179
|
+
extra_rdoc_files: []
|
180
|
+
files:
|
181
|
+
- .gitignore
|
182
|
+
- .rspec
|
183
|
+
- Gemfile
|
184
|
+
- Guardfile
|
185
|
+
- LICENSE.txt
|
186
|
+
- README.md
|
187
|
+
- Rakefile
|
188
|
+
- lib/mixpal.rb
|
189
|
+
- lib/mixpal/event.rb
|
190
|
+
- lib/mixpal/integration.rb
|
191
|
+
- lib/mixpal/tracker.rb
|
192
|
+
- lib/mixpal/user.rb
|
193
|
+
- lib/mixpal/util.rb
|
194
|
+
- lib/mixpal/version.rb
|
195
|
+
- mixpanel_assistant.gemspec
|
196
|
+
- spec/lib/mixpal/event_spec.rb
|
197
|
+
- spec/lib/mixpal/tracker_spec.rb
|
198
|
+
- spec/lib/mixpal/user_spec.rb
|
199
|
+
- spec/lib/mixpal/util_spec.rb
|
200
|
+
- spec/lib/mixpal_spec.rb
|
201
|
+
- spec/spec_helper.rb
|
202
|
+
- spec/support/matchers/element_matchers.rb
|
203
|
+
- spec/support/mock_rails.rb
|
204
|
+
- spec/support/mock_storage.rb
|
205
|
+
homepage: https://github.com/patbenatar/mixpal
|
206
|
+
licenses:
|
207
|
+
- MIT
|
208
|
+
post_install_message:
|
209
|
+
rdoc_options: []
|
210
|
+
require_paths:
|
211
|
+
- lib
|
212
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
213
|
+
requirements:
|
214
|
+
- - ! '>='
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
segments:
|
217
|
+
- 0
|
218
|
+
hash: 4327721075013694130
|
219
|
+
version: '0'
|
220
|
+
none: false
|
221
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
222
|
+
requirements:
|
223
|
+
- - ! '>='
|
224
|
+
- !ruby/object:Gem::Version
|
225
|
+
segments:
|
226
|
+
- 0
|
227
|
+
hash: 4327721075013694130
|
228
|
+
version: '0'
|
229
|
+
none: false
|
230
|
+
requirements: []
|
231
|
+
rubyforge_project:
|
232
|
+
rubygems_version: 1.8.23
|
233
|
+
signing_key:
|
234
|
+
specification_version: 3
|
235
|
+
summary: As the JavaScript library is Mixpanel's preferred method of usage, Mixpal
|
236
|
+
aims to make it easier to work with from your Rails backend. Most notably it persists
|
237
|
+
tracking data across redirects, perfect for handling events like user sign ups or
|
238
|
+
form submissions.
|
239
|
+
test_files:
|
240
|
+
- spec/lib/mixpal/event_spec.rb
|
241
|
+
- spec/lib/mixpal/tracker_spec.rb
|
242
|
+
- spec/lib/mixpal/user_spec.rb
|
243
|
+
- spec/lib/mixpal/util_spec.rb
|
244
|
+
- spec/lib/mixpal_spec.rb
|
245
|
+
- spec/spec_helper.rb
|
246
|
+
- spec/support/matchers/element_matchers.rb
|
247
|
+
- spec/support/mock_rails.rb
|
248
|
+
- spec/support/mock_storage.rb
|