mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
Add Segment for teachers, misc analytics cleanup tweaks
This commit is contained in:
parent
24fc14260e
commit
260fd21f4e
12 changed files with 170 additions and 53 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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, @)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
46
app/core/services/segment.coffee
Normal file
46
app/core/services/segment.coffee
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -90,6 +90,7 @@ module.exports = class TeacherCoursesView extends RootView
|
|||
|
||||
onLoaded: ->
|
||||
super()
|
||||
me.setRole 'teacher'
|
||||
@addFreeCourseInstances()
|
||||
|
||||
addFreeCourseInstances: ->
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue