Add Segment for teachers, misc analytics cleanup tweaks

This commit is contained in:
Nick Winter 2016-02-02 12:48:19 -08:00 committed by Scott Erickson
parent 24fc14260e
commit 260fd21f4e
12 changed files with 170 additions and 53 deletions

View file

@ -40,16 +40,18 @@
<!-- Errorception -->
<script>
(function(_,e,rr,s){_errs=[s];var c=_.onerror;_.onerror=function(){var a=arguments;_errs.push(a);
c&&c.apply(this,a)};var b=function(){var c=e.createElement(rr),b=e.getElementsByTagName(rr)[0];
c.src="//beacon.errorception.com/"+s+".js";c.async=!0;b.parentNode.insertBefore(c,b)};
_.addEventListener?_.addEventListener("load",b,!1):_.attachEvent("onload",b)})
(window,document,"script","51a79585ee207206390002a2");
(function(_,e,rr,s){_errs=[s];var c=_.onerror;_.onerror=function(){var a=arguments;_errs.push(a);
c&&c.apply(this,a)};var b=function(){var c=e.createElement(rr),b=e.getElementsByTagName(rr)[0];
c.src="//beacon.errorception.com/"+s+".js";c.async=!0;b.parentNode.insertBefore(c,b)};
_.addEventListener?_.addEventListener("load",b,!1):_.attachEvent("onload",b)})
(window,document,"script","51a79585ee207206390002a2");
</script>
<!-- start Mixpanel --><script type="text/javascript">(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
mixpanel.init("e71a4e60db7e1dc5e685be96776280f9");</script><!-- end Mixpanel -->
<!-- Mixpanel -->
<script type="text/javascript">
(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
mixpanel.init("e71a4e60db7e1dc5e685be96776280f9");
</script>
<script src="https://checkout.stripe.com/checkout.js"></script>

View file

@ -52,6 +52,7 @@ module.exports = class CocoClass
# for initting subscriptions
return unless Backbone?.Mediator?
for channel, func of @subscriptions
console.log @nick, 'listening to', channel, 'with', func
func = utils.normalizeFunc(func, @)
Backbone.Mediator.subscribe(channel, func, @)

View file

@ -1,19 +1,26 @@
{me} = require 'core/auth'
SuperModel = require 'models/SuperModel'
utils = require 'core/utils'
CocoClass = require 'core/CocoClass'
debugAnalytics = false
debugAnalytics = true
targetInspectJSLevelSlugs = ['cupboards-of-kithgard']
module.exports = class Tracker
module.exports = class Tracker extends CocoClass
subscriptions:
'application:service-loaded': 'onServiceLoaded'
constructor: ->
super()
if window.tracker
console.error 'Overwrote our Tracker!', window.tracker
window.tracker = @
@isProduction = document.location.href.search('codecombat.com') isnt -1
@isProduction = true
@trackReferrers()
@identify()
@supermodel = new SuperModel()
@updateRole() if me.get 'role'
enableInspectletJS: (levelSlug) ->
# InspectletJS loading is delayed and targeting specific levels for more focused investigations
@ -30,7 +37,7 @@ module.exports = class Tracker
insp.async = true
insp.id = 'inspsync'
insp.src = (if 'https:' == document.location.protocol then 'https' else 'http') + '://cdn.inspectlet.com/inspectlet.js'
insp.onreadystatechange = => scriptLoaded() if insp.readyState is 'complete'
insp.onreadystatechange = -> scriptLoaded() if insp.readyState is 'complete'
insp.onload = scriptLoaded
x = document.getElementsByTagName('script')[0]
@inspectletScriptNode = x.parentNode.insertBefore insp, x
@ -62,8 +69,11 @@ module.exports = class Tracker
@explicitTraits ?= {}
@explicitTraits[key] = value for key, value of traits
for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'testGroupNumber', 'gender', 'lastLevel', 'siteref', 'ageRange', 'schoolName', 'coursePrepaidID']
for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'testGroupNumber', 'gender', 'lastLevel', 'siteref', 'ageRange', 'schoolName', 'coursePrepaidID', 'role']
traits[userTrait] ?= me.get(userTrait)
if @isTeacher()
traits.teacher = true
console.log 'Would identify', me.id, traits if debugAnalytics
return unless @isProduction and not me.isAdmin()
@ -81,7 +91,10 @@ module.exports = class Tracker
mixpanel.identify(me.id)
mixpanel.register(traits)
trackPageView: ->
if @isTeacher() and @segmentLoaded
analytics.identify me.id, traits
trackPageView: (includeIntegrations=[]) ->
name = Backbone.history.getFragment()
url = "/#{name}"
console.log "Would track analytics pageview: #{url}" if debugAnalytics
@ -93,9 +106,17 @@ module.exports = class Tracker
ga? 'send', 'pageview', url
# Mixpanel
mixpanelIncludes = ['', 'courses', 'courses/purchase', 'courses/teachers', 'courses/students', 'schools', 'teachers', 'teachers/freetrial', 'teachers/quote']
mixpanelIncludes = ['', 'courses', 'courses/purchase', 'courses/teachers', 'courses/students', 'schools', 'teachers', 'teachers/freetrial', 'teachers/quote', 'play', 'play/level/dungeons-of-kithgard']
mixpanel.track('page viewed', 'page name' : name, url : url) if name in mixpanelIncludes
if @isTeacher() and @segmentLoaded
options = {}
if includeIntegrations?.length
options.integrations = All: false
for integration in includeIntegrations
options.integrations[integration] = true
analytics.page url, {}, options
trackEvent: (action, properties={}, includeIntegrations=[]) =>
@trackEventInternal action, _.cloneDeep properties unless me?.isAdmin() and @isProduction
console.log 'Tracking external analytics event:', action, properties, includeIntegrations if debugAnalytics
@ -119,35 +140,44 @@ module.exports = class Tracker
# Only log explicit events for now
mixpanel.track(action, properties) if 'Mixpanel' in includeIntegrations
if @isTeacher() and @segmentLoaded
options = {}
if includeIntegrations
# https://segment.com/docs/libraries/analytics.js/#selecting-integrations
options.integrations = All: false
for integration in includeIntegrations
options.integrations[integration] = true
analytics?.track action, {}, options
trackEventInternal: (event, properties) =>
# Skipping heavily logged actions we don't use internally
unless event in ['Simulator Result', 'Started Level Load', 'Finished Level Load']
# Trimming properties we don't use internally
# TODO: delete properites.level for 'Saw Victory' after 2/8/15. Should be using levelID instead.
if event in ['Clicked Start Level', 'Inventory Play', 'Heard Sprite', 'Started Level', 'Saw Victory', 'Click Play', 'Choose Inventory', 'Homepage Loaded', 'Change Hero']
delete properties.category
delete properties.label
else if event in ['Loaded World Map', 'Started Signup', 'Finished Signup', 'Login', 'Facebook Login', 'Google Login', 'Show subscription modal']
delete properties.category
return if event in ['Simulator Result', 'Started Level Load', 'Finished Level Load']
# Trimming properties we don't use internally
# TODO: delete properites.level for 'Saw Victory' after 2/8/15. Should be using levelID instead.
if event in ['Clicked Start Level', 'Inventory Play', 'Heard Sprite', 'Started Level', 'Saw Victory', 'Click Play', 'Choose Inventory', 'Homepage Loaded', 'Change Hero']
delete properties.category
delete properties.label
else if event in ['Loaded World Map', 'Started Signup', 'Finished Signup', 'Login', 'Facebook Login', 'Google Login', 'Show subscription modal']
delete properties.category
properties[key] = value for key, value of @explicitTraits if @explicitTraits?
console.log 'Tracking internal analytics event:', event, properties if debugAnalytics
if @isProduction
eventObject = {}
eventObject["event"] = event
eventObject["properties"] = properties unless _.isEmpty properties
eventObject["user"] = me.id
dataToSend = JSON.stringify eventObject
# console.log dataToSend if debugAnalytics
$.post("#{window.location.protocol or 'http:'}//analytics.codecombat.com/analytics", dataToSend).fail ->
console.error "Analytics post failed!"
else
request = @supermodel.addRequestResource {
url: '/db/analytics.log.event/-/log_event'
data: {event: event, properties: properties}
method: 'POST'
}, 0
request.load()
properties[key] = value for key, value of @explicitTraits if @explicitTraits?
console.log 'Tracking internal analytics event:', event, properties if debugAnalytics
if @isProduction
eventObject = {}
eventObject["event"] = event
eventObject["properties"] = properties unless _.isEmpty properties
eventObject["user"] = me.id
dataToSend = JSON.stringify eventObject
# console.log dataToSend if debugAnalytics
$.post("#{window.location.protocol or 'http:'}//analytics.codecombat.com/analytics", dataToSend).fail ->
console.error "Analytics post failed!"
else
request = @supermodel.addRequestResource {
url: '/db/analytics.log.event/-/log_event'
data: {event: event, properties: properties}
method: 'POST'
}, 0
request.load()
trackTiming: (duration, category, variable, label) ->
# https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings
@ -155,3 +185,18 @@ module.exports = class Tracker
console.log 'Would track timing event:', arguments if debugAnalytics
return unless me and @isProduction and not me.isAdmin()
ga? 'send', 'timing', category, variable, duration, label
isTeacher: ->
return me.get('role') in ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent']
updateRole: ->
return unless @isTeacher()
return require('core/services/segment')() unless @segmentLoaded
@identify()
#analytics.page() # It looks like we don't want to call this here because it somehow already gets called once in addition to this.
# TODO: record any events and pageviews that have built up before we knew we were a teacher.
onServiceLoaded: (e) ->
return unless e.service is 'segment'
@segmentLoaded = true
@updateRole()

View file

@ -0,0 +1,46 @@
module.exports = initializeSegmentio = ->
analytics = window.analytics = window.analytics or []
return if analytics.initialize
return console?.error 'Segment snippet included twice.' if analytics.invoked
analytics.invoked = true
analytics.methods = [
'trackSubmit'
'trackClick'
'trackLink'
'trackForm'
'pageview'
'identify'
'reset'
'group'
'track'
'ready'
'alias'
'page'
'once'
'off'
'on'
]
analytics.factory = (t) ->
->
e = Array::slice.call(arguments)
e.unshift t
analytics.push e
analytics
for method in analytics.methods
analytics[method] = analytics.factory method
analytics.load = (t) ->
e = document.createElement 'script'
e.type = 'text/javascript'
e.async = true
e.src = (if document.location.protocol is 'https:' then 'https://' else 'http://') + 'cdn.segment.com/analytics.js/v1/' + t + '/analytics.min.js'
n = document.getElementsByTagName('script')[0]
n.parentNode.insertBefore e, n
Backbone.Mediator.publish 'application:service-loaded', service: 'segment'
return
analytics.SNIPPET_VERSION = '3.1.0'
analytics.load 'yJpJZWBw68fEj0aPSv8ffMMgof5kFnU9'
#analytics.page() # Don't track the page view on initial inclusion

View file

@ -59,6 +59,17 @@ module.exports = class User extends CocoModel
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled
setRole: (role, force=false) ->
return if me.isAdmin()
oldRole = @get 'role'
console.log 'had role', oldRole, 'new role', role
return if oldRole is role or (oldRole and not force)
console.log 'gonna set it!'
@set 'role', role
@patch()
application.tracker?.updateRole()
return @get 'role'
a = 5
b = 100
c = b

View file

@ -327,6 +327,7 @@ _.extend UserSchema.properties,
description: 'Prepaid which has paid for this user\'s course access'
})
schoolName: {type: 'string'}
role: {type: 'string'} # unset, 'student', 'teacher', 'parent', 'technology coordinator', 'advisor', 'principal', 'superintendent', ...
c.extendBasicProperties UserSchema, 'user'

View file

@ -67,3 +67,6 @@ module.exports =
'store:hero-purchased': c.object {required: ['hero', 'heroSlug']},
hero: {type: 'object'}
heroSlug: {type: 'string'}
'application:service-loaded': c.object {required: ['service']},
service: {type: 'string'} # 'segment'

View file

@ -27,24 +27,24 @@ formSchema = {
module.exports = class RequestQuoteView extends RootView
id: 'request-quote-view'
template: require 'templates/request-quote-view'
events:
'submit form': 'onSubmitForm'
'click #login-btn': 'onClickLoginButton'
'click #signup-btn': 'onClickSignupButton'
initialize: ->
@trialRequest = new TrialRequest()
@trialRequests = new TrialRequests()
@trialRequests.fetchOwn()
@supermodel.loadCollection(@trialRequests)
onLoaded: ->
if @trialRequests.size()
@trialRequest = @trialRequests.first()
me.setRole 'teacher'
super()
onSubmitForm: (e) ->
e.preventDefault()
form = @$('form')
@ -73,6 +73,7 @@ module.exports = class RequestQuoteView extends RootView
@trialRequest.save()
@trialRequest.on 'sync', @onTrialRequestSubmit, @
@trialRequest.on 'error', @onTrialRequestError, @
me.setRole attrs.role.toLowerCase(), true
onTrialRequestError: ->
@$('#submit-request-btn').text('Submit').attr('disabled', false)
@ -87,16 +88,16 @@ module.exports = class RequestQuoteView extends RootView
})
@openModalView(modal)
window.nextURL = '/courses/teachers' unless @trialRequest.isNew()
onClickSignupButton: ->
props = @trialRequest.get('properties') or {}
me.set('name', props.name)
modal = new AuthModal({
mode: 'signup'
initialValues: {
initialValues: {
email: props.email
schoolName: props.organization
}
})
@openModalView(modal)
window.nextURL = '/courses/teachers' unless @trialRequest.isNew()
window.nextURL = '/courses/teachers' unless @trialRequest.isNew()

View file

@ -34,3 +34,7 @@ module.exports = class SalesView extends RootView
scrollTop: $('[name="' + $(e.target).closest('a').attr('href').substr(1) + '"]').offset().top
}, 300)
false
constructor: ->
super arguments...
me.setRole 'teacher'

View file

@ -37,9 +37,10 @@ module.exports = class PurchaseCoursesView extends RootView
events:
'input #students-input': 'onInputStudentsInput'
'click #purchase-btn': 'onClickPurchaseButton'
onLoaded: ->
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
me.setRole 'teacher'
super()
getPriceString: -> '$' + (@getPrice()/100).toFixed(2)
@ -72,8 +73,8 @@ module.exports = class PurchaseCoursesView extends RootView
updatePrice: ->
@renderSelectors '#price-form-group'
numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000
numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000
onClickPurchaseButton: ->
return @openModalView new AuthModal() if me.isAnonymous()

View file

@ -90,6 +90,7 @@ module.exports = class TeacherCoursesView extends RootView
onLoaded: ->
super()
me.setRole 'teacher'
@addFreeCourseInstances()
addFreeCourseInstances: ->

View file

@ -32,6 +32,7 @@ UserSchema.index({'stripe.subscriptionID': 1}, {unique: true, sparse: true})
UserSchema.index({'siteref': 1}, {name: 'siteref index', sparse: true})
UserSchema.index({'schoolName': 1}, {name: 'schoolName index', sparse: true})
UserSchema.index({'country': 1}, {name: 'country index', sparse: true})
UserSchema.index({'role': 1}, {name: 'role index', sparse: true})
UserSchema.post('init', ->
@set('anonymous', false) if @get('email')
@ -315,7 +316,7 @@ UserSchema.statics.privateProperties = [
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion', 'country',
'schoolName', 'ageRange'
'schoolName', 'ageRange', 'role'
]
UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [
@ -323,7 +324,7 @@ UserSchema.statics.editableProperties = [
'firstName', 'lastName', 'gender', 'ageRange', 'facebookID', 'gplusID', 'emails',
'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage',
'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile', 'savedEmployerFilterAlerts',
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName'
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role'
]
UserSchema.plugin plugins.NamedPlugin