mixpanel-mail 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in mixpanel-mail.gemspec
4
+ gemspec
5
+
6
+ # Rake!
7
+ gem 'rake'
8
+
9
+ # Development dependencies
10
+ group :development do
11
+ gem "rspec", "~> 2.6.0"
12
+ gem "webmock", "~> 1.7.4"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mixpanel-mail (0.1.0)
5
+ multi_json
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ addressable (2.2.6)
11
+ crack (0.3.1)
12
+ diff-lcs (1.1.3)
13
+ multi_json (1.0.3)
14
+ rake (0.9.2)
15
+ rspec (2.6.0)
16
+ rspec-core (~> 2.6.0)
17
+ rspec-expectations (~> 2.6.0)
18
+ rspec-mocks (~> 2.6.0)
19
+ rspec-core (2.6.4)
20
+ rspec-expectations (2.6.0)
21
+ diff-lcs (~> 1.1.2)
22
+ rspec-mocks (2.6.0)
23
+ webmock (1.7.6)
24
+ addressable (> 2.2.5, ~> 2.2)
25
+ crack (>= 0.1.7)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ mixpanel-mail!
32
+ rake
33
+ rspec (~> 2.6.0)
34
+ webmock (~> 1.7.4)
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :default => :spec
@@ -0,0 +1,53 @@
1
+ unless defined?(ActionMailer)
2
+ raise StandardError, "ActionMailer is not loaded"
3
+ end
4
+
5
+ require 'mixpanel-mail'
6
+ require 'digest/md5'
7
+
8
+ module ActionMailer
9
+ class MixpanelInterceptor
10
+ cattr_accessor :token
11
+
12
+ class << self
13
+ def delivering_email(mail)
14
+ # Skip Mixpanel if the campaign is not specified
15
+ return unless mail.header['mp_campaign']
16
+
17
+ # Skip Mixpanel if we don't have HTML
18
+ html = mail.html_part ? mail.html_part.body : nil
19
+ return unless html.present?
20
+
21
+ # Convert header options to mixpanel options
22
+ opts = ::Mixpanel::Mail::OPTIONS.inject({}) do |sum, key|
23
+ if value = pop_mp_header(mail, key)
24
+ sum[key] = value
25
+ end
26
+ sum
27
+ end
28
+
29
+ # Generate email distinct_id for Mixpanel
30
+ id = Digest::MD5.hexdigest(mail.header['To'].to_s)
31
+
32
+ begin
33
+ mail.html_part.body = mp_mail.add_tracking(id, html, opts)
34
+ rescue => e
35
+ Rails.logger.warn("Failed to Mixpanelize Mail: #{e}")
36
+ mail.html_part.body = html
37
+ end
38
+ end
39
+
40
+ private
41
+ def mp_mail
42
+ @mixpanel_mail ||= Mixpanel::Mail.new(token, 'default')
43
+ end
44
+
45
+ def pop_mp_header(mail, key)
46
+ header_key = "mp_#{key}"
47
+ value = mail.header[header_key]
48
+ mail.header[header_key] = nil if value
49
+ value
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ require "mixpanel_mail/vendor/active_support"
2
+ require "mixpanel_mail/version"
3
+ require 'net/http'
4
+ require 'multi_json'
5
+ require 'uri'
6
+
7
+ #
8
+ # Adopted from http://mixpanel.com/api/docs/guides/email-analytics
9
+ #
10
+
11
+ module Mixpanel
12
+ class Mail
13
+ ENDPOINT = 'http://api.mixpanel.com/email'
14
+ ENDPOINT_URI = URI.parse(ENDPOINT)
15
+ OPTIONS = %w(campaign type properties redirect_host click_tracking)
16
+
17
+ attr_accessor :params
18
+
19
+ def initialize(token, campaign, options = {})
20
+ @params = {}
21
+ params['token'] = token
22
+ params['campaign'] = campaign
23
+ params.merge!(groom_options(options))
24
+ end
25
+
26
+ def add_tracking(distinct_id, body, options = {})
27
+ p = params.dup()
28
+ p['distinct_id'] = distinct_id
29
+ p['body'] = body
30
+ p.merge!(groom_options(options)) unless options.empty?
31
+ response = Net::HTTP.post_form(::Mixpanel::Mail::ENDPOINT_URI, p)
32
+ case response
33
+ when Net::HTTPSuccess
34
+ response.body
35
+ else
36
+ response.error!
37
+ end
38
+ end
39
+
40
+ private
41
+ def groom_options(options)
42
+ opts = options.dup
43
+ opts.stringify_keys!
44
+
45
+ # Limit request to include only valid options
46
+ opts.slice!(*OPTIONS)
47
+
48
+ # Default type is HTML, so we only allow TEXT
49
+ opts.delete('type') unless opts['type'] == 'text'
50
+
51
+ # Marshal properties as JSON
52
+ if opts['properties']
53
+ opts['properties'] = MultiJson.encode(opts['properties'])
54
+ end
55
+
56
+ # Click tracking is enabled by default
57
+ if opts['click_tracking'] == false
58
+ opts['click_tracking'] = '0'
59
+ end
60
+
61
+ opts
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,7 @@
1
+ unless defined?(ActiveSupport)
2
+ $LOAD_PATH << File.dirname(__FILE__)
3
+ require 'active_support/core_ext/hash/keys'
4
+ require 'active_support/core_ext/hash/slice'
5
+ end
6
+
7
+
@@ -0,0 +1,47 @@
1
+ class Hash
2
+ # Return a new hash with all keys converted to strings.
3
+ def stringify_keys
4
+ dup.stringify_keys!
5
+ end
6
+
7
+ # Destructively convert all keys to strings.
8
+ def stringify_keys!
9
+ keys.each do |key|
10
+ self[key.to_s] = delete(key)
11
+ end
12
+ self
13
+ end
14
+
15
+ # Return a new hash with all keys converted to symbols, as long as
16
+ # they respond to +to_sym+.
17
+ def symbolize_keys
18
+ dup.symbolize_keys!
19
+ end
20
+
21
+ # Destructively convert all keys to symbols, as long as they respond
22
+ # to +to_sym+.
23
+ def symbolize_keys!
24
+ keys.each do |key|
25
+ self[(key.to_sym rescue key) || key] = delete(key)
26
+ end
27
+ self
28
+ end
29
+
30
+ alias_method :to_options, :symbolize_keys
31
+ alias_method :to_options!, :symbolize_keys!
32
+
33
+ # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
34
+ # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
35
+ # as keys, this will fail.
36
+ #
37
+ # ==== Examples
38
+ # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years"
39
+ # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key: name"
40
+ # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
41
+ def assert_valid_keys(*valid_keys)
42
+ valid_keys.flatten!
43
+ each_key do |k|
44
+ raise(ArgumentError, "Unknown key: #{k}") unless valid_keys.include?(k)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ class Hash
2
+ # Slice a hash to include only the given keys. This is useful for
3
+ # limiting an options hash to valid keys before passing to a method:
4
+ #
5
+ # def search(criteria = {})
6
+ # assert_valid_keys(:mass, :velocity, :time)
7
+ # end
8
+ #
9
+ # search(options.slice(:mass, :velocity, :time))
10
+ #
11
+ # If you have an array of keys you want to limit to, you should splat them:
12
+ #
13
+ # valid_keys = [:mass, :velocity, :time]
14
+ # search(options.slice(*valid_keys))
15
+ def slice(*keys)
16
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
17
+ hash = self.class.new
18
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
19
+ hash
20
+ end
21
+
22
+ # Replaces the hash with only the given keys.
23
+ # Returns a hash contained the removed key/value pairs
24
+ # {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d => 4}
25
+ def slice!(*keys)
26
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
27
+ omit = slice(*self.keys - keys)
28
+ hash = slice(*keys)
29
+ replace(hash)
30
+ omit
31
+ end
32
+
33
+ # Removes and returns the key/value pairs matching the given keys.
34
+ # {:a => 1, :b => 2, :c => 3, :d => 4}.extract!(:a, :b) # => {:a => 1, :b => 2}
35
+ def extract!(*keys)
36
+ result = {}
37
+ keys.each {|key| result[key] = delete(key) }
38
+ result
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ module Mixpanel
2
+ class Mail
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mixpanel_mail/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mixpanel-mail"
7
+ s.version = Mixpanel::Mail::VERSION
8
+ s.authors = ["Michael Rykov"]
9
+ s.email = ["mrykov@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Helpers for Mixpanel Tracking in Emails}
12
+ s.description = %q{Helpers for Mixpanel Tracking in Emails}
13
+
14
+ s.rubyforge_project = "mixpanel-mail"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # Runtime depencendies
22
+ s.add_runtime_dependency "multi_json"
23
+
24
+ # Development dependencies
25
+ # See Gemfile
26
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mixpanel::Mail do
4
+ TOKEN, CAMPAIGN = 'abcd123', 'my-email'
5
+
6
+ it 'should intialize token & campaign' do
7
+ lambda {
8
+ mail = mp_mail
9
+ mail.params['token'].should eq(TOKEN)
10
+ mail.params['campaign'].should eq(CAMPAIGN)
11
+ }.should_not raise_exception
12
+ end
13
+
14
+ describe 'options grooming' do
15
+ it 'should filter out bad options' do
16
+ mp_option_check('bad', 'woot' => nil)
17
+ end
18
+
19
+ it 'should recognize "type" option' do
20
+ mp_option_check('type', {
21
+ 'text' => 'text',
22
+ 'html' => nil,
23
+ 'bad_bad' => nil
24
+ })
25
+ end
26
+
27
+ it 'should recognize "click_tracking" option' do
28
+ mp_option_check('click_tracking', {
29
+ true => true,
30
+ false => '0'
31
+ })
32
+ end
33
+
34
+ it 'should recognize "properties" option' do
35
+ example = { :hello => :world }
36
+ mp_option_check('properties', {
37
+ nil => nil,
38
+ {} => '{}',
39
+ example => MultiJson.encode(example)
40
+ })
41
+ end
42
+
43
+ it 'should recognize "redirect_host" option' do
44
+ mp_option_check('redirect_host', {
45
+ nil => nil, 'anything' => 'anything'
46
+ })
47
+ end
48
+
49
+ it 'should recognize "campaign" option' do
50
+ mp_option_check('campaign', 'alt-campaign' => 'alt-campaign')
51
+ end
52
+ end
53
+
54
+ describe 'add tracking' do
55
+ it 'should make a simple request' do
56
+ verify_request_with_options
57
+ end
58
+
59
+ it 'should make a request with "type" request' do
60
+ verify_request_with_options(
61
+ { :type => 'text' },
62
+ { 'type' => 'text' }
63
+ )
64
+ end
65
+
66
+ it 'should make a request with "click_tracking" request' do
67
+ verify_request_with_options(
68
+ { :click_tracking => false },
69
+ { 'click_tracking' => '0' }
70
+ )
71
+ end
72
+
73
+ it 'should make a request with "properties" request' do
74
+ props = { :foo => 'bar' }
75
+ verify_request_with_options(
76
+ { :properties => props },
77
+ { 'properties' => MultiJson.encode(props) }
78
+ )
79
+ end
80
+
81
+ it 'should make a request with "redirect_host" request' do
82
+ verify_request_with_options(
83
+ { :redirect_host => 'http://www.google.com' },
84
+ { 'redirect_host' => 'http://www.google.com' }
85
+ )
86
+ end
87
+ end
88
+
89
+ private
90
+ def mp_mail(options = {})
91
+ ::Mixpanel::Mail.new(TOKEN, CAMPAIGN, options)
92
+ end
93
+
94
+ def mp_option_check(key, value_expectations = {})
95
+ value_expectations.each do |value, expectation|
96
+ mp_mail(key => value).params[key].should eq(expectation)
97
+ mp_mail(key.to_sym => value).params[key].should eq(expectation)
98
+ end
99
+ end
100
+
101
+ def verify_request_with_options(options = {}, expectations = {})
102
+ stub_post
103
+ mp_mail(options).add_tracking('my-dist-id', 'Hello World!')
104
+ mp_mail.add_tracking('my-dist-id', 'Hello World!', options)
105
+ a_post.with(:body => expectations.merge(
106
+ 'token' => TOKEN,
107
+ 'campaign' => CAMPAIGN,
108
+ 'distinct_id' => 'my-dist-id',
109
+ 'body' => 'Hello World!'
110
+ )).should have_been_made.twice
111
+ end
112
+ end
@@ -0,0 +1,11 @@
1
+ require 'webmock/rspec'
2
+ require 'mixpanel-mail'
3
+
4
+ # Requires supporting ruby files with custom matchers and macros, etc,
5
+ # in spec/support/ and its subdirectories.
6
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each { |f| require f }
7
+
8
+ RSpec.configure do |config|
9
+ # == Mock Framework
10
+ config.mock_with :rspec
11
+ end
@@ -0,0 +1,10 @@
1
+ # Define a_get, a_post, etc and stub_get, stub_post, etc
2
+ [:delete, :get, :post, :put].each do |method|
3
+ self.class.send(:define_method, "a_#{method}") do
4
+ a_request(method, Mixpanel::Mail::ENDPOINT)
5
+ end
6
+
7
+ self.class.send(:define_method, "stub_#{method}") do
8
+ stub_request(method, Mixpanel::Mail::ENDPOINT)
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mixpanel-mail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Michael Rykov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-27 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
16
+ requirement: &70183731286560 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70183731286560
25
+ description: Helpers for Mixpanel Tracking in Emails
26
+ email:
27
+ - mrykov@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - Gemfile
34
+ - Gemfile.lock
35
+ - Rakefile
36
+ - lib/action_mailer/mixpanel_interceptor.rb
37
+ - lib/mixpanel-mail.rb
38
+ - lib/mixpanel_mail/vendor/active_support.rb
39
+ - lib/mixpanel_mail/vendor/active_support/core_ext/hash/keys.rb
40
+ - lib/mixpanel_mail/vendor/active_support/core_ext/hash/slice.rb
41
+ - lib/mixpanel_mail/version.rb
42
+ - mixpanel-mail.gemspec
43
+ - spec/mixpanel/mail_spec.rb
44
+ - spec/spec_helper.rb
45
+ - spec/support/webmock.rb
46
+ homepage: ''
47
+ licenses: []
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project: mixpanel-mail
66
+ rubygems_version: 1.8.10
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Helpers for Mixpanel Tracking in Emails
70
+ test_files:
71
+ - spec/mixpanel/mail_spec.rb
72
+ - spec/spec_helper.rb
73
+ - spec/support/webmock.rb