mixpal 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mixpal.gemspec
4
+ gemspec
@@ -0,0 +1,5 @@
1
+ guard "rspec", :version => 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch("spec/spec_helper.rb") { "spec" }
5
+ end
@@ -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.
@@ -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
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -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
@@ -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
@@ -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
@@ -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,3 @@
1
+ module Mixpal
2
+ VERSION = "0.0.4"
3
+ 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
@@ -0,0 +1,4 @@
1
+ require "spec_helper"
2
+
3
+ describe Mixpal do
4
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ require File.expand_path("spec/support/mock_storage")
2
+
3
+ class MockRails
4
+ class_attribute :cache
5
+ self.cache = MockStorage.new
6
+ end
@@ -0,0 +1,19 @@
1
+ class MockStorage
2
+ attr_reader :data
3
+
4
+ def initialize
5
+ @data = {}
6
+ end
7
+
8
+ def write(key, value)
9
+ data[key] = value
10
+ end
11
+
12
+ def read(key)
13
+ data[key]
14
+ end
15
+
16
+ def delete(key)
17
+ !!data.delete(key)
18
+ end
19
+ 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