diff --git a/.jshintrc b/.jshintrc index c16505dd07..d1064e9ba9 100644 --- a/.jshintrc +++ b/.jshintrc @@ -8,5 +8,5 @@ "asi": true, "boss": true, - "newcap": false, + "newcap": false } diff --git a/Gruntfile.js b/Gruntfile.js index 3376be4c61..85bb31be74 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -110,7 +110,7 @@ module.exports = function(grunt) { //Load build files from public/manifest.json grunt.registerTask('loadManifestFiles', 'Load all build files from public/manifest.json', function(){ - var files = grunt.file.readJSON('./public/manifest.json'); + var files = grunt.file.readJSON('./website/public/manifest.json'); var uglify = {}; var cssmin = {}; @@ -130,6 +130,8 @@ module.exports = function(grunt) { }); }); + console.log(uglify); + console.log(cssmin); grunt.config.set('uglify.build.files', uglify); grunt.config.set('uglify.build.options', {compress: false}); @@ -142,6 +144,7 @@ module.exports = function(grunt) { // Register tasks. grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']); grunt.registerTask('build:dev', ['stylus']); + grunt.registerTask('test', ['loadManifestFiles', 'uglify', 'cssmin']); grunt.registerTask('run:dev', [ 'build:dev', 'concurrent' ]); diff --git a/bower.json b/bower.json index da34bcb073..4581445b88 100644 --- a/bower.json +++ b/bower.json @@ -26,7 +26,7 @@ "angular-loading-bar": "~0.6.0", "bootstrap": "~3.1.0", "bootstrap-growl": "git://github.com/ifightcrime/bootstrap-growl.git#master", - "bootstrap-tour": "~0.8.1", + "bootstrap-tour": "~0.10.1", "BrowserQuest": "git://github.com/browserquest/BrowserQuest.git", "github-buttons": "git://github.com/mdo/github-buttons.git", "marked": "~0.2.9", diff --git a/gulpfile.js b/gulpfile.js index 02736c7720..a9dff50ab3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,6 +21,7 @@ var gulp = require('gulp'), pkg = require('./package'); var paths = { + build: "./website/build", stylus: { src: { app: './website/public/css/index.styl', @@ -165,6 +166,7 @@ gulp.task('sprite', function(cb) { STEP++; console.log("Finished spritesmith" + key + ".png"); if(STEP >= COUNT) { + console.log(paths.sprites.cssminSrc); gulp.src(paths.sprites.cssminSrc) .pipe(concat('habitrpg-shared.css')) .pipe(cssmin()) @@ -185,6 +187,45 @@ gulp.task('browserify', function() { .pipe(gulp.dest(paths.common.dest)) }) +gulp.task('build', function() { + var files = require('./website/public/manifest'); + var uglifySrc = {}; + var cssminSrc = {}; + + _.each(files, function(val, key){ + + var js = uglifySrc[key + '.js'] = []; + + _.each(files[key]['js'], function(val){ + js.push('./website/public/' + val); + }); + + var css = cssminSrc[key + '.css'] = []; + + _.each(files[key]['css'], function(val){ + var path = (val == 'app.css' || val == 'static.css') ? paths.build : './website/public/'; + css.push(path + val) + }); + + }); + + // Concat CSS + _.each(cssminSrc, function(val, key) { + gulp.src(val) + .pipe(concat(key)) + .pipe(cssmin()) + .pipe(gulp.dest(paths.build)) + }); + + // Uglify JS + _.each(uglifySrc, function(val, key) { + gulp.src(val) + .pipe(concat(key)) + .pipe(uglify()) + .pipe(gulp.dest(paths.build)) + }); +}); + gulp.task('watch', ['stylus', 'browserify'], function() { gulp.watch(paths.stylus.watch, ['stylus']); gulp.watch(paths.common.watch, ['browserify']); diff --git a/src/controllers/auth.js b/src/controllers/auth.js new file mode 100644 index 0000000000..68c2b7c066 --- /dev/null +++ b/src/controllers/auth.js @@ -0,0 +1,307 @@ +var _ = require('lodash'); +var validator = require('validator'); +var passport = require('passport'); +var shared = require('habitrpg-shared'); +var async = require('async'); +var utils = require('../utils'); +var nconf = require('nconf'); +var request = require('request'); +var User = require('../models/user').model; +var ga = require('./../utils').ga; +var i18n = require('./../i18n'); + +var isProd = nconf.get('NODE_ENV') === 'production'; + +var api = module.exports; + +var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"}; +var NO_USER_FOUND = {err: "No user found."}; +var NO_SESSION_FOUND = { err: "You must be logged in." }; +var accountSuspended = function(uuid){ + return { + err: 'Account has been suspended, please contact leslie@habitrpg.com with your UUID ('+uuid+') for assistance.', + code: 'ACCOUNT_SUSPENDED' + }; +} +// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790 +var RegexEscape = function(s){ + return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i'); +} + +api.auth = function(req, res, next) { + var uid = req.headers['x-api-user']; + var token = req.headers['x-api-key']; + if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID); + User.findOne({_id: uid,apiToken: token}, function(err, user) { + if (err) return next(err); + if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); + if (user.auth.blocked) return res.json(401, accountSuspended(user._id)); + + res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true; + res.locals.user = user; + req.session.userId = user._id; + return next(); + }); +}; + +api.authWithSession = function(req, res, next) { //[todo] there is probably a more elegant way of doing this... + if (!(req.session && req.session.userId)) + return res.json(401, NO_SESSION_FOUND); + User.findOne({_id: req.session.userId}, function(err, user) { + if (err) return next(err); + if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); + res.locals.user = user; + next(); + }); +}; + +api.authWithUrl = function(req, res, next) { + User.findOne({_id:req.query._id, apiToken:req.query.apiToken}, function(err,user){ + if (err) return next(err); + if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); + res.locals.user = user; + next(); + }) +} + +api.registerUser = function(req, res, next) { + async.auto({ + validate: function(cb) { + if (!(req.body.username && req.body.password && req.body.email)) + return cb({code:401, err: ":username, :email, :password, :confirmPassword required"}); + if (req.body.password !== req.body.confirmPassword) + return cb({code:401, err: ":password and :confirmPassword don't match"}); + if (!validator.isEmail(req.body.email)) + return cb({code:401, err: ":email invalid"}); + cb(); + }, + findEmail: function(cb) { + User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {_id:1}, cb); + }, + findUname: function(cb) { + User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {_id:1}, cb); + }, + findFacebook: function(cb){ + User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb); + }, + register: ['validate', 'findEmail', 'findUname', 'findFacebook', function(cb, data) { + if (data.findEmail) return cb({code:401, err:"Email already taken"}); + if (data.findUname) return cb({code:401, err:"Username already taken"}); + var salt = utils.makeSalt(); + var newUser = { + auth: { + local: { + username: req.body.username, + email: req.body.email, + salt: salt, + hashed_password: utils.encryptPassword(req.body.password, salt) + }, + timestamps: {created: +new Date(), loggedIn: +new Date()} + } + }; + // existing user, allow them to add local authentication + if (data.findFacebook) { + data.findFacebook.auth.local = newUser.auth.local; + data.findFacebook.save(cb); + // new user, register them + } else { + newUser.preferences = newUser.preferences || {}; + newUser.preferences.language = req.language; // User language detected from browser, not saved + var user = new User(newUser); + utils.txnEmail(user, 'welcome'); + ga.event('register', 'Local').send(); + user.save(cb); + } + }] + }, function(err, data) { + if (err) return err.code ? res.json(err.code, err) : next(err); + res.json(200, data.register[0]); + }); +}; + +/* + Register new user with uname / password + */ + + +api.loginLocal = function(req, res, next) { + var username = req.body.username; + var password = req.body.password; + if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'}); + var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username}; + User.findOne(login, {auth:1}, function(err, user){ + if (err) return next(err); + if (!user) return res.json(401, {err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"}); + if (user.auth.blocked) return res.json(401, accountSuspended(user._id)); + // We needed the whole user object first so we can get his salt to encrypt password comparison + User.findOne( + {$and: [login, {'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt)}]} + , {_id:1, apiToken:1} + , function(err, user){ + if (err) return next(err); + if (!user) return res.json(401,{err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"}); + res.json({id: user._id,token: user.apiToken}); + password = null; + }); + }); +}; + +/* + POST /user/auth/social + */ +api.loginSocial = function(req, res, next) { + var access_token = req.body.authResponse.access_token, + network = req.body.network; + if (network!=='facebook') + return res.json(401, {err:"Only Facebook supported currently."}); + async.auto({ + profile: function (cb) { + passport._strategies[network].userProfile(access_token, cb); + }, + user: ['profile', function (cb, results) { + var q = {}; + q['auth.' + network + '.id'] = results.profile.id; + User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb); + }], + register: ['profile', 'user', function (cb, results) { + if (results.user) return cb(null, results.user); + // Create new user + var prof = results.profile; + var user = { + preferences: { + language: req.language // User language detected from browser, not saved + }, + auth: { + timestamps: {created: +new Date(), loggedIn: +new Date()} + } + }; + user.auth[network] = prof; + user = new User(user); + user.save(cb); + + utils.txnEmail(user, 'welcome'); + ga.event('register', network).send(); + }] + }, function(err, results){ + if (err) return res.json(401, {err: err.toString ? err.toString() : err}); + var acct = results.register[0] ? results.register[0] : results.register; + if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id)); + return res.json(200, {id:acct._id, token:acct.apiToken}); + }) +}; + +/** + * DELETE /user/auth/social + */ +api.deleteSocial = function(req,res,next){ + if (!res.locals.user.auth.local.username) + return res.json(401, {err:"Account lacks another authentication method, can't detach Facebook"}); + //FIXME for some reason, the following gives https://gist.github.com/lefnire/f93eb306069b9089d123 + //res.locals.user.auth.facebook = null; + //res.locals.user.auth.save(function(err, saved){ + User.update({_id:res.locals.user._id}, {$unset:{'auth.facebook':1}}, function(err){ + if (err) return next(err); + res.send(200); + }) +} + +api.resetPassword = function(req, res, next){ + var email = req.body.email, + salt = utils.makeSalt(), + newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later) + hashed_password = utils.encryptPassword(newPassword, salt); + + User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){ + if (err) return next(err); + if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email}); + user.auth.local.salt = salt; + user.auth.local.hashed_password = hashed_password; + utils.txnEmail(user, 'reset-password', [ + {name: "NEW_PASSWORD", content: newPassword}, + {name: "USERNAME", content: user.auth.local.username} + ]); + user.save(function(err){ + if(err) return next(err); + res.send('New password sent to '+ email); + email = salt = newPassword = hashed_password = null; + }); + }); +}; + +var invalidPassword = function(user, password){ + var hashed_password = utils.encryptPassword(password, user.auth.local.salt); + if (hashed_password !== user.auth.local.hashed_password) + return {code:401, err:"Incorrect password"}; + return false; +} + +api.changeUsername = function(req, res, next) { + async.waterfall([ + function(cb){ + User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb); + }, + function(found, cb){ + if (found) return cb({code:401, err: "Username already taken"}); + if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password)); + res.locals.user.auth.local.username = req.body.username; + res.locals.user.save(cb); + } + ], function(err){ + if (err) return err.code ? res.json(err.code, err) : next(err); + res.send(200); + }) +} + +api.changeEmail = function(req, res, next){ + async.waterfall([ + function(cb){ + User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb); + }, + function(found, cb){ + if(found) return cb({code:401, err: "Email already taken"}); + if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password)); + res.locals.user.auth.local.email = req.body.email; + res.locals.user.save(cb); + } + ], function(err){ + if (err) return err.code ? res.json(err.code,err) : next(err); + res.send(200); + }) +} + +api.changePassword = function(req, res, next) { + var user = res.locals.user, + oldPassword = req.body.oldPassword, + newPassword = req.body.newPassword, + confirmNewPassword = req.body.confirmNewPassword; + + if (newPassword != confirmNewPassword) + return res.json(401, {err: "Password & Confirm don't match"}); + + var salt = user.auth.local.salt, + hashed_old_password = utils.encryptPassword(oldPassword, salt), + hashed_new_password = utils.encryptPassword(newPassword, salt); + + if (hashed_old_password !== user.auth.local.hashed_password) + return res.json(401, {err:"Old password doesn't match"}); + + user.auth.local.hashed_password = hashed_new_password; + user.save(function(err, saved){ + if (err) next(err); + res.send(200); + }) +} + +/* + Registers a new user. Only accepting username/password registrations, no Facebook + */ + +api.setupPassport = function(router) { + + router.get('/logout', i18n.getUserLanguage, function(req, res) { + req.logout(); + delete req.session.userId; + res.redirect('/'); + }) + +}; diff --git a/website/public/js/controllers/groupsCtrl.js b/website/public/js/controllers/groupsCtrl.js index 376c25d810..c911b1e2f2 100644 --- a/website/public/js/controllers/groupsCtrl.js +++ b/website/public/js/controllers/groupsCtrl.js @@ -111,7 +111,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', 'Groups', function($scope, $rootScope, Members, Shared, $http, Notification, Groups) { $scope.timestamp = function(timestamp){ - return moment(timestamp).format('MM/DD/YYYY'); + return moment(timestamp).format($rootScope.User.user.preferences.dateFormat.toUpperCase()); } // We watch Members.selectedMember because it's asynchronously set, so would be a hassle to handle updates here $scope.$watch( function() { return Members.selectedMember; }, function (member) { diff --git a/website/public/js/controllers/rootCtrl.js b/website/public/js/controllers/rootCtrl.js index efe2862967..65e1188e77 100644 --- a/website/public/js/controllers/rootCtrl.js +++ b/website/public/js/controllers/rootCtrl.js @@ -166,7 +166,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ } matrix = [[env.t('date'), env.t('score')]]; _.each(history, function(obj) { - matrix.push([moment(obj.date).format('MM/DD/YY'), obj.value]); + matrix.push([moment(obj.date).format(User.user.preferences.dateFormat.toUpperCase().replace('YYYY','YY') ), obj.value]); }); data = google.visualization.arrayToDataTable(matrix); options = { diff --git a/website/public/js/services/guideServices.js b/website/public/js/services/guideServices.js index 8e4eb1bc60..74d710dbe5 100644 --- a/website/public/js/services/guideServices.js +++ b/website/public/js/services/guideServices.js @@ -5,8 +5,8 @@ */ angular.module('habitrpg').factory('Guide', -['$rootScope', 'User', '$timeout', -function($rootScope, User, $timeout) { +['$rootScope', 'User', '$timeout', '$state', +function($rootScope, User, $timeout, $state) { /** * Init and show the welcome tour. Note we do it listening to a $rootScope broadcasted 'userLoaded' message, * this because we need to determine whether to show the tour *after* the user has been pulled from the server, @@ -17,7 +17,7 @@ function($rootScope, User, $timeout) { if (User.user.flags.showTour === false) return; var tourSteps = [ { - element: ".main-herobox", + orphan:true, title: window.env.t('welcomeHabit'), content: window.env.t('welcomeHabitT1') + " Justin, " + window.env.t('welcomeHabitT2'), }, { @@ -29,11 +29,11 @@ function($rootScope, User, $timeout) { title: window.env.t('avatarCustom'), content: window.env.t('avatarCustomText'), }, { - element: "#bars", + element: ".hero-stats", title: window.env.t('hitPoints'), content: window.env.t('hitPointsText'), }, { - element: "#bars", + element: ".hero-stats", title: window.env.t('expPoints'), content: window.env.t('expPointsText'), }, { @@ -55,7 +55,7 @@ function($rootScope, User, $timeout) { element: "ul.todos", title: window.env.t('todos'), content: window.env.t('tourTodos'), - placement: "top" + placement: "top", }, { element: "ul.main-list.rewards", title: window.env.t('rewards'), @@ -67,34 +67,32 @@ function($rootScope, User, $timeout) { content: window.env.t('hoverOverText'), placement: "right" }, { - element: "ul.habits li:first-child", + orphan:true, title: window.env.t('unlockFeatures'), content: window.env.t('unlockFeaturesT1') + " " + window.env.t('habitWiki') + " " + window.env.t('unlockFeaturesT2'), placement: "right" } ]; - _.each(tourSteps, function(step){ - if (env.worldDmg.guide) { - step.content = "
" + step.content + "
"; - } else { - step.content = "
" + step.content + "
"; - } - }); $('.main-herobox').popover('destroy'); var tour = new Tour({ - template: "
" + - "

" + - "
" + - "" + - "" + - "
" + - "
", + backdrop: true, + //orphan: true, + //keyboard: false, + template: '', onEnd: function(){ User.set({'flags.showTour': false}); } }); - tourSteps.forEach(function(step) { - tour.addStep(_.defaults(step, {html: true})); + _.each(tourSteps, function(step) { + step.content = "
" + step.content + "
"; + step.onShow = function(){ + // Since all the steps are currently on the tasks page, ensure we go back there for each step in case they + // clicked elsewhere during the tour. FIXME: $state.go() returns a promise, necessary for async tour steps; + // however, that's not working here - have to use timeout instead :/ + if (!$state.is('tasks')) return $timeout(function(){$state.go('tasks');}, 0) + } + step.html = true; + tour.addStep(step); }); tour.restart(); // Tour doesn't quite mesh with our handling of flags.showTour, just restart it on page load //tour.start(true); diff --git a/website/public/manifest.json b/website/public/manifest.json index 9592271a9e..16a2b35758 100644 --- a/website/public/manifest.json +++ b/website/public/manifest.json @@ -73,6 +73,7 @@ "bower_components/pnotify/jquery.pnotify.default.css", "bower_components/pnotify/jquery.pnotify.default.icons.css", "common/sprites/habitrpg-shared.css", + "bower_components/bootstrap-tour/build/css/bootstrap-tour.css", "fontello/css/fontelico.css" ] }, diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index a798666d62..e400c07a06 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -23,9 +23,9 @@ var accountSuspended = function(uuid){ code: 'ACCOUNT_SUSPENDED' }; } -// escape email for regex, then search case-insensitive. See http://stackoverflow.com/a/3561711/362790 -var mongoEmailRegex = function(email){ - return new RegExp('^' + email.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i'); +// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790 +var RegexEscape = function(s){ + return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i'); } api.auth = function(req, res, next) { @@ -76,10 +76,10 @@ api.registerUser = function(req, res, next) { cb(); }, findEmail: function(cb) { - User.findOne({'auth.local.email': req.body.email}, cb); + User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {_id:1}, cb); }, findUname: function(cb) { - User.findOne({'auth.local.username': req.body.username}, cb); + User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {_id:1}, cb); }, findFacebook: function(cb){ User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb); @@ -211,7 +211,7 @@ api.resetPassword = function(req, res, next){ newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later) hashed_password = utils.encryptPassword(newPassword, salt); - User.findOne({'auth.local.email':mongoEmailRegex(email)}, function(err, user){ + User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){ if (err) return next(err); if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email}); user.auth.local.salt = salt; @@ -238,7 +238,7 @@ var invalidPassword = function(user, password){ api.changeUsername = function(req, res, next) { async.waterfall([ function(cb){ - User.findOne({'auth.local.username': req.body.username}, {auth:1}, cb); + User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb); }, function(found, cb){ if (found) return cb({code:401, err: "Username already taken"}); @@ -255,7 +255,7 @@ api.changeUsername = function(req, res, next) { api.changeEmail = function(req, res, next){ async.waterfall([ function(cb){ - User.findOne({'auth.local.email': mongoEmailRegex(req.body.email)}, {auth:1}, cb); + User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb); }, function(found, cb){ if(found) return cb({code:401, err: "Email already taken"}); diff --git a/website/views/index.jade b/website/views/index.jade index 5698a42884..8c93d28e04 100644 --- a/website/views/index.jade +++ b/website/views/index.jade @@ -10,6 +10,11 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a meta(name='viewport', content='width=device-width, initial-scale=1.0') meta(name='apple-mobile-web-app-capable', content='yes') + if(env.NODE_ENV == 'production') + script(type='text/javascript'). + window.NREUM||(NREUM={}),__nr_require=function(t,e,n){function r(n){if(!e[n]){var o=e[n]={exports:{}};t[n][0].call(o.exports,function(e){var o=t[n][1][e];return r(o?o:e)},o,o.exports)}return e[n].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;od;d++)c[d].apply(u,n);return u}function a(t,e){f[t]=s(t).concat(e)}function s(t){return f[t]||[]}function c(){return n(e)}var f={};return{on:a,emit:e,create:c,listeners:s,_events:f}}function r(){return{}}var o="nr@context",i=t("gos");e.exports=n()},{gos:"7eSDFh"}],ee:[function(t,e){e.exports=t("QJf3ax")},{}],3:[function(t){function e(t,e,n,i,s){try{c?c-=1:r("err",[s||new UncaughtException(t,e,n)])}catch(f){try{r("ierr",[f,(new Date).getTime(),!0])}catch(u){}}return"function"==typeof a?a.apply(this,o(arguments)):!1}function UncaughtException(t,e,n){this.message=t||"Uncaught error with no additional information",this.sourceURL=e,this.line=n}function n(t){r("err",[t,(new Date).getTime()])}var r=t("handle"),o=t(5),i=t("ee"),a=window.onerror,s=!1,c=0;t("loader").features.err=!0,window.onerror=e,NREUM.noticeError=n;try{throw new Error}catch(f){"stack"in f&&(t(1),t(4),"addEventListener"in window&&t(2),window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&t(3),s=!0)}i.on("fn-start",function(){s&&(c+=1)}),i.on("fn-err",function(t,e,r){s&&(this.thrown=!0,n(r))}),i.on("fn-end",function(){s&&!this.thrown&&c>0&&(c-=1)}),i.on("internal-error",function(t){r("ierr",[t,(new Date).getTime(),!0])})},{1:8,2:5,3:9,4:7,5:21,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],4:[function(t){function e(){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var n=t("ee"),r=t("handle"),o=t(2);t("loader").features.stn=!0,t(1),n.on("fn-start",function(t){var e=t[0];e instanceof Event&&(this.bstStart=Date.now())}),n.on("fn-end",function(t,e){var n=t[0];n instanceof Event&&r("bst",[n,e,this.bstStart,Date.now()])}),o.on("fn-start",function(t,e,n){this.bstStart=Date.now(),this.bstType=n}),o.on("fn-end",function(t,e){r("bstTimer",[e,this.bstStart,Date.now(),this.bstType])}),n.on("pushState-start",function(){this.time=Date.now(),this.startPath=location.pathname+location.hash}),n.on("pushState-end",function(){r("bstHist",[location.pathname+location.hash,this.startPath,this.time])}),"addEventListener"in window.performance&&(window.performance.addEventListener("webkitresourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.webkitClearResourceTimings()},!1),window.performance.addEventListener("resourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.clearResourceTimings()},!1)),document.addEventListener("scroll",e,!1),document.addEventListener("keypress",e,!1),document.addEventListener("click",e,!1)}},{1:6,2:8,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],5:[function(t,e){function n(t){i.inPlace(t,["addEventListener","removeEventListener"],"-",r)}function r(t){return t[1]}var o=(t(1),t("ee").create()),i=t(2)(o),a=t("gos");if(e.exports=o,n(window),"getPrototypeOf"in Object){for(var s=document;s&&!s.hasOwnProperty("addEventListener");)s=Object.getPrototypeOf(s);s&&n(s);for(var c=XMLHttpRequest.prototype;c&&!c.hasOwnProperty("addEventListener");)c=Object.getPrototypeOf(c);c&&n(c)}else XMLHttpRequest.prototype.hasOwnProperty("addEventListener")&&n(XMLHttpRequest.prototype);o.on("addEventListener-start",function(t){if(t[1]){var e=t[1];"function"==typeof e?this.wrapped=t[1]=a(e,"nr@wrapped",function(){return i(e,"fn-",null,e.name||"anonymous")}):"function"==typeof e.handleEvent&&i.inPlace(e,["handleEvent"],"fn-")}}),o.on("removeEventListener-start",function(t){var e=this.wrapped;e&&(t[1]=e)})},{1:21,2:22,ee:"QJf3ax",gos:"7eSDFh"}],6:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window.history,["pushState"],"-")},{1:22,2:21,ee:"QJf3ax"}],7:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window,["requestAnimationFrame","mozRequestAnimationFrame","webkitRequestAnimationFrame","msRequestAnimationFrame"],"raf-"),n.on("raf-start",function(t){t[0]=r(t[0],"fn-")})},{1:22,2:21,ee:"QJf3ax"}],8:[function(t,e){function n(t,e,n){var r=t[0];"string"==typeof r&&(r=new Function(r)),t[0]=o(r,"fn-",null,n)}var r=(t(2),t("ee").create()),o=t(1)(r);e.exports=r,o.inPlace(window,["setTimeout","setInterval","setImmediate"],"setTimer-"),r.on("setTimer-start",n)},{1:22,2:21,ee:"QJf3ax"}],9:[function(t,e){function n(){c.inPlace(this,d,"fn-")}function r(t,e){c.inPlace(e,["onreadystatechange"],"fn-")}function o(t,e){return e}var i=t("ee").create(),a=t(1),s=t(2),c=s(i),f=s(a),u=window.XMLHttpRequest,d=["onload","onerror","onabort","onloadstart","onloadend","onprogress","ontimeout"];e.exports=i,window.XMLHttpRequest=function(t){var e=new u(t);try{i.emit("new-xhr",[],e),f.inPlace(e,["addEventListener","removeEventListener"],"-",function(t,e){return e}),e.addEventListener("readystatechange",n,!1)}catch(r){try{i.emit("internal-error",[r])}catch(o){}}return e},window.XMLHttpRequest.prototype=u.prototype,c.inPlace(XMLHttpRequest.prototype,["open","send"],"-xhr-",o),i.on("send-xhr-start",r),i.on("open-xhr-start",r)},{1:5,2:22,ee:"QJf3ax"}],10:[function(t){function e(t){if("string"==typeof t&&t.length)return t.length;if("object"!=typeof t)return void 0;if("undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer&&t.byteLength)return t.byteLength;if("undefined"!=typeof Blob&&t instanceof Blob&&t.size)return t.size;if("undefined"!=typeof FormData&&t instanceof FormData)return void 0;try{return JSON.stringify(t).length}catch(e){return void 0}}function n(t){var n=this.params,r=this.metrics;if(!this.ended){this.ended=!0;for(var i=0;c>i;i++)t.removeEventListener(s[i],this.listener,!1);if(!n.aborted){if(r.duration=(new Date).getTime()-this.startTime,4===t.readyState){n.status=t.status;var a=t.responseType,f="arraybuffer"===a||"blob"===a||"json"===a?t.response:t.responseText,u=e(f);if(u&&(r.rxSize=u),this.sameOrigin){var d=t.getResponseHeader("X-NewRelic-App-Data");d&&(n.cat=d.split(", ").pop())}}else n.status=0;r.cbTime=this.cbTime,o("xhr",[n,r,this.startTime])}}}function r(t,e){var n=i(e),r=t.params;r.host=n.hostname+":"+n.port,r.pathname=n.pathname,t.sameOrigin=n.sameOrigin}if(window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&!/CriOS/.test(navigator.userAgent)){t("loader").features.xhr=!0;var o=t("handle"),i=t(2),a=t("ee"),s=["load","error","abort","timeout"],c=s.length,f=t(1);t(4),t(3),a.on("new-xhr",function(){this.totalCbs=0,this.called=0,this.cbTime=0,this.end=n,this.ended=!1,this.xhrGuids={}}),a.on("open-xhr-start",function(t){this.params={method:t[0]},r(this,t[1]),this.metrics={}}),a.on("open-xhr-end",function(t,e){"loader_config"in NREUM&&"xpid"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader("X-NewRelic-ID",NREUM.loader_config.xpid)}),a.on("send-xhr-start",function(t,n){var r=this.metrics,o=t[0],i=this;if(r&&o){var f=e(o);f&&(r.txSize=f)}this.startTime=(new Date).getTime(),this.listener=function(t){try{"abort"===t.type&&(i.params.aborted=!0),("load"!==t.type||i.called===i.totalCbs&&(i.onloadCalled||"function"!=typeof n.onload))&&i.end(n)}catch(e){try{a.emit("internal-error",[e])}catch(r){}}};for(var u=0;c>u;u++)n.addEventListener(s[u],this.listener,!1)}),a.on("xhr-cb-time",function(t,e,n){this.cbTime+=t,e?this.onloadCalled=!0:this.called+=1,this.called!==this.totalCbs||!this.onloadCalled&&"function"==typeof n.onload||this.end(n)}),a.on("xhr-load-added",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&!this.xhrGuids[n]&&(this.xhrGuids[n]=!0,this.totalCbs+=1)}),a.on("xhr-load-removed",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&this.xhrGuids[n]&&(delete this.xhrGuids[n],this.totalCbs-=1)}),a.on("addEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-added",[t[1],t[2]],e)}),a.on("removeEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-removed",[t[1],t[2]],e)}),a.on("fn-start",function(t,e,n){e instanceof XMLHttpRequest&&("onload"===n&&(this.onload=!0),("load"===(t[0]&&t[0].type)||this.onload)&&(this.xhrCbStart=(new Date).getTime()))}),a.on("fn-end",function(t,e){this.xhrCbStart&&a.emit("xhr-cb-time",[(new Date).getTime()-this.xhrCbStart,this.onload,e],e)})}},{1:"XL7HBI",2:11,3:9,4:5,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],11:[function(t,e){e.exports=function(t){var e=document.createElement("a"),n=window.location,r={};e.href=t,r.port=e.port;var o=e.href.split("://");return!r.port&&o[1]&&(r.port=o[1].split("/")[0].split("@").pop().split(":")[1]),r.port&&"0"!==r.port||(r.port="https"===o[0]?"443":"80"),r.hostname=e.hostname||n.hostname,r.pathname=e.pathname,r.protocol=o[0],"/"!==r.pathname.charAt(0)&&(r.pathname="/"+r.pathname),r.sameOrigin=!e.hostname||e.hostname===document.domain&&e.port===n.port&&e.protocol===n.protocol,r}},{}],gos:[function(t,e){e.exports=t("7eSDFh")},{}],"7eSDFh":[function(t,e){function n(t,e,n){if(r.call(t,e))return t[e];var o=n();if(Object.defineProperty&&Object.keys)try{return Object.defineProperty(t,e,{value:o,writable:!0,enumerable:!1}),o}catch(i){}return t[e]=o,o}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],D5DuLP:[function(t,e){function n(t,e,n){return r.listeners(t).length?r.emit(t,e,n):(o[t]||(o[t]=[]),void o[t].push(e))}var r=t("ee").create(),o={};e.exports=n,n.ee=r,r.q=o},{ee:"QJf3ax"}],handle:[function(t,e){e.exports=t("D5DuLP")},{}],XL7HBI:[function(t,e){function n(t){var e=typeof t;return!t||"object"!==e&&"function"!==e?-1:t===window?0:i(t,o,function(){return r++})}var r=1,o="nr@id",i=t("gos");e.exports=n},{gos:"7eSDFh"}],id:[function(t,e){e.exports=t("XL7HBI")},{}],loader:[function(t,e){e.exports=t("G9z0Bl")},{}],G9z0Bl:[function(t,e){function n(){var t=l.info=NREUM.info;if(t&&t.licenseKey&&t.applicationID&&f&&f.body){s(h,function(e,n){e in t||(t[e]=n)}),l.proto="https"===p.split(":")[0]||t.sslForHttp?"https://":"http://",a("mark",["onload",i()]);var e=f.createElement("script");e.src=l.proto+t.agent,f.body.appendChild(e)}}function r(){"complete"===f.readyState&&o()}function o(){a("mark",["domContent",i()])}function i(){return(new Date).getTime()}var a=t("handle"),s=t(1),c=window,f=c.document,u="addEventListener",d="attachEvent",p=(""+location).split("?")[0],h={beacon:"bam.nr-data.net",errorBeacon:"bam.nr-data.net",agent:"js-agent.newrelic.com/nr-515.min.js"},l=e.exports={offset:i(),origin:p,features:{}};f[u]?(f[u]("DOMContentLoaded",o,!1),c[u]("load",n,!1)):(f[d]("onreadystatechange",r),c[d]("onload",n)),a("mark",["firstbyte",i()])},{1:20,handle:"D5DuLP"}],20:[function(t,e){function n(t,e){var n=[],o="",i=0;for(o in t)r.call(t,o)&&(n[i]=e(o,t[o]),i+=1);return n}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],21:[function(t,e){function n(t,e,n){e||(e=0),"undefined"==typeof n&&(n=t?t.length:0);for(var r=-1,o=n-e||0,i=Array(0>o?0:o);++rd;d++)c[d].apply(u,n);return u}function a(t,e){f[t]=s(t).concat(e)}function s(t){return f[t]||[]}function c(){return n(e)}var f={};return{on:a,emit:e,create:c,listeners:s,_events:f}}function r(){return{}}var o="nr@context",i=t("gos");e.exports=n()},{gos:"7eSDFh"}],ee:[function(t,e){e.exports=t("QJf3ax")},{}],3:[function(t){function e(t,e,n,i,s){try{c?c-=1:r("err",[s||new UncaughtException(t,e,n)])}catch(f){try{r("ierr",[f,(new Date).getTime(),!0])}catch(u){}}return"function"==typeof a?a.apply(this,o(arguments)):!1}function UncaughtException(t,e,n){this.message=t||"Uncaught error with no additional information",this.sourceURL=e,this.line=n}function n(t){r("err",[t,(new Date).getTime()])}var r=t("handle"),o=t(5),i=t("ee"),a=window.onerror,s=!1,c=0;t("loader").features.err=!0,window.onerror=e,NREUM.noticeError=n;try{throw new Error}catch(f){"stack"in f&&(t(1),t(4),"addEventListener"in window&&t(2),window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&t(3),s=!0)}i.on("fn-start",function(){s&&(c+=1)}),i.on("fn-err",function(t,e,r){s&&(this.thrown=!0,n(r))}),i.on("fn-end",function(){s&&!this.thrown&&c>0&&(c-=1)}),i.on("internal-error",function(t){r("ierr",[t,(new Date).getTime(),!0])})},{1:8,2:5,3:9,4:7,5:21,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],4:[function(t){function e(){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var n=t("ee"),r=t("handle"),o=t(2);t("loader").features.stn=!0,t(1),n.on("fn-start",function(t){var e=t[0];e instanceof Event&&(this.bstStart=Date.now())}),n.on("fn-end",function(t,e){var n=t[0];n instanceof Event&&r("bst",[n,e,this.bstStart,Date.now()])}),o.on("fn-start",function(t,e,n){this.bstStart=Date.now(),this.bstType=n}),o.on("fn-end",function(t,e){r("bstTimer",[e,this.bstStart,Date.now(),this.bstType])}),n.on("pushState-start",function(){this.time=Date.now(),this.startPath=location.pathname+location.hash}),n.on("pushState-end",function(){r("bstHist",[location.pathname+location.hash,this.startPath,this.time])}),"addEventListener"in window.performance&&(window.performance.addEventListener("webkitresourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.webkitClearResourceTimings()},!1),window.performance.addEventListener("resourcetimingbufferfull",function(){r("bstResource",[window.performance.getEntriesByType("resource")]),window.performance.clearResourceTimings()},!1)),document.addEventListener("scroll",e,!1),document.addEventListener("keypress",e,!1),document.addEventListener("click",e,!1)}},{1:6,2:8,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],5:[function(t,e){function n(t){i.inPlace(t,["addEventListener","removeEventListener"],"-",r)}function r(t){return t[1]}var o=(t(1),t("ee").create()),i=t(2)(o),a=t("gos");if(e.exports=o,n(window),"getPrototypeOf"in Object){for(var s=document;s&&!s.hasOwnProperty("addEventListener");)s=Object.getPrototypeOf(s);s&&n(s);for(var c=XMLHttpRequest.prototype;c&&!c.hasOwnProperty("addEventListener");)c=Object.getPrototypeOf(c);c&&n(c)}else XMLHttpRequest.prototype.hasOwnProperty("addEventListener")&&n(XMLHttpRequest.prototype);o.on("addEventListener-start",function(t){if(t[1]){var e=t[1];"function"==typeof e?this.wrapped=t[1]=a(e,"nr@wrapped",function(){return i(e,"fn-",null,e.name||"anonymous")}):"function"==typeof e.handleEvent&&i.inPlace(e,["handleEvent"],"fn-")}}),o.on("removeEventListener-start",function(t){var e=this.wrapped;e&&(t[1]=e)})},{1:21,2:22,ee:"QJf3ax",gos:"7eSDFh"}],6:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window.history,["pushState"],"-")},{1:22,2:21,ee:"QJf3ax"}],7:[function(t,e){var n=(t(2),t("ee").create()),r=t(1)(n);e.exports=n,r.inPlace(window,["requestAnimationFrame","mozRequestAnimationFrame","webkitRequestAnimationFrame","msRequestAnimationFrame"],"raf-"),n.on("raf-start",function(t){t[0]=r(t[0],"fn-")})},{1:22,2:21,ee:"QJf3ax"}],8:[function(t,e){function n(t,e,n){var r=t[0];"string"==typeof r&&(r=new Function(r)),t[0]=o(r,"fn-",null,n)}var r=(t(2),t("ee").create()),o=t(1)(r);e.exports=r,o.inPlace(window,["setTimeout","setInterval","setImmediate"],"setTimer-"),r.on("setTimer-start",n)},{1:22,2:21,ee:"QJf3ax"}],9:[function(t,e){function n(){c.inPlace(this,d,"fn-")}function r(t,e){c.inPlace(e,["onreadystatechange"],"fn-")}function o(t,e){return e}var i=t("ee").create(),a=t(1),s=t(2),c=s(i),f=s(a),u=window.XMLHttpRequest,d=["onload","onerror","onabort","onloadstart","onloadend","onprogress","ontimeout"];e.exports=i,window.XMLHttpRequest=function(t){var e=new u(t);try{i.emit("new-xhr",[],e),f.inPlace(e,["addEventListener","removeEventListener"],"-",function(t,e){return e}),e.addEventListener("readystatechange",n,!1)}catch(r){try{i.emit("internal-error",[r])}catch(o){}}return e},window.XMLHttpRequest.prototype=u.prototype,c.inPlace(XMLHttpRequest.prototype,["open","send"],"-xhr-",o),i.on("send-xhr-start",r),i.on("open-xhr-start",r)},{1:5,2:22,ee:"QJf3ax"}],10:[function(t){function e(t){if("string"==typeof t&&t.length)return t.length;if("object"!=typeof t)return void 0;if("undefined"!=typeof ArrayBuffer&&t instanceof ArrayBuffer&&t.byteLength)return t.byteLength;if("undefined"!=typeof Blob&&t instanceof Blob&&t.size)return t.size;if("undefined"!=typeof FormData&&t instanceof FormData)return void 0;try{return JSON.stringify(t).length}catch(e){return void 0}}function n(t){var n=this.params,r=this.metrics;if(!this.ended){this.ended=!0;for(var i=0;c>i;i++)t.removeEventListener(s[i],this.listener,!1);if(!n.aborted){if(r.duration=(new Date).getTime()-this.startTime,4===t.readyState){n.status=t.status;var a=t.responseType,f="arraybuffer"===a||"blob"===a||"json"===a?t.response:t.responseText,u=e(f);if(u&&(r.rxSize=u),this.sameOrigin){var d=t.getResponseHeader("X-NewRelic-App-Data");d&&(n.cat=d.split(", ").pop())}}else n.status=0;r.cbTime=this.cbTime,o("xhr",[n,r,this.startTime])}}}function r(t,e){var n=i(e),r=t.params;r.host=n.hostname+":"+n.port,r.pathname=n.pathname,t.sameOrigin=n.sameOrigin}if(window.XMLHttpRequest&&XMLHttpRequest.prototype&&XMLHttpRequest.prototype.addEventListener&&!/CriOS/.test(navigator.userAgent)){t("loader").features.xhr=!0;var o=t("handle"),i=t(2),a=t("ee"),s=["load","error","abort","timeout"],c=s.length,f=t(1);t(4),t(3),a.on("new-xhr",function(){this.totalCbs=0,this.called=0,this.cbTime=0,this.end=n,this.ended=!1,this.xhrGuids={}}),a.on("open-xhr-start",function(t){this.params={method:t[0]},r(this,t[1]),this.metrics={}}),a.on("open-xhr-end",function(t,e){"loader_config"in NREUM&&"xpid"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader("X-NewRelic-ID",NREUM.loader_config.xpid)}),a.on("send-xhr-start",function(t,n){var r=this.metrics,o=t[0],i=this;if(r&&o){var f=e(o);f&&(r.txSize=f)}this.startTime=(new Date).getTime(),this.listener=function(t){try{"abort"===t.type&&(i.params.aborted=!0),("load"!==t.type||i.called===i.totalCbs&&(i.onloadCalled||"function"!=typeof n.onload))&&i.end(n)}catch(e){try{a.emit("internal-error",[e])}catch(r){}}};for(var u=0;c>u;u++)n.addEventListener(s[u],this.listener,!1)}),a.on("xhr-cb-time",function(t,e,n){this.cbTime+=t,e?this.onloadCalled=!0:this.called+=1,this.called!==this.totalCbs||!this.onloadCalled&&"function"==typeof n.onload||this.end(n)}),a.on("xhr-load-added",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&!this.xhrGuids[n]&&(this.xhrGuids[n]=!0,this.totalCbs+=1)}),a.on("xhr-load-removed",function(t,e){var n=""+f(t)+!!e;this.xhrGuids&&this.xhrGuids[n]&&(delete this.xhrGuids[n],this.totalCbs-=1)}),a.on("addEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-added",[t[1],t[2]],e)}),a.on("removeEventListener-end",function(t,e){e instanceof XMLHttpRequest&&"load"===t[0]&&a.emit("xhr-load-removed",[t[1],t[2]],e)}),a.on("fn-start",function(t,e,n){e instanceof XMLHttpRequest&&("onload"===n&&(this.onload=!0),("load"===(t[0]&&t[0].type)||this.onload)&&(this.xhrCbStart=(new Date).getTime()))}),a.on("fn-end",function(t,e){this.xhrCbStart&&a.emit("xhr-cb-time",[(new Date).getTime()-this.xhrCbStart,this.onload,e],e)})}},{1:"XL7HBI",2:11,3:9,4:5,ee:"QJf3ax",handle:"D5DuLP",loader:"G9z0Bl"}],11:[function(t,e){e.exports=function(t){var e=document.createElement("a"),n=window.location,r={};e.href=t,r.port=e.port;var o=e.href.split("://");return!r.port&&o[1]&&(r.port=o[1].split("/")[0].split("@").pop().split(":")[1]),r.port&&"0"!==r.port||(r.port="https"===o[0]?"443":"80"),r.hostname=e.hostname||n.hostname,r.pathname=e.pathname,r.protocol=o[0],"/"!==r.pathname.charAt(0)&&(r.pathname="/"+r.pathname),r.sameOrigin=!e.hostname||e.hostname===document.domain&&e.port===n.port&&e.protocol===n.protocol,r}},{}],gos:[function(t,e){e.exports=t("7eSDFh")},{}],"7eSDFh":[function(t,e){function n(t,e,n){if(r.call(t,e))return t[e];var o=n();if(Object.defineProperty&&Object.keys)try{return Object.defineProperty(t,e,{value:o,writable:!0,enumerable:!1}),o}catch(i){}return t[e]=o,o}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],D5DuLP:[function(t,e){function n(t,e,n){return r.listeners(t).length?r.emit(t,e,n):(o[t]||(o[t]=[]),void o[t].push(e))}var r=t("ee").create(),o={};e.exports=n,n.ee=r,r.q=o},{ee:"QJf3ax"}],handle:[function(t,e){e.exports=t("D5DuLP")},{}],XL7HBI:[function(t,e){function n(t){var e=typeof t;return!t||"object"!==e&&"function"!==e?-1:t===window?0:i(t,o,function(){return r++})}var r=1,o="nr@id",i=t("gos");e.exports=n},{gos:"7eSDFh"}],id:[function(t,e){e.exports=t("XL7HBI")},{}],loader:[function(t,e){e.exports=t("G9z0Bl")},{}],G9z0Bl:[function(t,e){function n(){var t=l.info=NREUM.info;if(t&&t.licenseKey&&t.applicationID&&f&&f.body){s(h,function(e,n){e in t||(t[e]=n)}),l.proto="https"===p.split(":")[0]||t.sslForHttp?"https://":"http://",a("mark",["onload",i()]);var e=f.createElement("script");e.src=l.proto+t.agent,f.body.appendChild(e)}}function r(){"complete"===f.readyState&&o()}function o(){a("mark",["domContent",i()])}function i(){return(new Date).getTime()}var a=t("handle"),s=t(1),c=window,f=c.document,u="addEventListener",d="attachEvent",p=(""+location).split("?")[0],h={beacon:"bam.nr-data.net",errorBeacon:"bam.nr-data.net",agent:"js-agent.newrelic.com/nr-515.min.js"},l=e.exports={offset:i(),origin:p,features:{}};f[u]?(f[u]("DOMContentLoaded",o,!1),c[u]("load",n,!1)):(f[d]("onreadystatechange",r),c[d]("onload",n)),a("mark",["firstbyte",i()])},{1:20,handle:"D5DuLP"}],20:[function(t,e){function n(t,e){var n=[],o="",i=0;for(o in t)r.call(t,o)&&(n[i]=e(o,t[o]),i+=1);return n}var r=Object.prototype.hasOwnProperty;e.exports=n},{}],21:[function(t,e){function n(t,e,n){e||(e=0),"undefined"==typeof n&&(n=t?t.length:0);for(var r=-1,o=n-e||0,i=Array(0>o?0:o);++r