mixpanel-mail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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