diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 000000000..1fdbf4373 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,5 @@ +{ + "directory": "tests/vendor", + "color" : false, + "interactive": false +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..100ef4eab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +[*] +end_of_line = lf +insert_final_newline = true + +[*.js] +indent_style = tab diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..23dee8c83 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*.headers diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..955eb05e3 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,112 @@ +{ + "env": { + "browser": true + }, + "plugins": [ + "html" + ], + "globals": { + "BOOMR": true, + "BOOMR_start": true, + "BOOMR_lstart": true, + "BOOMR_onload": true, + "BOOMR_test": true, + "console": false, + "unescape": false, + "BOOMR_configt": true, + "BOOMR_check_doc_domain": true + }, + "rules": { + // + // Rules that were enabled by default in pre-1.0 eslint + // https://github.com/eslint/eslint/blob/master/docs/user-guide/migrating-to-1.0.0.md + // re-enable all the ones we are not specifically disabling + // + "no-alert": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-wrappers": 2, + "no-octal-escape": 2, + "no-process-exit": 2, + "no-proto": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-undef-init": 2, + "no-unused-expressions": 2, + "no-use-before-define": [2, { "functions": false }], + "no-with": 2, + "comma-spacing": 2, + "curly": [2, "all"], + "eol-last": 2, + "no-extra-parens": [2, "functions"], + "eqeqeq": 2, + "new-parens": 2, + "semi": 2, + "space-infix-ops": 2, + "yoda": [2, "never"], + + // + // Changes over defaults + // + "keyword-spacing": 2, + "no-mixed-spaces-and-tabs": [2, true], + "quotes": [2, "double", "avoid-escape"], + "dot-notation": [2, {"allowKeywords": false}], + "space-unary-ops": 1, + "key-spacing": [1, {"beforeColon": false, "afterColon": true, "mode": "minimum"}], + "no-empty": 2, + "brace-style": [1, "stroustrup", { "allowSingleLine": true }], + "semi-spacing": [2, {"before": false, "after": true}], + "indent": [2, "tab", {"VariableDeclarator": 0}], + "space-before-function-paren": [2, "never"], + "no-trailing-spaces": [2, { "skipBlankLines": false }], + "linebreak-style": [2, "unix"], + "comma-dangle": [2, "never"], + "operator-linebreak": [2, "after"], + "space-in-parens": [2, "never"], + + // + // Disabled rules + // + + // We have a lot of variables in underscore_casing + "camelcase": 0, + + // Not ready for strict-mode yet + "strict": 0, + + // We have some functions like BOOMR_check_doc_domain or BOOMR. + "new-cap": 0, + + // We use console.log for debugging + "no-console": 0, + + // We use _s in a couple places for internal vars + "no-underscore-dangle": 0, + + // We delete some global vars for compat with older IE versions + "no-delete-var": 0, + + // We use spaces for alignment in many places + "no-multi-spaces": 0 + } +} diff --git a/.gitignore b/.gitignore index 70d323c3d..240fd23b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,24 @@ -.gitignore *.gz -boomerang-*.js +*.swp +/boomerang-*.js +build/* +*.log +*.diff +.project +.settings/ +tests/vendor/ +tests/coverage/ +tests/results/* +tests/pages/* +node_modules/ +build/ +tests/server/env.json +npm-debug.log +/tests/e2e/e2e.json +/tests/e2e/e2e-debug.json +tests/page-templates/12-react/support/app-component.js +tests/page-templates/12-react/support/app-component.js.map +tests/page-templates/12-react/support/app.js +*.#* +*#* +*~ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..f1e0c7084 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,842 @@ +/* eslint-env node */ +"use strict"; + +// +// Imports +// +var fs = require("fs"); +var path = require("path"); +var fse = require("fs-extra"); +var stripJsonComments = require("strip-json-comments"); +var grunt = require("grunt"); + +// +// Constants +// +var BUILD_PATH = "build"; +var TEST_BUILD_PATH = path.join("tests", "build"); +var TEST_RESULTS_PATH = path.join("tests", "results"); +var TEST_DEBUG_PORT = 4002; +var TEST_URL_BASE = grunt.option("test-url") || "http://localhost:4002"; + +var DEFAULT_UGLIFY_BOOMERANGJS_OPTIONS = { + preserveComments: false, + mangle: { + // for errors.js + except: [ + "createStackForSend", + "loadFinished", + "BOOMR_addError", + "BOOMR_plugins_errors_onerror", + "BOOMR_plugins_errors_onxhrerror", + "BOOMR_plugins_errors_console_error", + "BOOMR_plugins_errors_wrap" + ] + }, + sourceMap: true, + compress: { + sequences: false + } +}; + +// +// Grunt config +// +module.exports = function() { + // + // Paths + // + var testsDir = path.join(__dirname, "tests"); + var pluginsDir = path.join(__dirname, "plugins"); + + // + // Determine source files: + // boomerang.js and plugins/*.js order + // + var src = [ "boomerang.js" ]; + var plugins = grunt.file.readJSON("plugins.json"); + src.push(plugins.plugins); + src.push(path.join(pluginsDir, "zzz-last-plugin.js")); + + // + // Ensure env.json exists + // + var envFile = path.resolve(path.join(testsDir, "server", "env.json")); + if (!fs.existsSync(envFile)) { + var envFileSample = path.resolve(path.join(testsDir, "server", "env.json.sample")); + console.info("Creating env.json from defaults"); + fse.copySync(envFileSample, envFile); + } + + var env = grunt.file.readJSON("tests/server/env.json"); + + // + // Build SauceLabs E2E test URLs + // + var e2eTests = []; + if (grunt.file.exists("tests/e2e/e2e.json")) { + e2eTests = JSON.parse(stripJsonComments(grunt.file.read("tests/e2e/e2e.json"))); + } + var e2eUrls = []; + + for (var i = 0; i < e2eTests.length; i++) { + e2eUrls.push(TEST_URL_BASE + "pages/" + e2eTests[i].path + "/" + e2eTests[i].file + ".html"); + } + + // + // Build numbers + // + var pkg = grunt.file.readJSON("package.json"); + var buildNumber = grunt.option("buildNumber") || 0; + var releaseVersion = pkg.releaseVersion + "." + buildNumber; + var buildDate = Math.round(Date.now() / 1000); + var boomerangVersion = releaseVersion + "." + buildDate; + + // + // Output files + // + + // build file name is based on the version number + var buildFilePrefix = pkg.name + "-" + boomerangVersion; + var buildPathPrefix = path.join(BUILD_PATH, buildFilePrefix); + + var testBuildFilePrefix = pkg.name; + var testBuildPathPrefix = path.join(TEST_BUILD_PATH, testBuildFilePrefix); + + var buildDebug = buildPathPrefix + "-debug.js"; + var buildRelease = buildPathPrefix + ".js"; + var buildReleaseMin = buildPathPrefix + ".min.js"; + var buildTest = testBuildPathPrefix + "-latest-debug.js"; + var buildTestMin = testBuildPathPrefix + "-latest-debug.min.js"; + + // + // Build configuration + // + var buildConfig = { + server: grunt.option("server") || "localhost" + }; + + var bannerFilePathRelative = "./lib/banner.txt"; + var bannerFilePathAbsolute = path.resolve(bannerFilePathRelative); + var bannerString = grunt.file.read(bannerFilePathAbsolute); + + // + // Config + // + grunt.initConfig({ + // package info + pkg: pkg, + + // + // Variables to use in tasks + // + buildConfig: buildConfig, + boomerangVersion: boomerangVersion, + buildFilePrefix: buildFilePrefix, + buildPathPrefix: buildPathPrefix, + testBuildPathPrefix: testBuildPathPrefix, + + // + // Tasks + // + concat: { + options: { + stripBanners: false, + seperator: ";" + }, + debug: { + src: src, + dest: buildDebug + }, + "debug-tests": { + src: [src, "tests/boomerang-test-plugin.js"], + dest: buildTest + }, + release: { + src: src, + dest: buildRelease + } + }, + mkdir: { + test: { + options: { + create: [TEST_RESULTS_PATH] + } + } + }, + eslint: { + target: [ + "Gruntfile.js", + "boomerang.js", + "*.config*.js", + "plugins/*.js", + "tasks/*.js", + "tests/*.js", + "tests/unit/*.js", + "tests/unit/*.html", + "tests/e2e/*.js", + "tests/server/*.js", + "tests/page-templates/**/*.js", + "tests/page-templates/**/*.html", + "tests/page-templates/**/*.js", + "tests/test-templates/**/*.js", + "!tests/page-templates/12-react/support/*" + ] + }, + "string-replace": { + all: { + files: [ + { + src: buildRelease, + dest: buildRelease + }, + { + src: buildDebug, + dest: buildDebug + }, + { + src: buildTest, + dest: buildTest + } + ], + options: { + replacements: [ + { + // Replace 1.0 with 1.[0 or jenkins build #].[date] + pattern: "%boomerang_version%", + replacement: boomerangVersion + }, + { + // strip out BOOMR = BOOMR || {}; in plugins + pattern: /BOOMR\s*=\s*BOOMR\s*\|\|\s*{};/g, + replacement: "" + }, + { + // strip out BOOMR.plugins = BOOMR.plugins || {}; in plugins + pattern: /BOOMR\.plugins\s*=\s*BOOMR\.plugins\s*\|\|\s*{};/g, + replacement: "" + } + ] + } + }, + "debug-tests": { + files: [{ + src: buildTest, + dest: buildTest + }], + options: { + replacements: [ + { + // Send beacons to null + pattern: /beacon_url: .*/, + replacement: "beacon_url: \"/blackhole\"," + } + ] + } + }, + release: { + files: [{ + src: buildRelease, + dest: buildRelease + }], + options: { + // strip out some NOPs + replacements: [ + { + pattern: /else{}/g, + replacement: "" + }, + { + pattern: /\(window\)\);/g, + replacement: "\(window\)\);\n" + }, + { + pattern: /\(\)\);\(function\(/g, + replacement: "\(\)\);\n(function(" + } + ] + } + }, + "remove-sourcemappingurl": { + files: [ + { + src: buildReleaseMin, + dest: buildReleaseMin + } + ], + options: { + replacements: [ + { + pattern: /\/\/# sourceMappingURL=.*/g, + replacement: "" + } + ] + } + } + }, + strip_code: { + debug: { + files: [{ + src: buildRelease + }], + options: { + start_comment: "BEGIN_DEBUG", + end_comment: "END_DEBUG" + } + }, + prod: { + files: [ + { + src: buildDebug + }, + { + src: "<%= testBuildPathPrefix %>*.js" + } + ], + options: { + start_comment: "BEGIN_PROD", + end_comment: "END_PROD" + } + } + }, + copy: { + webserver: { + files: [ + { + expand: true, + nonull: true, + cwd: "tests/", + src: "**/*", + force: true, + dest: env.publish + "/" + } + ] + } + }, + uglify: { + options: { + banner: bannerString + "/* Boomerang Version: <%= boomerangVersion %> */\n" + }, + default: { + options: DEFAULT_UGLIFY_BOOMERANGJS_OPTIONS, + files: [{ + expand: true, + cwd: "build/", + src: ["<%= buildFilePrefix %>-debug.js", + "<%= buildFilePrefix %>.js"], + dest: "build/", + ext: ".min.js", + extDot: "last" + }] + }, + "debug-test-min": { + options: DEFAULT_UGLIFY_BOOMERANGJS_OPTIONS, + files: [{ + src: buildTest, + dest: buildTestMin + }] + }, + plugins: { + options: { + preserveComments: false, + mangle: true, + banner: "", + sourceMap: true, + compress: { + sequences: false + } + }, + files: [{ + expand: true, + cwd: "plugins/", + src: ["./*.js"], + dest: "build/plugins/", + ext: ".min.js", + extDot: "first" + }] + }, + snippets: { + options: { + preserveComments: false, + mangle: true, + banner: "", + compress: { + sequences: false + } + }, + files: [{ + expand: true, + cwd: "tests/page-template-snippets/", + src: ["instrumentXHRSnippetNoScript.tpl"], + dest: "build/snippets/", + ext: ".min.js", + extDot: "first" + }] + } + }, + compress: { + main: { + options: { + mode: "gzip", + level: 9 + }, + files: [ + { + src: buildRelease, + dest: "<%= buildPathPrefix %>.js.gz" + }, + { + src: buildDebug, + dest: "<%= buildPathPrefix %>-debug.js.gz" + }, + { + src: "<%= buildPathPrefix %>.min.js", + dest: "<%= buildPathPrefix %>.min.js.gz" + }, + { + src: "<%= buildPathPrefix %>-debug.min.js", + dest: "<%= buildPathPrefix %>-debug.min.js.gz" + } + ] + }, + plugins: { + options: { + mode: "gzip", + level: 9 + }, + files: [{ + expand: true, + cwd: "build/plugins", + src: "./*.js", + dest: "build/plugins/", + ext: ".min.js.gz", + extDot: "first" + }] + } + }, + "babel": { + "spa-react-test-templates": { + files: { + "tests/page-templates/12-react/support/app-component.js": "tests/page-templates/12-react/support/app.jsx" + } + } + }, + browserify: { + "spa-react-test-templates": { + files: { + "tests/page-templates/12-react/support/app.js": [ + "node_modules/react/dist/react.js", + "node_modules/react-dom/dist/react-dom.js", + "node_modules/react-router/umd/ReactRouter.js", + "tests/page-templates/12-react/support/app-component.js" + ] + } + } + }, + filesize: { + csv: { + files: [{ + expand: true, + cwd: "build", + src: ["./**/*.min.js", "./**/*.min.js.gz"], + ext: ".min.js.gz", + extDot: "first" + }], + options: { + output: { + path: "tests/results/filesizes.csv", + format: "\"{filename}\",{size},{kb},{now:YYYYMMDDhhmmss};", /* https://github.com/k-maru/grunt-filesize/issues/8 */ + append: true + } + } + }, + console: { + files: [{ + expand: true, + cwd: "build", + src: ["./**/*.min.js", "./**/*.min.js.gz"], + ext: ".min.js.gz", + extDot: "first" + }], + options: { + output: { + format: "{filename}: Size of {size:0,0} bytes ({kb:0.00} kilobyte)", + stdout: true + } + } + } + }, + clean: { + options: {}, + build: [ + "build/*", + "tests/build/*", + "tests/results/*.tap", + "tests/results/*.xml", + "tests/coverage/*", + "tests/pages/**/*" + ], + "spa-react-test-templates": [ + "tests/pages/12-react/support/app.js", + "tests/page-templates/12-react/support/app.js", + "tests/page-templates/12-react/support/app-component.js", + "tests/page-templates/12-react/support/*.map" + ], + src: ["plugins/*~", "*.js~", "*.html~"] + }, + karma: { + options: { + singleRun: true, + colors: true, + configFile: "./tests/karma.config.js", + preprocessors: { + "./build/*.js": ["coverage"] + }, + basePath: "./", + files: [ + // relative to tests/ dir + "vendor/mocha/mocha.css", + "vendor/mocha/mocha.js", + "vendor/assertive-chai/dist/assertive-chai.js", + "boomerang-test-framework.js", + "unit/*.js", + "build/*.js" + ] + }, + unit: { + browsers: ["PhantomJS"], + frameworks: ["mocha"] + }, + all: { + browsers: ["Chrome", "Firefox", "IE", "Opera", "Safari", "PhantomJS"] + }, + chrome: { + browsers: ["Chrome"] + }, + ie: { + browsers: ["IE"] + }, + ff: { + browsers: ["Firefox"] + }, + opera: { + browsers: ["Opera"] + }, + safari: { + browsers: ["Safari"] + }, + debug: { + singleRun: false + } + }, + protractor: { + // NOTE: https://github.com/angular/protractor/issues/1512 Selenium+PhantomJS not working in 1.6.1 + options: { + noColor: false, + keepAlive: false + }, + phantomjs: { + configFile: "tests/protractor.config.phantom.js" + }, + chrome: { + configFile: "tests/protractor.config.chrome.js" + }, + debug: { + configFile: "tests/protractor.config.debug.js" + } + }, + protractor_webdriver: { + options: { + keepAlive: true + }, + e2e: { + } + }, + express: { + options: { + port: TEST_DEBUG_PORT, + hostname: "0.0.0.0" + }, + dev: { + options: { + script: "tests/server/app.js" + } + }, + "secondary": { + options: { + script: "tests/server/app.js", + port: (TEST_DEBUG_PORT + 1) + } + }, + doc: { + options: { + port: (TEST_DEBUG_PORT - 1), + script: "tests/server/doc-server.js" + } + } + }, + "saucelabs-mocha": { + all: { + options: { + // username: "", // SAUCE_USERNAME + // key: "", // SAUCE_ACCESS_KEY + build: process.env.CI_BUILD_NUMBER, + tunneled: false + } + }, + unit: { + options: { + urls: [TEST_URL_BASE + "unit/"], + testname: "Boomerang Unit Tests", + browsers: JSON.parse(stripJsonComments(grunt.file.read("tests/browsers-unit.json"))) + } + }, + "unit-debug": { + options: { + urls: [TEST_URL_BASE + "unit/"], + testname: "Boomerang Unit Tests", + browsers: [{ + "browserName": "internet explorer", + "version": "11", + "platform": "Windows 8.1" + }] + } + }, + e2e: { + options: { + urls: e2eUrls, + testname: "Boomerang E2E Tests", + browsers: JSON.parse(stripJsonComments(grunt.file.read("tests/browsers-unit.json"))) + } + }, + "e2e-debug": { + options: { + urls: e2eUrls, + testname: "Boomerang E2E Tests", + browsers: [{ + "browserName": "internet explorer", + "version": "11", + "platform": "Windows 8.1" + }] + } + } + }, + jsdoc: { + dist: { + src: ["boomerang.js", "plugins/*.js"], + jsdoc: "./node_modules/jsdoc/jsdoc.js", + options: { + destination: "build/doc", + package: "package.json", + readme: "README.md", + configure: "jsdoc.conf.json" + } + } + }, + watch: { + test: { + files: [ + "tests/e2e/*.js", + "tests/page-template-snippets/**/*", + "tests/page-templates/**/*", + "tests/unit/**/*", + "tests/test-templates/**/*.js", + "!tests/page-templates/12-react/support/*.jsx", + "!*.#*" + ], + tasks: ["pages-builder"] + }, + "test-react": { + files: [ + "tests/page-templates/12-react/support/*.jsx" + ], + tasks: ["test:build:react"] + }, + boomerang: { + files: [ + "boomerang.js", + "plugins/*.js", + "plugins.json" + ], + tasks: ["build:test"] + }, + express: { + files: [ + "tests/server/*.js" + ], + tasks: ["express:dev", "express:secondary"] + }, + doc: { + files: [ + "boomerang.js", + "plugins/*.js", + "doc/**/**" + ], + tasks: ["clean", "jsdoc"] + } + } + }); + + grunt.loadNpmTasks("gruntify-eslint"); + grunt.loadNpmTasks("grunt-babel"); + grunt.loadNpmTasks("grunt-browserify"); + grunt.loadNpmTasks("grunt-express-server"); + grunt.loadNpmTasks("grunt-karma"); + grunt.loadNpmTasks("grunt-contrib-concat"); + grunt.loadNpmTasks("grunt-string-replace"); + grunt.loadNpmTasks("grunt-contrib-uglify"); + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-contrib-compress"); + grunt.loadNpmTasks("grunt-filesize"); + grunt.loadNpmTasks("grunt-mkdir"); + grunt.loadNpmTasks("grunt-protractor-runner"); + grunt.loadNpmTasks("grunt-protractor-webdriver"); + grunt.loadNpmTasks("grunt-template"); + grunt.loadNpmTasks("grunt-saucelabs"); + grunt.loadNpmTasks("grunt-strip-code"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks("grunt-jsdoc"); + + // tasks/*.js + if (grunt.file.exists("tasks")) { + grunt.loadTasks("tasks"); + } + + grunt.registerTask("pages-builder", "Builds our HTML tests/pages", require(path.join(testsDir, "builder"))); + + // Custom aliases for configured grunt tasks + var aliases = { + "default": ["lint", "build", "test", "metrics"], + + // + // Build + // + "build": ["concat", "build:apply-templates", "uglify", "string-replace:remove-sourcemappingurl", "compress", "metrics"], + "build:test": ["concat:debug", "concat:debug-tests", "!build:apply-templates", "uglify:debug-test-min"], + + // Build steps + "build:apply-templates": [ + "string-replace:all", + "!string-replace:debug-tests", + "string-replace:release", + "!strip_code:debug", + "!strip_code:prod" + ], + + // metrics to generate + "metrics": ["filesize:console"], + + // + // Lint + // + "lint": ["eslint"], + + // + // Test tasks + // + "test": ["build", "test:build", "test:unit", "test:e2e"], + + // builds test files + "test:build": ["mkdir:test", "test:build:react", "pages-builder", "build"], + + // react test files + "test:build:react": ["babel:spa-react-test-templates", "browserify:spa-react-test-templates"], + + // useful for debugging tests, leaves a webbrowser open at http://localhost:3001 + "test:debug": [ + "test:build", + "build:test", + "express:dev", + "express:secondary", + "test:debug:watch" + ], + + // open your browser to http://localhost:4000/debug.html to debug + "test:karma:debug": ["test:build", "build:test", "karma:debug"], + + // unit tests + "test:unit": ["test:build", "build", "karma:unit"], + "test:unit:all": ["build", "karma:all"], + "test:unit:chrome": ["build", "karma:chrome"], + "test:unit:ff": ["build", "karma:ff"], + "test:unit:ie": ["build", "karma:ie"], + "test:unit:opera": ["build", "karma:opera"], + "test:unit:safari": ["build", "karma:safari"], + + // End-to-End tests + "test:e2e": ["test:build", "build", "test:e2e:phantomjs"], + "test:e2e:chrome": ["build", "express:dev", "express:secondary", "protractor_webdriver", "protractor:chrome"], + "test:e2e:debug": ["build", "test:build", "build:test", "express:dev", "express:secondary", "protractor_webdriver", "protractor:debug"], + "test:e2e:phantomjs": ["build", "express:dev", "express:secondary", "protractor_webdriver", "protractor:phantomjs"], + + "test:doc": ["clean", "jsdoc", "express:doc", "watch:doc"], + + // SauceLabs tests + "test:matrix": ["test:matrix:unit", "test:matrix:e2e"], + "test:matrix:e2e": ["pages-builder", "saucelabs-mocha:e2e"], + "test:matrix:e2e:debug": ["pages-builder", "saucelabs-mocha:e2e-debug"], + "test:matrix:unit": ["saucelabs-mocha:unit"], + "test:matrix:unit:debug": ["saucelabs-mocha:unit-debug"] + }; + + function isAlias(task) { + return aliases[task] ? true : false; + } + + // tasks that need to be run more than once (denoted by starting with !) + var rerunTasks = {}; + + function resolveAlias(task) { + var tasks = aliases[task], + resolved = false; + + function checkDuplicates(insertableTask) { + if (rerunTasks[insertableTask]) { + // always return true for tasks that were marked as rerun + return true; + } + + return tasks.indexOf(insertableTask) === -1; + } + + while (!resolved) { + if (tasks.filter(isAlias).length === 0) { + resolved = true; + } + + for (var index = 0; index < tasks.length; index++) { + // if the task starts with !, it should be run more than once + if (tasks[index].startsWith("!")) { + // trim back to the real name + tasks[index] = tasks[index].substr(1); + + // keep track of this task + rerunTasks[tasks[index]] = true; + } + + if (isAlias(tasks[index])) { + var aliasTask = tasks[index]; + var beforeTask = tasks.slice(0, index); + var afterTask = tasks.slice(index + 1, tasks.length); + var insertTask = aliases[aliasTask].filter(checkDuplicates); + tasks = [].concat(beforeTask, insertTask, afterTask); + } + } + } + + return tasks; + } + + Object.keys(aliases).map(function(alias) { + var resolved = resolveAlias(alias); + grunt.log.debug("Resolving task alias: " + alias + " to " + JSON.stringify(resolved)); + grunt.registerTask(alias, resolved); + }); + + // Don't re-generate Docs during test:debug builds running + grunt.registerTask("test:debug:watch", function() { + delete grunt.config.data.watch.doc; + grunt.task.run("watch"); + }); +}; diff --git a/LICENSE.txt b/LICENSE.txt index 8243188df..a1c50727b 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,8 @@ Software Copyright License Agreement (BSD License) -Copyright (c) 2010, Yahoo! Inc. +Copyright (c) 2011, Yahoo! Inc. +Copyright (c) 2011-2012, Log-Normal, Inc. +Copyright (c) 2012-2016, SOASTA, Inc. All rights reserved. Redistribution and use of this software in source and binary forms, diff --git a/Makefile b/Makefile deleted file mode 100644 index ceac7221f..000000000 --- a/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2011, Yahoo! Inc. All rights reserved. -# Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms. - -PLUGINS := - -VERSION := $(shell sed -ne '/^BOOMR\.version/{s/^.*"\([^"]*\)".*/\1/;p;q;}' boomerang.js) -DATE := $(shell date +%s) - -MINIFIER := cat - -all: boomerang-$(VERSION).$(DATE).js - -usage: - echo "Create a release version of boomerang:" - echo " make" - echo "" - echo "Create a release version of boomerang with the dns plugin:" - echo " make PLUGINS=dns.js" - echo "" - echo "Create a yuicompressor minified release version of boomerang:" - echo " make MINIFIER=\"java -jar /path/to/yuicompressor-2.4.2.jar --type js\"" - echo "" - echo "Create a jsmin minified release version of boomerang:" - echo " make MINIFIER=\"/path/to/jsmin\"" - echo "" - -boomerang-$(VERSION).$(DATE).js: boomerang.js $(PLUGINS) - echo - echo "Making $@ ..." - echo "using plugins: $(PLUGINS)..." - cat boomerang.js $(PLUGINS) zzz_last_plugin.js | sed -e 's/^\(BOOMR\.version = "\)$(VERSION)\("\)/\1$(VERSION).$(DATE)\2/' | $(MINIFIER) | perl -pe "s/\(window\)\);/\(window\)\);\n/g" > $@ && echo "done" - echo - -.PHONY: all -.SILENT: diff --git a/README.md b/README.md index c8ebf3bd1..1ed63d69c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,203 @@ -Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Some code Copyright (c) 2011, Yahoo! Inc. All rights reserved. +Some code Copyright (c) 2011-2012, Log-Normal Inc. All rights reserved. +Most code Copyright (c) 2012-2016 SOASTA, Inc. All rights reserved. + Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms. boomerang always comes back, except when it hits something. -This piece of javascript measures a whole bunch of performance characteristics of your user's -web browsing experience. All you have to do is stick it into your web pages and call the +summary +--- + +[![Join the chat at https://gitter.im/SOASTA/boomerang](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SOASTA/boomerang?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +boomerang is a JavaScript library that measures the page load time experienced by real users, commonly called RUM. + +Apart from page load time, boomerang measures a whole bunch of performance characteristics of your user's web browsing experience. All you have to do is stick it into your web pages and call the init() method. -documentation is in the docs/ directory, it's all HTML, so your best bet is to check it out -and view it locally, though it works best through a web server (you'll need cookies). +usage +--- + +## The simple synchronous way + +```html + + + +``` + +**Note** - you must include at least one plugin (it doesn't have to be rt) or else the beacon will never actually be called. + +## The faster, more involved, asynchronous way + +This is what I like to do for sites I control. + +### 1. Add a plugin to init your code + +Create a plugin (call it zzz_init.js or whatever you like) with your init code in there: +```javascript +BOOMR.init({ + config: parameters, + ... +}); +``` +You could also include any other code you need. For example, I include a timer to measure when boomerang has finished loading. + +I call my plugin `zzz_init.js` to remind me to include it last in the plugin list + +### 2. Build boomerang +The build process picks up all the plugins referenced in the `plugins.json` file. To change the plugins included in the boomerang build, change the contents of the file to your needs. + +```bash +grunt clean build +``` + +This creates deployable boomerang versions in the `build` directory, e.g. `build/boomerang-.min.js`. + +Install this file on your web server or origin server where your CDN can pick it up. Set a far future max-age header for it. This file will never change. + +### 3. Asynchronously include the script on your page + +#### 3.1. Adding it to the main document +Include the following code at the *top* of your HTML document: +```html + +``` + +Yes, the best practices say to include scripts at the bottom. That's different. That's for scripts that block downloading of other resources. Including a script this +way will not block other resources, however it _will_ block onload. Including the script at the top of your page gives it a good chance of loading +before the rest of your page does thereby reducing the probability of it blocking the `onload` event. If you don't want to block `onload` either, follow Stoyan's +advice from the Meebo team. + +#### 3.2. Adding it via an iframe + +The method described in 3.1 will still block `onload` on most browsers (Internet Explorer not included). To avoid +blocking `onload`, we could load boomerang in an iframe. Stoyan's documented +the technique on his blog. We've modified it to work across browsers with different configurations, documented on +the lognormal blog. + +For boomerang, this is the code you'll include: + +```html + +``` +The `id` of the script node created by this code MUST be `boomr-if-as` as boomerang looks for that id to determine if it's running within an iframe or not. + +Boomerang will still export the `BOOMR` object to the parent window if running inside an iframe, so the rest of your code should remain unchanged. + +#### 3.3. Identifying when boomerang has loaded + +If you load boomerang asynchronously, there's some uncertainty in when boomerang has completed loading. To get around this, you can subscribe to the +`onBoomerangLoaded` Custom Event on the `document` object: + +```javascript + // Modern browsers + if (document.addEventListener) { + document.addEventListener("onBoomerangLoaded", function(e) { + // e.detail.BOOMR is a reference to the BOOMR global object + }); + } + // IE 6, 7, 8 we use onPropertyChange and look for propertyName === "onBoomerangLoaded" + else if (document.attachEvent) { + document.attachEvent("onpropertychange", function(e) { + if (!e) e=event; + if (e.propertyName === "onBoomerangLoaded") { + // e.detail.BOOMR is a reference to the BOOMR global object + } + }); + } + +``` + +Note that this only works on browsers that support the CustomEvent interface, which at this time is Chrome (including Android), Firefox 6+ (including Android), +Opera (including Android, but not Opera Mini), Safari (including iOS), IE 6+ (but see the code above for the special way to listen for the event on IE6, 7 & 8). + +Boomerang also fires the `onBeforeBoomerangBeacon` and `onBoomerangBeacon` events just before and during beaconing. + +#### 3.4. Method queue pattern + +If you want to call a public method that lives on `BOOMR`, but either don't know if Boomerang has loaded or don't want to wait, you can use the method queue pattern! + +Instead of: +```javascript +BOOMR.addVar('myVarName', 'myVarValue') +``` + +... you can write: +```javascript +BOOMR_mq = window.BOOMR_mq || []; +BOOMR_mq.push(['addVar', 'myVarName', 'myVarValue']); +``` + +Or, if you care about the return value, instead of: +```javascript +var hasMyVar = BOOMR.hasVar('myVarName'); +``` +... you can write: +```javascript +var hasMyVar; +BOOMR_mq = window.BOOMR_mq || []; +BOOMR_mq.push({ + arguments: ['hasVar', 'myVarName'], + callback: function(returnValue) { + hasMyVar = returnValue; + } +}); +``` + +docs +--- +Documentation is in the docs/ sub directory, and is written in HTML. Your best bet is to check it out and view it locally, though it works best through a web server (you'll need cookies). +Thanks to github's awesome `gh-pages` feature, we're able to host the boomerang docs right here on github. Visit http://soasta.github.com/boomerang/doc/ for a browsable version where all +the examples work. + +In case you're browsing this elsewhere, the latest development version of the code and docs are available at https://github.com/bluesmoon/boomerang/, while the latest stable version is +at https://github.com/SOASTA/boomerang/ -An online version of the docs is here: http://lognormal.github.com/boomerang/doc/ +support +--- +We use github issues for discussions, feature requests and bug reports. Get in touch at https://github.com/SOASTA/boomerang/issues +You'll need a github account to participate, but then you'll need one to check out the code as well :) -The latest code and docs is available on http://github.com/lognormal/boomerang/ +Thanks for dropping by, and please leave us a message telling us if you use boomerang. -Discussions are best done using github issues at https://github.com/lognormal/boomerang/issues -You'll need a github account to participate. +boomerang is supported by the devs at SOASTA, and the awesome community of opensource developers that use +and hack it. That's you. Thank you! diff --git a/boomerang-0.9.1280532889.js b/boomerang-0.9.1280532889.js deleted file mode 100644 index 88f7527af..000000000 --- a/boomerang-0.9.1280532889.js +++ /dev/null @@ -1,5 +0,0 @@ -/* - * Copyright (c) 2011, Yahoo! Inc. All rights reserved. - * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. - */ -(function(a){var e,c,b,g=a.document;if(typeof BOOMR==="undefined"){BOOMR={}}if(BOOMR.version){return}BOOMR.version="0.9";e={beacon_url:"",site_domain:a.location.hostname.replace(/.*?([^.]+\.[^.]+)\.?$/,"$1").toLowerCase(),user_ip:"",events:{page_ready:[],page_unload:[],before_beacon:[]},vars:{},disabled_plugins:{},fireEvent:function(d,l){var j,k,m;if(!this.events.hasOwnProperty(d)){return false}m=this.events[d];for(j=0;j=0){h+=d.length;j=j.substring(h,j.indexOf(";",h));return j}return null},setCookie:function(h,d,n,r,l,m){var q="",j,p,o,i="";if(!h){return false}for(j in d){if(d.hasOwnProperty(j)){q+="&"+encodeURIComponent(j)+"="+encodeURIComponent(d[j])}}q=q.replace(/^&/,"");if(n){i=new Date();i.setTime(i.getTime()+n*1000);i=i.toGMTString()}p=h+"="+q;o=p+((n)?"; expires="+i:"")+((r)?"; path="+r:"")+((typeof l!=="undefined")?"; domain="+(l!==null?l:e.site_domain):"")+((m)?"; secure":"");if(p.length<4000){g.cookie=o;return(q===this.getCookie(h))}return false},getSubCookies:function(k){var j,h,d,n,m={};if(!k){return null}j=k.split("&");if(j.length===0){return null}for(h=0,d=j.length;h0)}},init:function(h){var l,d,j=["beacon_url","site_domain","user_ip"];if(!h){h={}}for(l=0;l50){BOOMR.utils.removeCookie(b.cookie);BOOMR.error("took more than 50ms to set cookie... aborting: "+d+" -> "+e,"rt")}return this}};BOOMR.plugins.RT={init:function(d){b.complete=false;b.timers={};BOOMR.utils.pluginConfig(b,d,"RT",["cookie","cookie_exp","strict_referrer"]);BOOMR.subscribe("page_ready",this.done,null,this);BOOMR.subscribe("page_unload",b.start,null,this);return this},startTimer:function(d){if(d){b.timers[d]={start:new Date().getTime()};b.complete=false}return this},endTimer:function(d,e){if(d){b.timers[d]=b.timers[d]||{};if(typeof b.timers[d].end==="undefined"){b.timers[d].end=(typeof e==="number"?e:new Date().getTime())}}return this},setTimer:function(d,e){if(d){b.timers[d]={delta:e}}return this},done:function(){var l,o,d,j,e,k={t_done:1,t_resp:1,t_page:1},i=0,m,h,n=[],f,g;if(b.complete){return this}this.endTimer("t_done");o=c.URL.replace(/#.*/,"");d=j=c.referrer.replace(/#.*/,"");e=BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(b.cookie));BOOMR.utils.removeCookie(b.cookie);if(e!==null&&typeof e.s!=="undefined"&&typeof e.r!=="undefined"){d=e.r;if(!b.strict_referrer||d===j){l=parseInt(e.s,10)}}if(!l){BOOMR.warn("start cookie not set, trying WebTiming API","rt");g=a.performance||a.msPerformance||a.webkitPerformance||a.mozPerformance;if(g&&g.timing){f=g.timing}else{if(a.chrome&&a.chrome.csi){f={requestStart:a.chrome.csi().startE}}}if(f){l=f.requestStart||f.fetchStart||f.navigationStart||undefined}else{BOOMR.warn("This browser doesn't support the WebTiming API","rt")}}BOOMR.removeVar("t_done","t_page","t_resp","u","r","r2");for(m in b.timers){if(!b.timers.hasOwnProperty(m)){continue}h=b.timers[m];if(typeof h.delta!=="number"){if(typeof h.start!=="number"){h.start=l}h.delta=h.end-h.start}if(isNaN(h.delta)){continue}if(k.hasOwnProperty(m)){BOOMR.addVar(m,h.delta)}else{n.push(m+"|"+h.delta)}i++}if(i){BOOMR.addVar({u:o,r:d});if(j!==d){BOOMR.addVar("r2",j)}if(n.length){BOOMR.addVar("t_other",n.join(","))}}b.timers={};b.complete=true;BOOMR.sendBeacon();return this},is_complete:function(){return b.complete}}}(window));(function(b){var e=b.document;BOOMR=BOOMR||{};BOOMR.plugins=BOOMR.plugins||{};var a=[{name:"image-0.png",size:11483,timeout:1400},{name:"image-1.png",size:40658,timeout:1200},{name:"image-2.png",size:164897,timeout:1300},{name:"image-3.png",size:381756,timeout:1500},{name:"image-4.png",size:1234664,timeout:1200},{name:"image-5.png",size:4509613,timeout:1200},{name:"image-6.png",size:9084559,timeout:1200}];a.end=a.length;a.start=0;a.l={name:"image-l.gif",size:35,timeout:1000};var c={base_url:"images/",timeout:15000,nruns:5,latency_runs:10,user_ip:"",cookie_exp:7*86400,cookie:"BA",results:[],latencies:[],latency:null,runs_left:0,aborted:false,complete:false,running:false,ncmp:function(f,d){return(f-d)},iqr:function(h){var g=h.length-1,f,m,k,d=[],j;f=(h[Math.floor(g*0.25)]+h[Math.ceil(g*0.25)])/2;m=(h[Math.floor(g*0.75)]+h[Math.ceil(g*0.75)])/2;k=(m-f)*1.5;g++;for(j=0;jf-k){d.push(h[j])}}return d},calc_latency:function(){var h,f,j=0,g=0,k,m,d,o,l;l=this.iqr(this.latencies.sort(this.ncmp));f=l.length;BOOMR.debug(l,"bw");for(h=1;h=0&&l<3;x--){if(typeof p[x]==="undefined"){break}if(p[x].t===null){continue}t++;l++;z=a[x].size*1000/p[x].t;g.push(z);s=a[x].size*1000/(p[x].t-this.latency.mean);v.push(s)}}BOOMR.debug("got "+t+" readings","bw");BOOMR.debug("bandwidths: "+g,"bw");BOOMR.debug("corrected: "+v,"bw");if(g.length>3){g=this.iqr(g.sort(this.ncmp));v=this.iqr(v.sort(this.ncmp))}else{g=g.sort(this.ncmp);v=v.sort(this.ncmp)}BOOMR.debug("after iqr: "+g,"bw");BOOMR.debug("corrected: "+v,"bw");t=Math.max(g.length,v.length);for(y=0;y=a.end-1||typeof this.results[this.nruns-h].r[f+1]!=="undefined"){BOOMR.debug(this.results[this.nruns-h],"bw");if(h===this.nruns){a.start=f}this.defer(this.iterate)}else{this.load_img(f+1,h,this.img_loaded)}},finish:function(){if(!this.latency){this.latency=this.calc_latency()}var f=this.calc_bw(),d={bw:f.median_corrected,bw_err:parseFloat(f.stderr_corrected,10),lat:this.latency.mean,lat_err:parseFloat(this.latency.stderr,10),bw_time:Math.round(new Date().getTime()/1000)};BOOMR.addVar(d);if(!isNaN(d.bw)){BOOMR.utils.setCookie(this.cookie,{ba:Math.round(d.bw),be:d.bw_err,l:d.lat,le:d.lat_err,ip:this.user_ip,t:d.bw_time},(this.user_ip?this.cookie_exp:0),"/",null)}this.complete=true;BOOMR.sendBeacon();this.running=false},iterate:function(){if(this.aborted){return false}if(!this.runs_left){this.finish()}else{if(this.latency_runs){this.load_img("l",this.latency_runs--,this.lat_loaded)}else{this.results.push({r:[]});this.load_img(a.start,this.runs_left--,this.img_loaded)}}},setVarsFromCookie:function(l){var i=parseInt(l.ba,10),k=parseFloat(l.be,10),j=parseInt(l.l,10)||0,f=parseFloat(l.le,10)||0,d=l.ip.replace(/\.\d+$/,"0"),m=parseInt(l.t,10),h=this.user_ip.replace(/\.\d+$/,"0"),g=Math.round((new Date().getTime())/1000);if(d===h&&m>=g-this.cookie_exp){this.complete=true;BOOMR.addVar({bw:i,lat:j,bw_err:k,lat_err:f});return true}return false}};BOOMR.plugins.BW={init:function(d){var f;BOOMR.utils.pluginConfig(c,d,"BW",["base_url","timeout","nruns","cookie","cookie_exp"]);if(d&&d.user_ip){c.user_ip=d.user_ip}a.start=0;c.runs_left=c.nruns;c.latency_runs=10;c.results=[];c.latencies=[];c.latency=null;c.complete=false;c.aborted=false;BOOMR.removeVar("ba","ba_err","lat","lat_err");f=BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(c.cookie));if(!f||!f.ba||!c.setVarsFromCookie(f)){BOOMR.subscribe("page_ready",this.run,null,this)}return this},run:function(){if(c.running||c.complete){return this}if(b.location.protocol==="https:"){BOOMR.info("HTTPS detected, skipping bandwidth test","bw");c.complete=true;return this}c.running=true;setTimeout(this.abort,c.timeout);c.defer(c.iterate);return this},abort:function(){c.aborted=true;c.finish();return this},is_complete:function(){return c.complete}}}(window)); diff --git a/boomerang.js b/boomerang.js index c66fc7f86..2188a55a9 100644 --- a/boomerang.js +++ b/boomerang.js @@ -1,1319 +1,2031 @@ -/* - * Copyright (c) 2011, Yahoo! Inc. All rights reserved. +/** + * @copyright (c) 2011, Yahoo! Inc. All rights reserved. + * @copyright (c) 2012, Log-Normal, Inc. All rights reserved. + * @copyright (c) 2012-2016, SOASTA, Inc. All rights reserved. * Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms. */ /** -\file boomerang.js -boomerang measures various performance characteristics of your user's browsing -experience and beacons it back to your server. - -\details -To use this you'll need a web site, lots of users and the ability to do -something with the data you collect. How you collect the data is up to -you, but we have a few ideas. + * @namespace Boomerang + * @desc + * boomerang measures various performance characteristics of your user's browsing + * experience and beacons it back to your server. + * + * To use this you'll need a web site, lots of users and the ability to do + * something with the data you collect. How you collect the data is up to + * you, but we have a few ideas. */ -// Measure the time the script started -// This has to be global so that we don't wait for the entire -// BOOMR function to download and execute before measuring the -// time. We also declare it without `var` so that we can later -// `delete` it. This is the only way that works on Internet Explorer +/** + * @memberof Boomerang + * @type {TimeStamp} + * @desc + * Measure the time the script started + * This has to be global so that we don't wait for the entire + * BOOMR function to download and execute before measuring the + * time. We also declare it without `var` so that we can later + * `delete` it. This is the only way that works on Internet Explorer +*/ BOOMR_start = new Date().getTime(); +/** + * @function + * @desc + * Check the value of document.domain and fix it if incorrect. + * This function is run at the top of boomerang, and then whenever + * init() is called. If boomerang is running within an iframe, this + * function checks to see if it can access elements in the parent + * iframe. If not, it will fudge around with document.domain until + * it finds a value that works. + * + * This allows site owners to change the value of document.domain at + * any point within their page's load process, and we will adapt to + * it. + * @param {string} domain - domain name as retrieved from page url + */ +function BOOMR_check_doc_domain(domain) { + /*eslint no-unused-vars:0*/ + var test; + + if (!window) { + return; + } + + // If domain is not passed in, then this is a global call + // domain is only passed in if we call ourselves, so we + // skip the frame check at that point + if (!domain) { + // If we're running in the main window, then we don't need this + if (window.parent === window || !document.getElementById("boomr-if-as")) { + return;// true; // nothing to do + } + + if (window.BOOMR && BOOMR.boomerang_frame && BOOMR.window) { + try { + // If document.domain is changed during page load (from www.blah.com to blah.com, for example), + // BOOMR.window.location.href throws "Permission Denied" in IE. + // Resetting the inner domain to match the outer makes location accessible once again + if (BOOMR.boomerang_frame.document.domain !== BOOMR.window.document.domain) { + BOOMR.boomerang_frame.document.domain = BOOMR.window.document.domain; + } + } + catch (err) { + if (!BOOMR.isCrossOriginError(err)) { + BOOMR.addError(err, "BOOMR_check_doc_domain.domainFix"); + } + } + } + domain = document.domain; + } + + if (domain.indexOf(".") === -1) { + return;// false; // not okay, but we did our best + } + + // 1. Test without setting document.domain + try { + test = window.parent.document; + return;// test !== undefined; // all okay + } + // 2. Test with document.domain + catch (err) { + document.domain = domain; + } + try { + test = window.parent.document; + return;// test !== undefined; // all okay + } + // 3. Strip off leading part and try again + catch (err) { + domain = domain.replace(/^[\w\-]+\./, ""); + } + + BOOMR_check_doc_domain(domain); +} + +BOOMR_check_doc_domain(); + + // beaconing section // the parameter is the window (function(w) { -var impl, boomr, k, d=w.document; + var impl, boomr, d, myurl, createCustomEvent, dispatchEvent, visibilityState, visibilityChange, orig_w = w; -// Short namespace because I don't want to keep typing BOOMERANG -if(typeof BOOMR === "undefined") { - BOOMR = {}; -} -// don't allow this code to be included twice -if(BOOMR.version) { - return; -} + // This is the only block where we use document without the w. qualifier + if (w.parent !== w && + document.getElementById("boomr-if-as") && + document.getElementById("boomr-if-as").nodeName.toLowerCase() === "script") { + w = w.parent; + myurl = document.getElementById("boomr-if-as").src; + } + + d = w.document; + + // Short namespace because I don't want to keep typing BOOMERANG + if (!w.BOOMR) { w.BOOMR = {}; } + BOOMR = w.BOOMR; + // don't allow this code to be included twice + if (BOOMR.version) { + return; + } -BOOMR.version = "0.9"; - - -// impl is a private object not reachable from outside the BOOMR object -// users can set properties by passing in to the init() method -impl = { - // properties - beacon_url: "", - // strip out everything except last two parts of hostname. - // This doesn't work well for domains that end with a country tld, - // but we allow the developer to override site_domain for that. - site_domain: w.location.hostname. - replace(/.*?([^.]+\.[^.]+)\.?$/, '$1'). - toLowerCase(), - //! User's ip address determined on the server. Used for the BA cookie - user_ip: '', - - events: { - "page_ready": [], - "page_unload": [], - "visibility_changed": [], - "before_beacon": [] - }, - - vars: {}, - - disabled_plugins: {}, - - fireEvent: function(e_name, data) { - var i, h, e; - if(!this.events.hasOwnProperty(e_name)) { - return false; + BOOMR.version = "%boomerang_version%"; + BOOMR.window = w; + BOOMR.boomerang_frame = orig_w; + + if (!BOOMR.plugins) { BOOMR.plugins = {}; } + + // CustomEvent proxy for IE9 & 10 from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent + (function() { + try { + if (new w.CustomEvent("CustomEvent") !== undefined) { + createCustomEvent = function(e_name, params) { + return new w.CustomEvent(e_name, params); + }; + } + } + catch (ignore) { + // empty } - e = this.events[e_name]; + try { + if (!createCustomEvent && d.createEvent && d.createEvent("CustomEvent")) { + createCustomEvent = function(e_name, params) { + var evt = d.createEvent("CustomEvent"); + params = params || { cancelable: false, bubbles: false }; + evt.initCustomEvent(e_name, params.bubbles, params.cancelable, params.detail); - for(i=0; i