converted all analytics to use new google analytics, removing mixpanel

and heap. Not tracking subscriptions or cancelations yet.
This commit is contained in:
Henry Oswald 2014-03-08 18:31:07 +00:00
parent 38d3bbb556
commit f2069c6208
27 changed files with 21 additions and 236 deletions

View file

@ -1,69 +0,0 @@
Settings = require 'settings-sharelatex'
if Settings.analytics?.mixpanel?
Mixpanel = require("mixpanel").init(Settings.analytics.mixpanel.token)
else
Mixpanel = null
logger = require "logger-sharelatex"
async = require 'async'
module.exports = AnalyticsManager =
track: (user, event, properties, callback = (error)->) ->
properties.distinct_id = @getDistinctId user
properties.mp_name_tag = user.email if user.email?
logger.log user_id: properties.distinct_id, event: event, properties: properties, "tracking event"
Mixpanel?.track event, properties
callback()
set: (user, properties, callback = (error)->) ->
properties["$first_name"] = user.first_name if user.first_name?
properties["$last_name"] = user.last_name if user.last_name?
properties["$email"] = user.email if user.email?
Mixpanel?.people.set @getDistinctId(user), properties
callback()
increment: (user, property, amount, callback = (error)->) ->
Mixpanel?.people.increment @getDistinctId(user), property, amount
callback()
# TODO: Remove this one month after the ability to start free trials was removed
trackFreeTrialExpired: (user, callback = (error)->) ->
async.series [
(callback) => @track user, "free trial expired", {}, callback
(callback) => @set user, { free_trial_expired_at: new Date() }, callback
], callback
trackSubscriptionStarted: (user, plan_code, callback = (error)->) ->
async.series [
(callback) => @track user, "subscribed", plan_code: plan_code, callback
(callback) => @set user, { plan_code: plan_code, subscribed_at: new Date() }, callback
], callback
trackSubscriptionCancelled: (user, callback = (error)->) ->
async.series [
(callback) => @track user, "cancelled", callback
(callback) => @set user, { cancelled_at: new Date() }, callback
], callback
trackLogIn: (user, callback = (error)->) ->
async.series [
(callback) => @track user, "logged in", {}, callback
(callback) => @set user, { last_logged_id: new Date() }, callback
], callback
trackOpenEditor: (user, project, callback = (error)->) ->
async.series [
(callback) => @set user, { last_opened_editor: new Date() }, callback
(callback) => @increment user, "editor_opens", 1, callback
], callback
trackReferral: (user, referal_source, referal_medium, callback = (error) ->) ->
async.series [
(callback) =>
@track user, "Referred another user", { source: referal_source, medium: referal_medium }, callback
(callback) =>
@track user, "Referred another user via #{referal_source}", { medium: referal_medium }, callback
], callback
getDistinctId: (user) -> user.id || user._id || user

View file

@ -12,7 +12,6 @@ LimitationsManager = require("../Subscription/LimitationsManager")
AuthorizationManager = require("../Security/AuthorizationManager")
AutomaticSnapshotManager = require("../Versioning/AutomaticSnapshotManager")
VersioningApiHandler = require("../Versioning/VersioningApiHandler")
AnalyticsManager = require("../Analytics/AnalyticsManager")
EditorRealTimeController = require("./EditorRealTimeController")
settings = require('settings-sharelatex')
slReqIdHelper = require('soa-req-id')
@ -47,7 +46,6 @@ module.exports = EditorController =
if error? or !canAccess
callback new Error("Not authorized")
else
AnalyticsManager.trackOpenEditor user, project
client.join(project_id)
client.set("project_id", project_id)
client.set("owner_id", project.owner_ref._id)

View file

@ -1,6 +1,5 @@
logger = require('logger-sharelatex')
User = require('../../models/User').User
AnalyticsManager = require("../Analytics/AnalyticsManager")
SubscriptionLocator = require "../Subscription/SubscriptionLocator"
Settings = require "settings-sharelatex"
@ -15,9 +14,6 @@ module.exports = ReferalAllocator =
return callback(error) if error?
return callback(new Error("user not found")) if !user? or !user._id?
# Can be backgrounded
AnalyticsManager.trackReferral user, referal_source, referal_medium
if referal_source == "bonus"
User.update query, {
$push:

View file

@ -38,8 +38,8 @@ module.exports =
userHasSubscription: (user, callback = (err, hasSubscription, subscription)->) ->
logger.log user_id:user._id, "checking if user has subscription"
SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
logger.log user:user, subscription:subscription, "checking if user has subscription"
hasValidSubscription = subscription? and subscription.recurlySubscription_id? and subscription?.state != "expired"
logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
callback err, hasValidSubscription, subscription
userHasFreeTrial: (user, callback = (err, hasFreeTrial, subscription)->) ->

View file

@ -2,7 +2,6 @@ async = require 'async'
logger = require 'logger-sharelatex'
SubscriptionUpdater = require("./SubscriptionUpdater")
SubscriptionLocator = require("./SubscriptionLocator")
AnalyticsManager = require("../Analytics/AnalyticsManager")
module.exports = SubscriptionBackgroundJobs =
# TODO: Remove this one month after the ability to start free trials was removed
@ -15,7 +14,6 @@ module.exports = SubscriptionBackgroundJobs =
do (subscription) =>
downgrades.push (cb) =>
logger.log subscription: subscription, "downgrading free trial"
AnalyticsManager.trackFreeTrialExpired subscription.admin_id
SubscriptionUpdater.downgradeFreeTrial(subscription, cb)
async.series downgrades, (error) -> callback(error, subscriptions)

View file

@ -2,7 +2,6 @@ RecurlyWrapper = require("./RecurlyWrapper")
Settings = require "settings-sharelatex"
User = require('../../models/User').User
logger = require('logger-sharelatex')
AnalyticsManager = require '../Analytics/AnalyticsManager'
SubscriptionUpdater = require("./SubscriptionUpdater")
LimitationsManager = require('./LimitationsManager')
EmailHandler = require("../Email/EmailHandler")
@ -15,7 +14,6 @@ module.exports =
return callback(error) if error?
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) ->
return callback(error) if error?
AnalyticsManager.trackSubscriptionStarted user, recurlySubscription?.plan?.plan_code
callback()
updateSubscription: (user, plan_code, callback)->
@ -33,7 +31,6 @@ module.exports =
if hasSubscription
RecurlyWrapper.cancelSubscription subscription.recurlySubscription_id, (error) ->
return callback(error) if error?
AnalyticsManager.trackSubscriptionCancelled user
emailOpts =
to: user.email
first_name: user.first_name

View file

@ -9,7 +9,6 @@ newsLetterManager = require('../managers/NewsletterManager')
dropboxHandler = require('../Features/Dropbox/DropboxHandler')
userRegistrationHandler = require('../Features/User/UserRegistrationHandler')
metrics = require('../infrastructure/Metrics')
AnalyticsManager = require('../Features/Analytics/AnalyticsManager')
ReferalAllocator = require('../Features/Referal/ReferalAllocator')
AuthenticationManager = require("../Features/Authentication/AuthenticationManager")
AuthenticationController = require("../Features/Authentication/AuthenticationController")

View file

@ -95,7 +95,6 @@ module.exports = (app)->
app.use (req, res, next)->
if req.session.user?
res.locals.mixpanelId = req.session.user._id
res.locals.user =
email: req.session.user.email
first_name: req.session.user.first_name
@ -106,9 +105,7 @@ module.exports = (app)->
if req.session.justLoggedIn
res.locals.justLoggedIn = true
delete req.session.justLoggedIn
res.locals.mixpanelToken = Settings.analytics?.mixpanel?.token
res.locals.gaToken = Settings.analytics?.ga?.token
res.locals.heapToken = Settings.analytics?.heap?.token
res.locals.tenderUrl = Settings.tenderUrl
next()

View file

@ -60,11 +60,5 @@ block content
include ../general/social-footer
include ../general/small-footer
.container
.row
.span12(style="text-align:right; margin-bottom:20px")
a(href="https://mixpanel.com/f/partner")
img(src="//cdn.mxpnl.com/site_media/images/partner/badge_light.png",alt="Mobile Analytics")

View file

@ -16,59 +16,13 @@ html(itemscope, itemtype='http://schema.org/Product')
- if (typeof(gaToken) != "undefined")
script(type='text/javascript')
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '#{gaToken}']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
// this is the new google analytics https://developers.google.com/analytics/devguides/collection/analyticsjs/
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '#{gaToken}', 'auto');
ga('create', '#{gaToken}', 'sharelatex.com');
ga('send', 'pageview');
- if (typeof(mixpanelToken) != "undefined")
script(type="text/javascript")
(function(c,a){window.mixpanel=a;var b,d,h,e;b=c.createElement("script");
b.type="text/javascript";b.async=!0;b.src=("https:"===c.location.protocol?"https:":"http:")+
'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';d=c.getElementsByTagName("script")[0];
d.parentNode.insertBefore(b,d);a._i=[];a.init=function(b,c,f){function d(a,b){
var c=b.split(".");2==c.length&&(a=a[c[0]],b=c[1]);a[b]=function(){a.push([b].concat(
Array.prototype.slice.call(arguments,0)))}}var g=a;"undefined"!==typeof f?g=a[f]=[]:
f="mixpanel";g.people=g.people||[];h=['disable','track','track_pageview','track_links',
'track_forms','register','register_once','unregister','identify','alias','name_tag',
'set_config','people.set','people.increment','people.track_charge','people.append'];
for(e=0;e<h.length;e++)d(g,h[e]);a._i.push([b,c,f])};a.__SV=1.2;})(document,window.mixpanel||[]);
mixpanel.init("#{mixpanelToken}");
- if (typeof(justRegistered) != "undefined" && justRegistered)
script(type="text/javascript")
mixpanel.alias("#{mixpanelId}");
mixpanel.track("registered");
mixpanel.name_tag("#{user.email}");
mixpanel.people.set({
$email: "#{user.email}",
$first_name: "#{user.first_name}",
$last_name: "#{user.last_name}",
$created: new Date(#{Date.now()})
});
- if (typeof(mixpanelId) != "undefined")
script(type="text/javascript")
mixpanel.identify("#{mixpanelId}");
- if (typeof(justLoggedIn) != "undefined" && justLoggedIn)
script(type="text/javascript")
mixpanel.track("Logged in");
script
window.csrfToken = "#{csrfToken}";

View file

@ -56,9 +56,7 @@ block content
};
script(type='text/javascript')
if (typeof(mixpanel) != "undefined") {
mixpanel.track('Opened Editor', { project_id: window.userSettings.project_id });
}
ga('send', 'event', 'editor-interaction', 'editor-opened')
- locals.supressDefaultJs = true
- var fingerprintedPath = fingerprint(jsPath+'libs/pdf.worker.js')

View file

@ -118,21 +118,20 @@ block content
script(type="text/javascript")
$(function() {
mixpanel.track("Viewed referral page");
$(".twitter").click(function() {
mixpanel.track("Clicked Bonus Referral Button", { medium: "twitter" });
ga('send', 'event', 'referal-button', 'clicked', "twitter")
});
$(".email").click(function() {
mixpanel.track("Clicked Bonus Referral Button", { medium: "email" });
ga('send', 'event', 'referal-button', 'clicked', "email")
});
$(".facebook").click(function() {
mixpanel.track("Clicked Bonus Referral Button", { medium: "facebook" });
ga('send', 'event', 'referal-button', 'clicked', "facebook")
});
$(".google-plus").click(function() {
mixpanel.track("Clicked Bonus Referral Button", { medium: "google_plus" });
ga('send', 'event', 'referal-button', 'clicked', "google-plus")
});
$(".link").click(function() {
mixpanel.track("Clicked Bonus Referral Button", { medium: "direct" });
ga('send', 'event', 'referal-button', 'clicked', "direct-link")
});
});

View file

@ -9,7 +9,8 @@ block content
#subscribeForm.box Loading subscription form...
script(type="text/javascript")
mixpanel.track("Page Viewed", { name: "payment_form", plan: "#{plan_code}" })
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
script(type="text/javascript")
Recurly.config(!{recurlyConfig})

View file

@ -163,5 +163,4 @@ block content
include ../general/small-footer
link(rel='stylesheet', href='/brand/plans.css?fingerprint='+fingerprint('/brand/mainStyle.css'))
script
mixpanel.track("Page Viewed", { name: "plans" })

View file

@ -35,8 +35,3 @@ block content
include ../general/small-footer
script
mixpanel.track("Page Viewed", { name: "register" })
$('#registerButton').click(function(){
mixpanel.track("registerpage.registerd")
})

View file

@ -146,12 +146,8 @@ module.exports =
# Fill in your unique token from various analytics services to enable
# them.
# analytics:
# mixpanel:
# token: ""
# ga:
# token: ""
# heap:
# token: ""
#
# ShareLaTeX's help desk is provided by tenderapp.com
# tenderUrl: ""

View file

@ -1,26 +0,0 @@
define [], () ->
chooseOption = (testName, option1, option2, callback = (error, option) ->) ->
if Math.random() < 0.5
option = option1
else
option = option2
loaded = false
do initTest = ->
return if loaded
if mixpanel?.get_property?
attributes = {}
attributes[testName] = option
mixpanel?.register_once( attributes )
mixpanel?.people.set( attributes )
loaded = true
callback null, mixpanel?.get_property( testName )
else
setTimeout(initTest, 300)
fallback = () ->
return if loaded
loaded = true
callback null, option1
setTimeout fallback, 1500

View file

@ -5,12 +5,12 @@ define () ->
@updateCount ||= 0
@updateCount++
if @updateCount == 100
mixpanel?.track("Updated doc multiple times in one session", project_id: @ide.project.id)
ga('send', 'event', 'editor-interaction', 'multi-doc-update')
@ide.pdfManager.on "compile:pdf", () =>
@compileCount ||= 0
@compileCount++
if @compileCount == 1
mixpanel?.track("Compiled project at least once in one session", project_id: @ide.project.id)
ga('send', 'event', 'editor-interaction', 'single-compile')
if @compileCount == 3
mixpanel?.track("Compiled project multiple times in one session", project_id: @ide.project.id)
ga('send', 'event', 'editor-interaction', 'multi-compile')

View file

@ -144,7 +144,7 @@ define [
_onError: (error) ->
console.error "ShareJS error", error
heap?.track "shareJsError", { error: (error.message or error), transport: ide.socket.socket.transport.name }
ga('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" )
@ide.socket.disconnect()
@doc?.clearInflightAndPendingOps()
@_cleanUp()

View file

@ -3,6 +3,3 @@ define [
"forms"
], ()->
$(document).ready ()->
mixpanel?.track_links(".signup-now", "homepage.signup-now")
$('#registerButton').click ->
mixpanel?.track("homepage.register-now")

View file

@ -218,7 +218,7 @@ define [
return if @timeOut?
@clearTimeout()
@timeOut = setTimeout((=>
heap?.track "savingShown"
ga('send', 'event', 'editor-interaction', 'notification-shown', "saving")
$("#savingProblems").show()
), 1000)

View file

@ -15,7 +15,7 @@ define [
@connected = false
@ide.trigger "disconnect"
setTimeout(=>
mixpanel?.track("disconnected")
ga('send', 'event', 'editor-interaction', 'disconnect')
, 2000)
if !@forcedDisconnect

View file

@ -274,7 +274,7 @@ define [
"caption=My LaTeX project (#{@ide.project.get("name")}) is available online on ShareLaTeX&" +
"redirect_uri=#{window.sharelatex.siteUrl}&" +
"display=popup"
mixpanel?.track("Project Shared", { method: "facebook" })
ga('send', 'event', 'editor-interaction', 'project-shared', "facebook")
window.open(
url
""
@ -284,7 +284,7 @@ define [
postToTwitter: () ->
@ensurePublic (error, success) =>
if success
mixpanel?.track("Project Shared", { method: "twitter" })
ga('send', 'event', 'editor-interaction', 'project-shared', "twitter")
window.open(
"https://www.twitter.com/share/?text=Check out my online LaTeX Project: #{@ide.project.get("name")}&url=#{encodeURIComponent(@url("t"))}"
""
@ -294,7 +294,7 @@ define [
postToGoogle: () ->
@ensurePublic (error, success) =>
if success
mixpanel?.track("Project Shared", { method: "google_plus" })
ga('send', 'event', 'editor-interaction', 'project-shared', "google-plus")
window.open(
"https://plus.google.com/share?url=#{encodeURIComponent(@url("gp"))}"
""
@ -304,7 +304,7 @@ define [
shareUrl: () ->
@ensurePublic (error, success) =>
if success
mixpanel?.track("Project Shared", { method: "url" })
ga('send', 'event', 'editor-interaction', 'project-shared', "url")
Modal.createModal
el: $(
"<p>You can share you project with your friends and colleagues via this URL:</p>" +

View file

@ -42,7 +42,6 @@ describe "EditorController", ->
@VersioningApiHandler =
enableVersioning : sinon.stub().callsArg(1)
@client = new MockClient()
@AnalyticsManager = {}
@settings =
apis:{thirdPartyDataStore:{emptyProjectFlushDelayMiliseconds:0.5}}
@ -65,7 +64,6 @@ describe "EditorController", ->
'../../handlers/ProjectHandler' : @ProjectHandler
"../Versioning/AutomaticSnapshotManager" : @AutomaticSnapshotManager
"../Versioning/VersioningApiHandler" : @VersioningApiHandler
"../Analytics/AnalyticsManager" : @AnalyticsManager
'../../models/Project' : Project: @Project
"settings-sharelatex":@settings
'../Dropbox/DropboxProjectLinker':@dropboxProjectLinker
@ -84,7 +82,6 @@ describe "EditorController", ->
@ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project)
@ProjectGetter.populateProjectWithUsers = sinon.stub().callsArgWith(1, null, @project)
@AuthorizationManager.setPrivilegeLevelOnClient = sinon.stub()
@AnalyticsManager.trackOpenEditor = sinon.stub()
describe "when authorized", ->
beforeEach ->
@ -120,9 +117,6 @@ describe "EditorController", ->
@VersioningApiHandler.enableVersioning.calledWith(@project)
.should.equal true
it "should track the event", ->
@AnalyticsManager.trackOpenEditor.calledWith(@user, @project).should.equal true
describe "when not authorized", ->
beforeEach ->
@AuthorizationManager.getPrivilegeLevelForProject =

View file

@ -9,7 +9,6 @@ describe 'Referal allocator', ->
beforeEach ->
@ReferalAllocator = SandboxedModule.require modulePath, requires:
'../../models/User': User: @User = {}
"../Analytics/AnalyticsManager": @AnalyticsManager = {}
"../Subscription/SubscriptionLocator": @SubscriptionLocator = {}
"settings-sharelatex": @Settings = {}
'logger-sharelatex':
@ -27,7 +26,6 @@ describe 'Referal allocator', ->
@referal_source = "bonus"
@User.update = sinon.stub().callsArgWith 3, null
@User.findOne = sinon.stub().callsArgWith 1, null, { _id: @user_id }
@AnalyticsManager.trackReferral = sinon.stub()
@ReferalAllocator.assignBonus = sinon.stub().callsArg 1
@ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback
@ -46,11 +44,6 @@ describe 'Referal allocator', ->
.calledWith( referal_id: @referal_id )
.should.equal true
it "should track the referral", ->
@AnalyticsManager.trackReferral
.calledWith({ _id: @user_id }, @referal_source, @referal_medium)
.should.equal true
it "shoudl assign the user their bonus", ->
@ReferalAllocator.assignBonus
.calledWith(@user_id)
@ -64,7 +57,6 @@ describe 'Referal allocator', ->
@referal_source = "public_share"
@User.update = sinon.stub().callsArgWith 3, null
@User.findOne = sinon.stub().callsArgWith 1, null, { _id: @user_id }
@AnalyticsManager.trackReferral = sinon.stub()
@ReferalAllocator.assignBonus = sinon.stub().callsArg 1
@ReferalAllocator.allocate @referal_id, @new_user_id, @referal_source, @referal_medium, @callback
@ -76,11 +68,6 @@ describe 'Referal allocator', ->
.calledWith( referal_id: @referal_id )
.should.equal true
it "should track the referral", ->
@AnalyticsManager.trackReferral
.calledWith({ _id: @user_id }, @referal_source, @referal_medium)
.should.equal true
it "should not assign the user a bonus", ->
@ReferalAllocator.assignBonus.called.should.equal false

View file

@ -9,14 +9,12 @@ describe "SubscriptionBackgroundTasks", ->
@Settings = defaultPlanCode:
collaborators: 13
versioning: true
@AnalyticsManager = {}
@SubscriptionUpdater = {}
@SubscriptionBackgroundTasks = SandboxedModule.require modulePath, requires:
"./SubscriptionLocator" : @SubscriptionLocator
"./SubscriptionUpdater" : @SubscriptionUpdater
"settings-sharelatex" : @Settings
"../Analytics/AnalyticsManager" : @AnalyticsManager
"logger-sharelatex" : log:->
describe 'downgradeExpiredFreeTrials', ->
@ -27,7 +25,6 @@ describe "SubscriptionBackgroundTasks", ->
]
@SubscriptionUpdater.downgradeFreeTrial = sinon.stub().callsArg(1)
@SubscriptionLocator.expiredFreeTrials = sinon.stub().callsArgWith(0, null, @subscriptions)
@AnalyticsManager.trackFreeTrialExpired = sinon.stub()
@callback = sinon.stub()
@SubscriptionBackgroundTasks.downgradeExpiredFreeTrials(@callback)
@ -36,11 +33,6 @@ describe "SubscriptionBackgroundTasks", ->
for subscription in @subscriptions
@SubscriptionUpdater.downgradeFreeTrial.calledWith(subscription).should.equal true
it "should track each downgrade", ->
@AnalyticsManager.trackFreeTrialExpired.callCount.should.equal @subscriptions.length
for subscription in @subscriptions
@AnalyticsManager.trackFreeTrialExpired.calledWith(subscription.admin_id).should.equal true
it "should return the subscriptions in the callback", ->
@callback.called.should.equal true
@callback.args[0][1].should.deep.equal @subscriptions

View file

@ -44,11 +44,6 @@ describe "Subscription Handler sanboxed", ->
cancelSubscription: sinon.stub().callsArgWith(1)
reactivateSubscription: sinon.stub().callsArgWith(1)
@AnalyticsManager =
trackSubscriptionCancelled: sinon.stub()
trackSubscriptionStarted: sinon.stub()
trackFreeTrialStarted: sinon.stub()
@SubscriptionUpdater =
syncSubscription: sinon.stub().callsArgWith(2)
startFreeTrial: sinon.stub().callsArgWith(1)
@ -64,7 +59,6 @@ describe "Subscription Handler sanboxed", ->
"settings-sharelatex": @Settings
'../../models/User': User:@User
'./SubscriptionUpdater': @SubscriptionUpdater
'../Analytics/AnalyticsManager': @AnalyticsManager
"logger-sharelatex":{log:->}
'./LimitationsManager':@LimitationsManager
"../Email/EmailHandler":@EmailHandler
@ -148,11 +142,6 @@ describe "Subscription Handler sanboxed", ->
@RecurlyWrapper.cancelSubscription.called.should.equal true
@RecurlyWrapper.cancelSubscription.calledWith(@subscription.recurlySubscription_id).should.equal true
it "should track the cancellation", ->
@AnalyticsManager.trackSubscriptionCancelled
.calledWith(@user)
.should.equal true
it "should send a cancellation email", ->
@EmailHandler.sendEmail.calledWith("canceledSubscription", {to:@user.email, first_name:@user.first_name}).should.equal true