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= 0 ) {
- i += name.length;
- cookies = cookies.substring(i, cookies.indexOf(';', i));
- return cookies;
- }
+ /**
+ * Variable priority lists:
+ * -1 = first
+ * 1 = last
+ */
+ varPriority: {
+ "-1": {},
+ "1": {}
+ },
+
+ errors: {},
+
+ disabled_plugins: {},
+
+ xb_handler: function(type) {
+ return function(ev) {
+ var target;
+ if (!ev) { ev = w.event; }
+ if (ev.target) { target = ev.target; }
+ else if (ev.srcElement) { target = ev.srcElement; }
+ if (target.nodeType === 3) {// defeat Safari bug
+ target = target.parentNode;
+ }
- return null;
+ // don't capture events on flash objects
+ // because of context slowdowns in PepperFlash
+ if (target && target.nodeName.toUpperCase() === "OBJECT" && target.type === "application/x-shockwave-flash") {
+ return;
+ }
+ impl.fireEvent(type, target);
+ };
},
- setCookie: function(name, subcookies, max_age, path, domain, sec) {
- var value = "",
- k, nameval, c,
- exp = "";
+ clearEvents: function() {
+ var eventName;
- if(!name) {
- return false;
+ for (eventName in this.events) {
+ if (this.events.hasOwnProperty(eventName)) {
+ this.events[eventName] = [];
+ }
}
+ },
- for(k in subcookies) {
- if(subcookies.hasOwnProperty(k)) {
- value += '&' + encodeURIComponent(k)
- + '=' + encodeURIComponent(subcookies[k]);
+ clearListeners: function() {
+ var type, i;
+
+ for (type in impl.listenerCallbacks) {
+ if (impl.listenerCallbacks.hasOwnProperty(type)) {
+ // remove all callbacks -- removeListener is guaranteed
+ // to remove the element we're calling with
+ while (impl.listenerCallbacks[type].length) {
+ BOOMR.utils.removeListener(
+ impl.listenerCallbacks[type][0].el,
+ type,
+ impl.listenerCallbacks[type][0].fn);
+ }
}
}
- value = value.replace(/^&/, '');
- if(max_age) {
- exp = new Date();
- exp.setTime(exp.getTime() + max_age*1000);
- exp = exp.toGMTString();
- }
+ impl.listenerCallbacks = {};
+ },
+
+ fireEvent: function(e_name, data) {
+ var i, handler, handlers, handlersLen;
- nameval = name + '=' + value;
- c = nameval +
- ((max_age) ? "; expires=" + exp : "" ) +
- ((path) ? "; path=" + path : "") +
- ((typeof domain !== "undefined") ? "; domain="
- + (domain !== null ? domain : impl.site_domain ) : "") +
- ((sec) ? "; secure" : "");
+ e_name = e_name.toLowerCase();
- if ( nameval.length < 4000 ) {
- d.cookie = c;
- // confirm cookie was set (could be blocked by user's settings, etc.)
- return ( value === this.getCookie(name) );
+ if (!this.events.hasOwnProperty(e_name)) {
+ return;// false;
}
- return false;
- },
+ if (this.public_events.hasOwnProperty(e_name)) {
+ dispatchEvent(this.public_events[e_name], data);
+ }
- getSubCookies: function(cookie) {
- var cookies_a,
- i, l, kv,
- cookies={};
+ handlers = this.events[e_name];
- if(!cookie) {
- return null;
+ // Before we fire any event listeners, let's call real_sendBeacon() to flush
+ // any beacon that is being held by the setImmediate.
+ if (e_name !== "before_beacon" && e_name !== "onbeacon") {
+ BOOMR.real_sendBeacon();
}
- cookies_a = cookie.split('&');
-
- if(cookies_a.length === 0) {
- return null;
+ // only call handlers at the time of fireEvent (and not handlers that are
+ // added during this callback to avoid an infinite loop)
+ handlersLen = handlers.length;
+ for (i = 0; i < handlersLen; i++) {
+ try {
+ handler = handlers[i];
+ handler.fn.call(handler.scope, data, handler.cb_data);
+ }
+ catch (err) {
+ BOOMR.addError(err, "fireEvent." + e_name + "<" + i + ">");
+ }
}
- for(i=0, l=cookies_a.length; i 0 && o[k] !== null && typeof o[k] === "object") {
+ value.push(
+ this.objectToString(
+ o[k],
+ separator + (separator === "\n\t" ? "\t" : ""),
+ nest_level - 1
+ )
+ );
+ }
+ else {
+ if (separator === "&") {
+ value.push(encodeURIComponent(o[k]));
+ }
+ else {
+ value.push(o[k]);
+ }
+ }
+ }
+ separator = ",";
+ }
+ else {
+ for (k in o) {
+ if (Object.prototype.hasOwnProperty.call(o, k)) {
+ if (nest_level > 0 && o[k] !== null && typeof o[k] === "object") {
+ value.push(encodeURIComponent(k) + "=" +
+ this.objectToString(
+ o[k],
+ separator + (separator === "\n\t" ? "\t" : ""),
+ nest_level - 1
+ )
+ );
+ }
+ else {
+ if (separator === "&") {
+ value.push(encodeURIComponent(k) + "=" + encodeURIComponent(o[k]));
+ }
+ else {
+ value.push(k + "=" + o[k]);
+ }
+ }
+ }
+ }
+ }
+
+ return value.join(separator);
+ },
+
+ getCookie: function(name) {
+ if (!name) {
+ return null;
+ }
+
+ name = " " + name + "=";
+
+ var i, cookies;
+ cookies = " " + d.cookie + ";";
+ if ((i = cookies.indexOf(name)) >= 0) {
+ i += name.length;
+ cookies = cookies.substring(i, cookies.indexOf(";", i)).replace(/^"/, "").replace(/"$/, "");
+ return cookies;
+ }
+ },
+
+ setCookie: function(name, subcookies, max_age) {
+ var value, nameval, savedval, c, exp;
+
+ if (!name || !impl.site_domain) {
+ BOOMR.debug("No cookie name or site domain: " + name + "/" + impl.site_domain);
+ return false;
+ }
+
+ value = this.objectToString(subcookies, "&");
+ nameval = name + "=\"" + value + "\"";
+
+ c = [nameval, "path=/", "domain=" + impl.site_domain];
+ if (max_age) {
+ exp = new Date();
+ exp.setTime(exp.getTime() + max_age * 1000);
+ exp = exp.toGMTString();
+ c.push("expires=" + exp);
+ }
+
+ if (nameval.length < 500) {
+ d.cookie = c.join("; ");
+ // confirm cookie was set (could be blocked by user's settings, etc.)
+ savedval = this.getCookie(name);
+ if (value === savedval) {
+ return true;
+ }
+ BOOMR.warn("Saved cookie value doesn't match what we tried to set:\n" + value + "\n" + savedval);
+ }
+ else {
+ BOOMR.warn("Cookie too long: " + nameval.length + " " + nameval);
+ }
- if(!config || !config[plugin_name]) {
return false;
- }
+ },
+
+ getSubCookies: function(cookie) {
+ var cookies_a,
+ i, l, kv,
+ gotcookies = false,
+ cookies = {};
- for(i=0; i0);
- }
- },
+ if (typeof cookie !== "string") {
+ BOOMR.debug("TypeError: cookie is not a string: " + typeof cookie);
+ return null;
+ }
- init: function(config) {
- var i, k,
- properties = ["beacon_url", "site_domain", "user_ip"];
+ cookies_a = cookie.split("&");
- if(!config) {
- config = {};
- }
+ for (i = 0, l = cookies_a.length; i < l; i++) {
+ kv = cookies_a[i].split("=");
+ if (kv[0]) {
+ kv.push(""); // just in case there's no value
+ cookies[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
+ gotcookies = true;
+ }
+ }
+
+ return gotcookies ? cookies : null;
+ },
+
+ removeCookie: function(name) {
+ return this.setCookie(name, {}, -86400);
+ },
+
+ /**
+ * Cleans up a URL by removing the query string (if configured), and
+ * limits the URL to the specified size.
+ *
+ * @param {string} url URL to clean
+ * @param {number} urlLimit Maximum size, in characters, of the URL
+ *
+ * @returns {string} Cleaned up URL
+ */
+ cleanupURL: function(url, urlLimit) {
+ if (!url || Object.prototype.toString.call(url) === "[object Array]") {
+ return "";
+ }
+
+ if (impl.strip_query_string) {
+ url = url.replace(/\?.*/, "?qs-redacted");
+ }
+
+ if (typeof urlLimit !== "undefined" && url && url.length > urlLimit) {
+ // We need to break this URL up. Try at the query string first.
+ var qsStart = url.indexOf("?");
+ if (qsStart !== -1 && qsStart < urlLimit) {
+ url = url.substr(0, qsStart) + "?...";
+ }
+ else {
+ // No query string, just stop at the limit
+ url = url.substr(0, urlLimit - 3) + "...";
+ }
+ }
+
+ return url;
+ },
+
+ hashQueryString: function(url, stripHash) {
+ if (!url) {
+ return url;
+ }
+ if (!url.match) {
+ BOOMR.addError("TypeError: Not a string", "hashQueryString", typeof url);
+ return "";
+ }
+ if (url.match(/^\/\//)) {
+ url = location.protocol + url;
+ }
+ if (!url.match(/^(https?|file):/)) {
+ BOOMR.error("Passed in URL is invalid: " + url);
+ return "";
+ }
+ if (stripHash) {
+ url = url.replace(/#.*/, "");
+ }
+ if (!BOOMR.utils.MD5) {
+ return url;
+ }
+ return url.replace(/\?([^#]*)/, function(m0, m1) { return "?" + (m1.length > 10 ? BOOMR.utils.MD5(m1) : m1); });
+ },
+
+ pluginConfig: function(o, config, plugin_name, properties) {
+ var i, props = 0;
+
+ if (!config || !config[plugin_name]) {
+ return false;
+ }
- for(i=0; i 0);
+ },
+ /**
+ * `filter` for arrays
+ *
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} predicate The function invoked per iteration.
+ * @returns {Array} Returns the new filtered array.
+ */
+ arrayFilter: function(array, predicate) {
+ var result = [];
+
+ if (typeof array.filter === "function") {
+ result = array.filter(predicate);
+ }
+ else {
+ var index = -1,
+ length = array.length,
+ value;
+
+ while (++index < length) {
+ value = array[index];
+ if (predicate(value, index, array)) {
+ result[result.length] = value;
+ }
+ }
+ }
+ return result;
+ },
+ /**
+ * @desc
+ * Add a MutationObserver for a given element and terminate after `timeout`ms.
+ * @param el DOM element to watch for mutations
+ * @param config MutationObserverInit object (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationObserverInit)
+ * @param timeout Number of milliseconds of no mutations after which the observer should be automatically disconnected
+ * If set to a falsy value, the observer will wait indefinitely for Mutations.
+ * @param callback Callback function to call either on timeout or if mutations are detected. The signature of this method is:
+ * function(mutations, callback_data)
+ * Where:
+ * mutations is the list of mutations detected by the observer or `undefined` if the observer timed out
+ * callback_data is the passed in `callback_data` parameter without modifications
+ *
+ * The callback function may return a falsy value to disconnect the observer after it returns, or a truthy value to
+ * keep watching for mutations. If the return value is numeric and greater than 0, then this will be the new timeout
+ * if it is boolean instead, then the timeout will not fire any more so the caller MUST call disconnect() at some point
+ * @param callback_data Any data to be passed to the callback function as its second parameter
+ * @param callback_ctx An object that represents the `this` object of the `callback` method. Leave unset the callback function is not a method of an object
+ *
+ * @returns {?object} - `null` if a MutationObserver could not be created OR
+ * - An object containing the observer and the timer object:
+ * { observer: , timer: }
+ *
+ * The caller can use this to disconnect the observer at any point by calling `retval.observer.disconnect()`
+ * Note that the caller should first check to see if `retval.observer` is set before calling `disconnect()` as it may
+ * have been cleared automatically.
+ */
+ addObserver: function(el, config, timeout, callback, callback_data, callback_ctx) {
+ var o = {observer: null, timer: null};
+
+ if (!BOOMR.window || !BOOMR.window.MutationObserver || !callback || !el) {
+ return null;
+ }
+
+ function done(mutations) {
+ var run_again = false;
+
+ if (o.timer) {
+ clearTimeout(o.timer);
+ o.timer = null;
+ }
+
+ if (callback) {
+ run_again = callback.call(callback_ctx, mutations, callback_data);
+
+ if (!run_again) {
+ callback = null;
+ }
+ }
+
+ if (!run_again && o.observer) {
+ o.observer.disconnect();
+ o.observer = null;
+ }
+
+ if (typeof run_again === "number" && run_again > 0) {
+ o.timer = setTimeout(done, run_again);
+ }
+ }
+
+ o.observer = new BOOMR.window.MutationObserver(done);
+
+ if (timeout) {
+ o.timer = setTimeout(done, o.timeout);
+ }
+
+ o.observer.observe(el, config);
+
+ return o;
+ },
+
+ addListener: function(el, type, fn) {
+ if (el.addEventListener) {
+ el.addEventListener(type, fn, false);
+ }
+ else if (el.attachEvent) {
+ el.attachEvent("on" + type, fn);
+ }
+
+ // ensure the type arry exists
+ impl.listenerCallbacks[type] = impl.listenerCallbacks[type] || [];
+
+ // save a reference to the target object and function
+ impl.listenerCallbacks[type].push({ el: el, fn: fn});
+ },
+
+ removeListener: function(el, type, fn) {
+ var i;
+
+ if (el.removeEventListener) {
+ el.removeEventListener(type, fn, false);
+ }
+ else if (el.detachEvent) {
+ el.detachEvent("on" + type, fn);
+ }
+
+ if (impl.listenerCallbacks.hasOwnProperty(type)) {
+ for (var i = 0; i < impl.listenerCallbacks[type].length; i++) {
+ if (fn === impl.listenerCallbacks[type][i].fn &&
+ el === impl.listenerCallbacks[type][i].el) {
+ impl.listenerCallbacks[type].splice(i, 1);
+ return;
+ }
+ }
+ }
+ },
+
+ pushVars: function(form, vars, prefix) {
+ var k, i, l = 0, input;
+
+ for (k in vars) {
+ if (vars.hasOwnProperty(k)) {
+ if (Object.prototype.toString.call(vars[k]) === "[object Array]") {
+ for (i = 0; i < vars[k].length; ++i) {
+ l += BOOMR.utils.pushVars(form, vars[k][i], k + "[" + i + "]");
+ }
+ }
+ else {
+ input = document.createElement("input");
+ input.type = "hidden"; // we need `hidden` to preserve newlines. see commit message for more details
+ input.name = (prefix ? (prefix + "[" + k + "]") : k);
+ input.value = (vars[k] === undefined || vars[k] === null ? "" : vars[k]);
+
+ form.appendChild(input);
+
+ l += encodeURIComponent(input.name).length + encodeURIComponent(input.value).length + 2;
+ }
+ }
+ }
+
+ return l;
+ },
+
+ isArray: function(ary) {
+ return Object.prototype.toString.call(ary) === "[object Array]";
+ },
+
+ inArray: function(val, ary) {
+ var i;
+
+ if (typeof val === "undefined" || typeof ary === "undefined" || !ary.length) {
+ return false;
+ }
+
+ for (i = 0; i < ary.length; i++) {
+ if (ary[i] === val) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Get a query parameter value from a URL's query string
+ *
+ * @param {string} param Query parameter name
+ * @param {string|Object} [url] URL containing the query string, or a link object. Defaults to BOOMR.window.location
+ *
+ * @returns {string|null} URI decoded value or null if param isn't a query parameter
+ */
+ getQueryParamValue: function(param, url) {
+ var l, params, i, kv;
+ if (!param) {
+ return null;
+ }
+
+ if (typeof url === "string") {
+ l = BOOMR.window.document.createElement("a");
+ l.href = url;
+ }
+ else if (typeof url === "object" && typeof url.search === "string") {
+ l = url;
+ }
+ else {
+ l = BOOMR.window.location;
+ }
+
+ // Now that we match, pull out all query string parameters
+ params = l.search.slice(1).split(/&/);
+
+ for (i = 0; i < params.length; i++) {
+ if (params[i]) {
+ kv = params[i].split("=");
+ if (kv.length && kv[0] === param) {
+ return decodeURIComponent(kv[1].replace(/\+/g, " "));
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Generates a pseudo-random UUID (Version 4):
+ * https://en.wikipedia.org/wiki/Universally_unique_identifier
+ *
+ * @returns {string} UUID
+ */
+ generateUUID: function() {
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
+ var r = Math.random() * 16 | 0;
+ var v = c === "x" ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ },
+
+ /**
+ * Generates a random ID based on the specified number of characters. Uses
+ * characters a-z0-9.
+ *
+ * @param {number} chars Number of characters (max 40)
+ * @returns {string} Random ID
+ */
+ generateId: function(chars) {
+ return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".substr(0, chars || 40).replace(/x/g, function(c) {
+ var c = (Math.random() || 0.01).toString(36);
+
+ // some implementations may return "0" for small numbers
+ if (c === "0") {
+ return "0";
+ }
+ else {
+ return c.substr(2, 1);
+ }
+ });
+ },
+
+ /**
+ * Attempt to serialize an object, preferring JSURL over JSON.stringify
+ *
+ * @param {Object} value Object to serialize
+ * @returns {string} serialized version of value, empty-string if not possible
+ */
+ serializeForUrl: function(value) {
+ if (BOOMR.utils.Compression && BOOMR.utils.Compression.jsUrl) {
+ return BOOMR.utils.Compression.jsUrl(value);
+ }
+ if (window.JSON) {
+ return JSON.stringify(value);
+ }
+ // not supported
+ BOOMR.debug("JSON is not supported");
+ return "";
}
- }
- if(typeof config.log !== "undefined") {
- this.log = config.log;
- }
- if(!this.log) {
- this.log = function(m,l,s) { };
- }
+ /* BEGIN_DEBUG */
+ , forEach: function(array, fn, thisArg) {
+ if (!BOOMR.utils.isArray(array) || typeof fn !== "function") {
+ return;
+ }
+ var length = array.length;
+ for (var i = 0; i < length; i++) {
+ if (array.hasOwnProperty(i)) {
+ fn.call(thisArg, array[i], i, array);
+ }
+ }
+ }
+ /* END_DEBUG */
+
+ }, // closes `utils`
+
+ init: function(config) {
+ var i, k,
+ properties = [
+ "beacon_url",
+ "beacon_type",
+ "beacon_auth_key",
+ "beacon_auth_token",
+ "site_domain",
+ "user_ip",
+ "strip_query_string",
+ "secondary_beacons",
+ "autorun",
+ "site_domain"
+ ];
+
+ BOOMR_check_doc_domain();
+
+ if (!config) {
+ config = {};
+ }
- for(k in this.plugins) {
- // config[pugin].enabled has been set to false
- if( config[k]
- && ("enabled" in config[k])
- && config[k].enabled === false
- ) {
- impl.disabled_plugins[k] = 1;
- continue;
+ if (!this.pageId) {
+ // generate a random page ID for this page's lifetime
+ this.pageId = BOOMR.utils.generateId(8);
}
- else if(impl.disabled_plugins[k]) {
- delete impl.disabled_plugins[k];
+
+ if (config.primary && impl.handlers_attached) {
+ return this;
}
- // plugin exists and has an init method
- if(this.plugins.hasOwnProperty(k)
- && typeof this.plugins[k].init === "function"
- ) {
- this.plugins[k].init(config);
+ if (config.log !== undefined) {
+ this.log = config.log;
+ }
+ if (!this.log) {
+ this.log = function(/* m,l,s */) {};
}
- }
- // The developer can override onload by setting autorun to false
- if(!("autorun" in config) || config.autorun !== false) {
- impl.addListener(w, "load",
- function() {
- impl.fireEvent("page_ready");
+ // Set autorun if in config right now, as plugins that listen for page_ready
+ // event may fire when they .init() if onload has already fired, and whether
+ // or not we should fire page_ready depends on config.autorun.
+ if (typeof config.autorun !== "undefined") {
+ impl.autorun = config.autorun;
+ }
+
+ for (k in this.plugins) {
+ if (this.plugins.hasOwnProperty(k)) {
+ // config[plugin].enabled has been set to false
+ if (config[k] &&
+ config[k].hasOwnProperty("enabled") &&
+ config[k].enabled === false) {
+ impl.disabled_plugins[k] = 1;
+
+ if (typeof this.plugins[k].disable === "function") {
+ this.plugins[k].disable();
}
- );
- }
- // visibilitychange is useful to detect if the page loaded through prerender
- // or if the page never became visible
- // http://www.w3.org/TR/2011/WD-page-visibility-20110602/
- // http://www.nczonline.net/blog/2011/08/09/introduction-to-the-page-visibility-api/
- var fire_visible = function() { impl.fireEvent("visibility_changed"); }
- if(d.webkitVisibilityState)
- impl.addListener(d, "webkitvisibilitychange", fire_visible);
- else if(d.msVisibilityState)
- impl.addListener(d, "msvisibilitychange", fire_visible);
- else if(d.visibilityState)
- impl.addListener(d, "visibilitychange", fire_visible);
-
- // This must be the last one to fire
- impl.addListener(w, "unload", function() { w=null; });
-
- return this;
- },
-
- // The page dev calls this method when they determine the page is usable.
- // Only call this if autorun is explicitly set to false
- page_ready: function() {
- impl.fireEvent("page_ready");
- return this;
- },
-
- subscribe: function(e_name, fn, cb_data, cb_scope) {
- var i, h, e;
-
- if(!impl.events.hasOwnProperty(e_name)) {
- return this;
- }
+ continue;
+ }
- e = impl.events[e_name];
+ // plugin was previously disabled
+ if (impl.disabled_plugins[k]) {
- // don't allow a handler to be attached more than once to the same event
- for(i=0; i -1)?'&':'?') +
- 'v=' + encodeURIComponent(BOOMR.version) +
- '&u=' + encodeURIComponent(d.URL.replace(/#.*/, ''));
- // use d.URL instead of location.href because of a safari bug
-
- for(k in impl.vars) {
- if(impl.vars.hasOwnProperty(k)) {
- nparams++;
- url += "&" + encodeURIComponent(k)
- + "="
- + (
- impl.vars[k]===undefined || impl.vars[k]===null
- ? ''
- : encodeURIComponent(impl.vars[k])
- );
+ /**
+ * Defer the function `fn` until the next instant the browser is free from user tasks
+ * @param [Function] fn The callback function. This function accepts the following arguments:
+ * - data: The passed in data object
+ * - cb_data: The passed in cb_data object
+ * - call stack: An Error object that holds the callstack for when setImmediate was called, used to determine what called the callback
+ * @param [object] data Any data to pass to the callback function
+ * @param [object] cb_data Any passthrough data for the callback function. This differs from `data` when setImmediate is called via an event handler and `data` is the Event object
+ * @param [object] cb_scope The scope of the callback function if it is a method of an object
+ * @returns nothing
+ */
+ setImmediate: function(fn, data, cb_data, cb_scope) {
+ var cb, cstack;
+
+ // DEBUG: This is to help debugging, we'll see where setImmediate calls were made from
+ if (typeof Error !== "undefined") {
+ cstack = new Error();
+ cstack = cstack.stack ? cstack.stack.replace(/^Error/, "Called") : undefined;
}
- }
+ // END-DEBUG
- // only send beacon if we actually have something to beacon back
- if(nparams) {
- img = new Image();
- img.src=url;
- }
+ cb = function() {
+ fn.call(cb_scope || null, data, cb_data || {}, cstack);
+ cb = null;
+ };
- return this;
- }
+ if (w.requestIdleCallback) {
+ w.requestIdleCallback(cb);
+ }
+ else if (w.setImmediate) {
+ w.setImmediate(cb);
+ }
+ else {
+ setTimeout(cb, 10);
+ }
+ },
-};
+ /**
+ * Gets the current time in milliseconds since the Unix Epoch (Jan 1 1970).
+ *
+ * In browsers that support DOMHighResTimeStamp, this will be replaced
+ * by a function that adds BOOMR.now() to navigationStart
+ * (with milliseconds.microseconds resolution).
+ *
+ * @returns {Number} Milliseconds since Unix Epoch
+ */
+ now: (function() {
+ return Date.now || function() { return new Date().getTime(); };
+ }()),
+
+ getPerformance: function() {
+ try {
+ if (BOOMR.window) {
+ if ("performance" in BOOMR.window && BOOMR.window.performance) {
+ return BOOMR.window.performance;
+ }
-delete BOOMR_start;
+ // vendor-prefixed fallbacks
+ return BOOMR.window.msPerformance || BOOMR.window.webkitPerformance || BOOMR.window.mozPerformance;
+ }
+ }
+ catch (ignore) {
+ // empty
+ }
+ },
-var make_logger = function(l) {
- return function(m, s) {
- this.log(m, l, "boomerang" + (s?"."+s:"")); return this;
- };
-};
+ visibilityState: (visibilityState === undefined ? function() { return "visible"; } : function() { return d[visibilityState]; }),
-boomr.debug = make_logger("debug");
-boomr.info = make_logger("info");
-boomr.warn = make_logger("warn");
-boomr.error = make_logger("error");
+ lastVisibilityEvent: {},
-if(w.YAHOO && w.YAHOO.widget && w.YAHOO.widget.Logger) {
- boomr.log = w.YAHOO.log;
-}
-else if(typeof w.Y !== "undefined" && typeof w.Y.log !== "undefined") {
- boomr.log = w.Y.log;
-}
-else if(typeof console !== "undefined" && typeof console.log !== "undefined") {
- boomr.log = function(m,l,s) { console.log(s + ": [" + l + "] ", m); };
-}
+ /**
+ * Registers an event
+ *
+ * @param {string} e_name Event name
+ *
+ * @returns {BOOMR} Boomerang object
+ */
+ registerEvent: function(e_name) {
+ if (impl.events.hasOwnProperty(e_name)) {
+ // already registered
+ return this;
+ }
+ // create a new queue of handlers
+ impl.events[e_name] = [];
-for(k in boomr) {
- if(boomr.hasOwnProperty(k)) {
- BOOMR[k] = boomr[k];
- }
-}
+ return this;
+ },
-BOOMR.plugins = BOOMR.plugins || {};
+ /**
+ * Disables boomerang from doing anything further:
+ * 1. Clears event handlers (such as onload)
+ * 2. Clears all event listeners
+ */
+ disable: function() {
+ impl.clearEvents();
+ impl.clearListeners();
+ },
-}(window));
+ /**
+ * Fires an event
+ *
+ * @param {string} e_name Event name
+ * @param {object} data Event payload
+ *
+ * @returns {BOOMR} Boomerang object
+ */
+ fireEvent: function(e_name, data) {
+ return impl.fireEvent(e_name, data);
+ },
-// end of boomerang beaconing section
-// Now we start built in plugins.
+ subscribe: function(e_name, fn, cb_data, cb_scope, once) {
+ var i, handler, ev;
+ e_name = e_name.toLowerCase();
-// This is the Round Trip Time plugin. Abbreviated to RT
-// the parameter is the window
-(function(w) {
+ if (!impl.events.hasOwnProperty(e_name)) {
+ // allow subscriptions before they're registered
+ impl.events[e_name] = [];
+ }
-var d=w.document;
+ ev = impl.events[e_name];
-BOOMR = BOOMR || {};
-BOOMR.plugins = BOOMR.plugins || {};
+ // don't allow a handler to be attached more than once to the same event
+ for (i = 0; i < ev.length; i++) {
+ handler = ev[i];
+ if (handler && handler.fn === fn && handler.cb_data === cb_data && handler.scope === cb_scope) {
+ return this;
+ }
+ }
-// private object
-var impl = {
- complete: false, //! Set when this plugin has completed
+ ev.push({
+ fn: fn,
+ cb_data: cb_data || {},
+ scope: cb_scope || null,
+ once: once || false
+ });
- timers: {}, //! Custom timers that the developer can use
- // Format for each timer is { start: XXX, end: YYY, delta: YYY-XXX }
- cookie: 'RT', //! Name of the cookie that stores the start time and referrer
- cookie_exp:600, //! Cookie expiry in seconds
- strict_referrer: true, //! By default, don't beacon if referrers don't match.
- // If set to false, beacon both referrer values and let
- // the back end decide
+ // attaching to page_ready after onload fires, so call soon
+ if (e_name === "page_ready" && impl.onloadfired && impl.autorun) {
+ this.setImmediate(fn, null, cb_data, cb_scope);
+ }
- navigationType: 0,
- navigationStart: undefined,
- responseStart: undefined,
+ // Attach unload handlers directly to the window.onunload and
+ // window.onbeforeunload events. The first of the two to fire will clear
+ // fn so that the second doesn't fire. We do this because technically
+ // onbeforeunload is the right event to fire, but all browsers don't
+ // support it. This allows us to fall back to onunload when onbeforeunload
+ // isn't implemented
+ if (e_name === "page_unload" || e_name === "before_unload") {
+ (function() {
+ var unload_handler, evt_idx = ev.length;
+
+ unload_handler = function(evt) {
+ if (fn) {
+ fn.call(cb_scope, evt || w.event, cb_data);
+ }
- // The start method is fired on page unload. It is called with the scope
- // of the BOOMR.plugins.RT object
- start: function() {
- var t_end, t_start = new Date().getTime();
+ // If this was the last unload handler, we'll try to send the beacon immediately after it is done
+ // The beacon will only be sent if one of the handlers has queued it
+ if (e_name === "page_unload" && evt_idx === impl.events[e_name].length) {
+ BOOMR.real_sendBeacon();
+ }
+ };
- // Disable use of RT cookie by setting its name to a falsy value
- if(!this.cookie) {
- return this;
- }
+ if (e_name === "page_unload") {
+ // pagehide is for iOS devices
+ // see http://www.webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
+ if (w.onpagehide || w.onpagehide === null) {
+ BOOMR.utils.addListener(w, "pagehide", unload_handler);
+ }
+ else {
+ BOOMR.utils.addListener(w, "unload", unload_handler);
+ }
+ }
+ BOOMR.utils.addListener(w, "beforeunload", unload_handler);
+ }());
+ }
- // We use document.URL instead of location.href because of a bug in safari 4
- // where location.href is URL decoded
- if(!BOOMR.utils.setCookie(this.cookie,
- { s: t_start, r: d.URL.replace(/#.*/, '') },
- this.cookie_exp,
- "/", null)
- ) {
- BOOMR.error("cannot set start cookie", "rt");
return this;
- }
-
- t_end = new Date().getTime();
- if(t_end - t_start > 50) {
- // It took > 50ms to set the cookie
- // The user Most likely has cookie prompting turned on so
- // t_start won't be the actual unload time
- // We bail at this point since we can't reliably tell t_done
- BOOMR.utils.removeCookie(this.cookie);
-
- // at some point we may want to log this info on the server side
- BOOMR.error("took more than 50ms to set cookie... aborting: "
- + t_start + " -> " + t_end, "rt");
- }
+ },
- return this;
- },
+ addError: function BOOMR_addError(err, src, extra) {
+ var str, E = BOOMR.plugins.Errors;
+
+ //
+ // Use the Errors plugin if it's enabled
+ //
+ if (E && E.is_supported()) {
+ if (typeof err === "string") {
+ E.send({
+ message: err,
+ extra: extra,
+ functionName: src,
+ noStack: true
+ }, E.VIA_APP, E.SOURCE_BOOMERANG);
+ }
+ else {
+ if (typeof src === "string") {
+ err.functionName = src;
+ }
- initNavTiming: function() {
- var ti, p, source;
+ if (typeof extra !== "undefined") {
+ err.extra = extra;
+ }
- if(this.navigationStart) {
- return;
- }
+ E.send(err, E.VIA_APP, E.SOURCE_BOOMERANG);
+ }
- // Get start time from WebTiming API see:
- // https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/NavigationTiming/Overview.html
- // http://blogs.msdn.com/b/ie/archive/2010/06/28/measuring-web-page-performance.aspx
- // http://blog.chromium.org/2010/07/do-you-know-how-slow-your-web-page-is.html
- p = w.performance || w.msPerformance || w.webkitPerformance || w.mozPerformance;
+ return;
+ }
- if(p && p.navigation) {
- this.navigationType = p.navigation.type;
- }
+ if (typeof err !== "string") {
+ str = String(err);
+ if (str.match(/^\[object/)) {
+ str = err.name + ": " + (err.description || err.message).replace(/\r\n$/, "");
+ }
+ err = str;
+ }
+ if (src !== undefined) {
+ err = "[" + src + ":" + BOOMR.now() + "] " + err;
+ }
+ if (extra) {
+ err += ":: " + extra;
+ }
- if(p && p.timing) {
- ti = p.timing;
- }
- else if(w.chrome && w.chrome.csi && w.chrome.csi().startE) {
- // Older versions of chrome also have a timing API that's sort of documented here:
- // http://ecmanaut.blogspot.com/2010/06/google-bom-feature-ms-since-pageload.html
- // source here:
- // http://src.chromium.org/viewvc/chrome/trunk/src/chrome/renderer/loadtimes_extension_bindings.cc?view=markup
- ti = {
- navigationStart: w.chrome.csi().startE
- };
- source = "csi";
- }
- else if(w.gtbExternal && w.gtbExternal.startE()) {
- // The Google Toolbar exposes navigation start time similar to old versions of chrome
- // This would work for any browser that has the google toolbar installed
- ti = {
- navigationStart: w.gtbExternal.startE()
- };
- source = 'gtb';
- }
+ if (impl.errors[err]) {
+ impl.errors[err]++;
+ }
+ else {
+ impl.errors[err] = 1;
+ }
+ },
- if(ti) {
- // Always use navigationStart since it falls back to fetchStart
- // If not set, we leave t_start alone so that timers that depend
- // on it don't get sent back. Never use requestStart since if
- // the first request fails and the browser retries, it will contain
- // the value for the new request.
- BOOMR.addVar("rt.start", source || "navigation");
- this.navigationStart = ti.navigationStart || undefined;
- this.responseStart = ti.responseStart || undefined;
+ isCrossOriginError: function(err) {
+ // These are expected for cross-origin iframe access, although the Internet Explorer check will only
+ // work for browsers using English.
+ return err.name === "SecurityError" ||
+ (err.name === "TypeError" && err.message === "Permission denied") ||
+ (err.name === "Error" && err.message && err.message.match(/^(Permission|Access is) denied/));
+ },
- // bug in Firefox 7 & 8 https://bugzilla.mozilla.org/show_bug.cgi?id=691547
- if(navigator.userAgent.match(/Firefox\/[78]\./)) {
- this.navigationStart = ti.unloadEventStart || ti.fetchStart || undefined;
+ addVar: function(name, value) {
+ if (typeof name === "string") {
+ impl.vars[name] = value;
}
- }
- else {
- BOOMR.warn("This browser doesn't support the WebTiming API", "rt");
- }
+ else if (typeof name === "object") {
+ var o = name, k;
+ for (k in o) {
+ if (o.hasOwnProperty(k)) {
+ impl.vars[k] = o[k];
+ }
+ }
+ }
+ return this;
+ },
- return;
- }
-};
+ removeVar: function(arg0) {
+ var i, params;
+ if (!arguments.length) {
+ return this;
+ }
-BOOMR.plugins.RT = {
- // Methods
+ if (arguments.length === 1 &&
+ Object.prototype.toString.apply(arg0) === "[object Array]") {
+ params = arg0;
+ }
+ else {
+ params = arguments;
+ }
- init: function(config) {
- impl.complete = false;
- impl.timers = {};
+ for (i = 0; i < params.length; i++) {
+ if (impl.vars.hasOwnProperty(params[i])) {
+ delete impl.vars[params[i]];
+ }
+ }
- BOOMR.utils.pluginConfig(impl, config, "RT",
- ["cookie", "cookie_exp", "strict_referrer"]);
+ return this;
+ },
- BOOMR.subscribe("page_ready", this.done, null, this);
- BOOMR.subscribe("page_unload", impl.start, null, impl);
+ hasVar: function(name) {
+ return impl.vars.hasOwnProperty(name);
+ },
- if(BOOMR.t_start) {
- // How long does it take Boomerang to load up and execute
- this.startTimer('boomerang', BOOMR.t_start);
- this.endTimer('boomerang', BOOMR.t_end); // t_end === null defaults to current time
+ /**
+ * Sets a variable's priority in the beacon URL.
+ * -1 = beginning of the URL
+ * 0 = middle of the URL (default)
+ * 1 = end of the URL
+ *
+ * @param {string} name Variable name
+ * @param {number} pri Priority (-1 or 1)
+ */
+ setVarPriority: function(name, pri) {
+ if (typeof pri !== "number" || Math.abs(pri) !== 1) {
+ return this;
+ }
- // How long did it take till Boomerang started
- this.endTimer('boomr_fb', BOOMR.t_start);
- }
+ impl.varPriority[pri][name] = 1;
- return this;
- },
+ return this;
+ },
- startTimer: function(timer_name, time_value) {
- if(timer_name) {
- if (timer_name === 't_page') {
- this.endTimer('t_resp', time_value);
+ /**
+ * Sets the Referrers
+ * @param {string} r Referrer from the cookie
+ * @param {string} [r2] Referrer from document.referrer, if different
+ */
+ setReferrer: function(r, r2) {
+ // cookie referrer
+ impl.r = r;
+
+ // document.referrer, if different
+ if (r2 && r !== r2) {
+ impl.r2 = r2;
}
- impl.timers[timer_name] = {start: (typeof time_value === "number" ? time_value : new Date().getTime())};
- impl.complete = false;
- }
-
- return this;
- },
-
- endTimer: function(timer_name, time_value) {
- if(timer_name) {
- impl.timers[timer_name] = impl.timers[timer_name] || {};
- if(!("end" in impl.timers[timer_name])) {
- impl.timers[timer_name].end =
- (typeof time_value === "number" ? time_value : new Date().getTime());
+ else {
+ impl.r2 = undefined;
}
- }
+ },
- return this;
- },
+ requestStart: function(name) {
+ var t_start = BOOMR.now();
+ BOOMR.plugins.RT.startTimer("xhr_" + name, t_start);
- setTimer: function(timer_name, time_delta) {
- if(timer_name) {
- impl.timers[timer_name] = { delta: time_delta };
- }
+ return {
+ loaded: function(data) {
+ BOOMR.responseEnd(name, t_start, data);
+ }
+ };
+ },
- return this;
- },
+ /**
+ * Determines is Boomerang can send a beacon.
+ *
+ * Queryies all plugins to see if they implement readyToSend(),
+ * and if so, that they return true;
+ *
+ * If not, the beacon cannot be sent.
+ *
+ * @returns {boolean} True if Boomerang can send a beacon
+ */
+ readyToSend: function() {
+ var plugin;
+
+ for (plugin in this.plugins) {
+ if (this.plugins.hasOwnProperty(plugin)) {
+ if (impl.disabled_plugins[plugin]) {
+ continue;
+ }
- // Called when the page has reached a "usable" state. This may be when the
- // onload event fires, or it could be at some other moment during/after page
- // load when the page is usable by the user
- done: function() {
- var t_start, r, r2,
- subcookies, basic_timers = { t_done: 1, t_resp: 1, t_page: 1},
- ntimers = 0, t_name, timer, t_other=[];
+ if (typeof this.plugins[plugin].readyToSend === "function" &&
+ this.plugins[plugin].readyToSend() === false) {
+ BOOMR.debug("Plugin " + plugin + " is not ready to send");
+ return false;
+ }
+ }
+ }
- if(impl.complete) {
- return this;
- }
+ return true;
+ },
- impl.initNavTiming();
+ responseEnd: function(name, t_start, data, t_end) {
+ // take the now timestamp for start and end, if unspecified, in case we delay this beacon
+ t_start = typeof t_start === "number" ? t_start : BOOMR.now();
+ t_end = typeof t_end === "number" ? t_end : BOOMR.now();
- if(
- (d.webkitVisibilityState && d.webkitVisibilityState === "prerender")
- ||
- (d.msVisibilityState && d.msVisibilityState === 3)
- ) {
- // This means that onload fired through a pre-render. We'll capture this
- // time, but wait for t_done until after the page has become either visible
- // or hidden (ie, it moved out of the pre-render state)
- // http://code.google.com/chrome/whitepapers/pagevisibility.html
- // http://www.w3.org/TR/2011/WD-page-visibility-20110602/
- // http://code.google.com/chrome/whitepapers/prerender.html
+ // wait until all plugins are ready to send
+ if (!BOOMR.readyToSend()) {
+ BOOMR.debug("Attempted to call responseEnd before all plugins were Ready to Send, trying again...");
- this.startTimer("t_load", impl.navigationStart);
- this.endTimer("t_load"); // this will measure actual onload time for a prerendered page
- this.startTimer("t_prerender", impl.navigationStart);
- this.startTimer("t_postrender"); // time from prerender to visible or hidden
+ // try again later
+ setTimeout(function() {
+ BOOMR.responseEnd(name, t_start, data, t_end);
+ }, 1000);
- BOOMR.subscribe("visibility_changed", this.done, null, this);
+ return;
+ }
- return this;
- }
+ // Wait until we've sent the Page Load beacon first
+ if (!BOOMR.hasSentPageLoadBeacon() &&
+ !BOOMR.utils.inArray(name.initiator, BOOMR.constants.BEACON_TYPE_SPAS)) {
+ // wait for a beacon, then try again
+ BOOMR.subscribe("page_load_beacon", function() {
+ BOOMR.responseEnd(name, t_start, data, t_end);
+ }, null, BOOMR, true);
- // If the dev has already called endTimer, then this call will do nothing
- // else, it will stop the page load timer
- this.endTimer("t_done");
+ return;
+ }
+
+ if (typeof name === "object") {
+ if (!name.url) {
+ BOOMR.debug("BOOMR.responseEnd: First argument must have a url property if it's an object");
+ return;
+ }
- if(impl.responseStart) {
- // Use NavTiming API to figure out resp latency and page time
- // t_resp will use the cookie if available or fallback to NavTiming
- this.endTimer("t_resp", impl.responseStart);
- if(impl.timers.t_load) {
- this.setTimer("t_page", impl.timers.t_load.end - impl.responseStart);
+ impl.fireEvent("xhr_load", name);
}
else {
- this.setTimer("t_page", new Date().getTime() - impl.responseStart);
+ // flush out any queue'd beacons before we set the Page Group
+ // and timers
+ BOOMR.real_sendBeacon();
+
+ BOOMR.addVar("xhr.pg", name);
+ BOOMR.plugins.RT.startTimer("xhr_" + name, t_start);
+ impl.fireEvent("xhr_load", {
+ name: "xhr_" + name,
+ data: data,
+ timing: {
+ loadEventEnd: t_end
+ }
+ });
}
- }
- else if(impl.timers.hasOwnProperty('t_page')) {
- // If the dev has already started t_page timer, we can end it now as well
- this.endTimer("t_page");
- }
+ },
- // If a prerender timer was started, we can end it now as well
- if(impl.timers.hasOwnProperty('t_postrender')) {
- this.endTimer("t_postrender");
- this.endTimer("t_prerender");
- }
+ //
+ // uninstrumentXHR and instrumentXHR are stubs that will be replaced
+ // by auto-xhr.js if active.
+ //
+ /**
+ * Undo XMLHttpRequest instrumentation and reset the original
+ */
+ uninstrumentXHR: function() {
+ },
+ /**
+ * Instrument all requests made via XMLHttpRequest to send beacons
+ * This is implemented in plugins/auto-xhr.js
+ */
+ instrumentXHR: function() { },
+
+ sendBeacon: function(beacon_url_override) {
+ // This plugin wants the beacon to go somewhere else,
+ // so update the location
+ if (beacon_url_override) {
+ impl.beacon_url_override = beacon_url_override;
+ }
- // A beacon may be fired automatically on page load or if the page dev fires
- // it manually with their own timers. It may not always contain a referrer
- // (eg: XHR calls). We set default values for these cases
+ if (!impl.beaconQueued) {
+ impl.beaconQueued = true;
+ BOOMR.setImmediate(BOOMR.real_sendBeacon, null, null, BOOMR);
+ }
- r = r2 = d.referrer.replace(/#.*/, '');
+ return true;
+ },
- // If impl.cookie is not set, the dev does not want to use cookie time
- if(impl.cookie) {
- subcookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(impl.cookie));
- BOOMR.utils.removeCookie(impl.cookie);
+ real_sendBeacon: function() {
+ var k, form, url, img, errors = [], params = [], paramsJoined, useImg = 1,
+ varsSent = {}, varsToSend = {}, urlFirst = [], urlLast = [],
+ xhr;
- if(subcookies && subcookies.s && subcookies.r) {
- r = subcookies.r;
- if(!impl.strict_referrer || r === r2) {
- t_start = parseInt(subcookies.s, 10);
- }
+ if (!impl.beaconQueued) {
+ return false;
}
- }
- if(t_start && impl.navigationType != 2) { // 2 is TYPE_BACK_FORWARD but the constant may not be defined across browsers
- BOOMR.addVar("rt.start", "cookie"); // if the user hit the back button, referrer will match, and cookie will match
- } // but will have time of previous page start, so t_done will be wrong
- else {
- t_start = impl.navigationStart;
- }
+ impl.beaconQueued = false;
- // make sure old variables don't stick around
- BOOMR.removeVar('t_done', 't_page', 't_resp', 'r', 'r2', 'rt.bstart', 'rt.end');
+ BOOMR.debug("Checking if we can send beacon");
- BOOMR.addVar('rt.bstart', BOOMR.t_start);
- BOOMR.addVar('rt.end', impl.timers.t_done.end);
+ // At this point someone is ready to send the beacon. We send
+ // the beacon only if all plugins have finished doing what they
+ // wanted to do
+ for (k in this.plugins) {
+ if (this.plugins.hasOwnProperty(k)) {
+ if (impl.disabled_plugins[k]) {
+ continue;
+ }
+ if (!this.plugins[k].is_complete()) {
+ BOOMR.debug("Plugin " + k + " is not complete, deferring beacon send");
+ return false;
+ }
+ }
+ }
- for(t_name in impl.timers) {
- if(!impl.timers.hasOwnProperty(t_name)) {
- continue;
+ // Sanity test that the browser is still available (and not shutting down)
+ if (!window || !window.Image || !window.navigator || !BOOMR.window) {
+ BOOMR.debug("DOM not fully available, not sending a beacon");
+ return false;
}
- timer = impl.timers[t_name];
+ // For SPA apps, don't strip hashtags as some SPA frameworks use #s for tracking routes
+ // instead of History pushState() APIs. Use d.URL instead of location.href because of a
+ // Safari bug.
+ var isSPA = BOOMR.utils.inArray(impl.vars["http.initiator"], BOOMR.constants.BEACON_TYPE_SPAS);
+ var isPageLoad = typeof impl.vars["http.initiator"] === "undefined" || isSPA;
- // if delta is a number, then it was set using setTimer
- // if not, then we have to calculate it using start & end
- if(typeof timer.delta !== "number") {
- if(typeof timer.start !== "number") {
- timer.start = t_start;
- }
- timer.delta = timer.end - timer.start;
+ var pgu = isSPA ? d.URL : d.URL.replace(/#.*/, "");
+ impl.vars.pgu = BOOMR.utils.cleanupURL(pgu);
+
+ // Use the current document.URL if it hasn't already been set, or for SPA apps,
+ // on each new beacon (since each SPA soft navigation might change the URL)
+ if (!impl.vars.u || isSPA) {
+ impl.vars.u = impl.vars.pgu;
}
- // If the caller did not set a start time, and if there was no start cookie
- // then timer.delta will be NaN, in which case we discard it.
- if(isNaN(timer.delta)) {
- continue;
+ if (impl.vars.pgu === impl.vars.u) {
+ delete impl.vars.pgu;
}
- if(basic_timers.hasOwnProperty(t_name)) {
- BOOMR.addVar(t_name, timer.delta);
+ // Add cleaned-up referrer URLs to the beacon, if available
+ if (impl.r) {
+ impl.vars.r = BOOMR.utils.cleanupURL(impl.r);
}
else {
- t_other.push(t_name + '|' + timer.delta);
+ delete impl.vars.r;
}
- ntimers++;
- }
- if(ntimers) {
- BOOMR.addVar("r", r);
-
- if(r2 !== r) {
- BOOMR.addVar("r2", r2);
+ if (impl.r2) {
+ impl.vars.r2 = BOOMR.utils.cleanupURL(impl.r2);
}
-
- if(t_other.length) {
- BOOMR.addVar("t_other", t_other.join(','));
+ else {
+ delete impl.vars.r2;
}
- }
-
- impl.timers = {};
- impl.complete = true;
-
- BOOMR.sendBeacon(); // we call sendBeacon() anyway because some other plugin
- // may have blocked waiting for RT to complete
- return this;
- },
- is_complete: function() { return impl.complete; }
+ impl.vars.v = BOOMR.version;
-};
-
-}(window));
-// End of RT plugin
+ if (BOOMR.visibilityState()) {
+ impl.vars["vis.st"] = BOOMR.visibilityState();
+ if (BOOMR.lastVisibilityEvent.visible) {
+ impl.vars["vis.lv"] = BOOMR.now() - BOOMR.lastVisibilityEvent.visible;
+ }
+ if (BOOMR.lastVisibilityEvent.hidden) {
+ impl.vars["vis.lh"] = BOOMR.now() - BOOMR.lastVisibilityEvent.hidden;
+ }
+ }
-// This is the Bandwidth & Latency plugin abbreviated to BW
-// the parameter is the window
-(function(w) {
+ impl.vars["ua.plt"] = navigator.platform;
+ impl.vars["ua.vnd"] = navigator.vendor;
-var d=w.document;
-
-BOOMR = BOOMR || {};
-BOOMR.plugins = BOOMR.plugins || {};
-
-// We choose image sizes so that we can narrow down on a bandwidth range as
-// soon as possible the sizes chosen correspond to bandwidth values of
-// 14-64kbps, 64-256kbps, 256-1024kbps, 1-2Mbps, 2-8Mbps, 8-30Mbps & 30Mbps+
-// Anything below 14kbps will probably timeout before the test completes
-// Anything over 60Mbps will probably be unreliable since latency will make up
-// the largest part of download time. If you want to extend this further to
-// cover 100Mbps & 1Gbps networks, use image sizes of 19,200,000 & 153,600,000
-// bytes respectively
-// See https://spreadsheets.google.com/ccc?key=0AplxPyCzmQi6dDRBN2JEd190N1hhV1N5cHQtUVdBMUE&hl=en_GB
-// for a spreadsheet with the details
-var images=[
- { 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 }
-];
-
-images.end = images.length;
-images.start = 0;
-
-// abuse arrays to do the latency test simply because it avoids a bunch of
-// branches in the rest of the code.
-// I'm sorry Douglas
-images.l = { name: "image-l.gif", size: 35, timeout: 1000 };
-
-// private object
-var impl = {
- // properties
- base_url: 'images/',
- timeout: 15000,
- nruns: 5,
- latency_runs: 10,
- user_ip: '',
- cookie_exp: 7*86400,
- cookie: 'BA',
-
- // state
- results: [],
- latencies: [],
- latency: null,
- runs_left: 0,
- aborted: false,
- complete: false,
- running: false,
-
- // methods
-
- // numeric comparator. Returns negative number if a < b, positive if a > b and 0 if they're equal
- // used to sort an array numerically
- ncmp: function(a, b) { return (a-b); },
-
- // Calculate the interquartile range of an array of data points
- iqr: function(a)
- {
- var l = a.length-1, q1, q3, fw, b = [], i;
-
- q1 = (a[Math.floor(l*0.25)] + a[Math.ceil(l*0.25)])/2;
- q3 = (a[Math.floor(l*0.75)] + a[Math.ceil(l*0.75)])/2;
-
- fw = (q3-q1)*1.5;
-
- l++;
-
- for(i=0; i q1-fw) {
- b.push(a[i]);
+ if (this.pageId) {
+ impl.vars.pid = this.pageId;
}
- }
- return b;
- },
-
- calc_latency: function()
- {
- var i, n,
- sum=0, sumsq=0,
- amean, median,
- std_dev, std_err,
- lat_filtered;
-
- // We first do IQR filtering and use the resulting data set
- // for all calculations
- lat_filtered = this.iqr(this.latencies.sort(this.ncmp));
- n = lat_filtered.length;
-
- BOOMR.debug(lat_filtered, "bw");
-
- // First we get the arithmetic mean, standard deviation and standard error
- // We ignore the first since it paid the price of DNS lookup, TCP connect
- // and slow start
- for(i=1; i 1 ? " (*" + impl.errors[k] + ")" : ""));
+ }
+ }
- amean = Math.round(sum / n);
+ if (errors.length > 0) {
+ impl.vars.errors = errors.join("\n");
+ }
- std_dev = Math.sqrt( sumsq/n - sum*sum/(n*n));
+ impl.errors = {};
- // See http://en.wikipedia.org/wiki/1.96 and http://en.wikipedia.org/wiki/Standard_error_%28statistics%29
- std_err = (1.96 * std_dev/Math.sqrt(n)).toFixed(2);
+ // If we reach here, all plugins have completed
+ impl.fireEvent("before_beacon", impl.vars);
- std_dev = std_dev.toFixed(2);
+ // Use the override URL if given
+ impl.beacon_url = impl.beacon_url_override || impl.beacon_url;
+ // Don't send a beacon if no beacon_url has been set
+ // you would do this if you want to do some fancy beacon handling
+ // in the `before_beacon` event instead of a simple GET request
+ BOOMR.debug("Ready to send beacon: " + BOOMR.utils.objectToString(impl.vars));
+ if (!impl.beacon_url) {
+ BOOMR.debug("No beacon URL, so skipping.");
+ return true;
+ }
- n = lat_filtered.length-1;
+ //
+ // Try to send an IMG beacon if possible (which is the most compatible),
+ // otherwise send an XHR beacon if the URL length is longer than 2,000 bytes.
+ //
+
+ // clone the vars object for two reasons: first, so all listeners of
+ // onbeacon get an exact clone (in case listeners are doing
+ // BOOMR.removeVar), and second, to help build our priority list of vars.
+ for (k in impl.vars) {
+ if (impl.vars.hasOwnProperty(k)) {
+ varsSent[k] = impl.vars[k];
+ varsToSend[k] = impl.vars[k];
+ }
+ }
- median = Math.round(
- (lat_filtered[Math.floor(n/2)] + lat_filtered[Math.ceil(n/2)]) / 2
- );
+ // get high- and low-priority variables first, which remove any of
+ // those vars from varsToSend
+ urlFirst = this.getVarsOfPriority(varsToSend, -1);
+ urlLast = this.getVarsOfPriority(varsToSend, 1);
- return { mean: amean, median: median, stddev: std_dev, stderr: std_err };
- },
+ // merge the 3 lists
+ params = urlFirst.concat(this.getVarsOfPriority(varsToSend, 0), urlLast);
+ paramsJoined = params.join("&");
- calc_bw: function()
- {
- var i, j, n=0,
- r, bandwidths=[], bandwidths_corrected=[],
- sum=0, sumsq=0, sum_corrected=0, sumsq_corrected=0,
- amean, std_dev, std_err, median,
- amean_corrected, std_dev_corrected, std_err_corrected, median_corrected,
- nimgs, bw, bw_c;
+ // if there are already url parameters in the beacon url,
+ // change the first parameter prefix for the boomerang url parameters to &
+ url = impl.beacon_url + ((impl.beacon_url.indexOf("?") > -1) ? "&" : "?") + paramsJoined;
- for(i=0; i BOOMR.constants.MAX_GET_LENGTH) {
+ // switch to a XHR beacon if the the user has specified a POST OR GET length is too long
+ useImg = false;
}
- r=this.results[i].r;
+ BOOMR.removeVar("qt");
- // the next loop we iterate through backwards and only consider the largest
- // 3 images that succeeded that way we don't consider small images that
- // downloaded fast without really saturating the network
- nimgs=0;
- for(j=r.length-1; j>=0 && nimgs<3; j--) {
- // if we hit an undefined image time, we skipped everything before this
- if(!r[j]) {
- break;
- }
- if(r[j].t === null) {
- continue;
- }
+ // If we reach here, we've transferred all vars to the beacon URL.
+ // The only thing that can stop it now is if we're rate limited
+ impl.fireEvent("onbeacon", varsSent);
- n++;
- nimgs++;
+ // keep track of page load beacons
+ if (!impl.hasSentPageLoadBeacon && isPageLoad) {
+ impl.hasSentPageLoadBeacon = true;
- // multiply by 1000 since t is in milliseconds and not seconds
- bw = images[j].size*1000/r[j].t;
- bandwidths.push(bw);
+ // let this beacon go out first
+ BOOMR.setImmediate(function() {
+ impl.fireEvent("page_load_beacon", varsSent);
+ });
+ }
- bw_c = images[j].size*1000/(r[j].t - this.latency.mean);
- bandwidths_corrected.push(bw_c);
+ if (params.length === 0) {
+ // do not make the request if there is no data
+ return this;
}
- }
- BOOMR.debug('got ' + n + ' readings', "bw");
+ if (!BOOMR.orig_XMLHttpRequest && (!BOOMR.window || !BOOMR.window.XMLHttpRequest)) {
+ // if we don't have XHR available, force an image beacon and hope
+ // for the best
+ useImg = true;
+ }
- BOOMR.debug('bandwidths: ' + bandwidths, "bw");
- BOOMR.debug('corrected: ' + bandwidths_corrected, "bw");
+ if (useImg) {
+ // just in case Image isn't a valid constructor
+ try {
+ img = new Image();
+ }
+ catch (e) {
+ BOOMR.debug("Image is not a constructor, not sending a beacon");
+ return false;
+ }
- // First do IQR filtering since we use the median here
- // and should use the stddev after filtering.
- if(bandwidths.length > 3) {
- bandwidths = this.iqr(bandwidths.sort(this.ncmp));
- bandwidths_corrected = this.iqr(bandwidths_corrected.sort(this.ncmp));
- } else {
- bandwidths = bandwidths.sort(this.ncmp);
- bandwidths_corrected = bandwidths_corrected.sort(this.ncmp);
- }
+ img.src = url;
- BOOMR.debug('after iqr: ' + bandwidths, "bw");
- BOOMR.debug('corrected: ' + bandwidths_corrected, "bw");
+ if (impl.secondary_beacons) {
+ for (k = 0; k < impl.secondary_beacons.length; k++) {
+ url = impl.secondary_beacons[k] + "?" + paramsJoined;
- // Now get the mean & median.
- // Also get corrected values that eliminate latency
- n = Math.max(bandwidths.length, bandwidths_corrected.length);
- for(i=0; i= images.end-1
- || typeof this.results[this.nruns-run].r[i+1] !== "undefined"
- ) {
- BOOMR.debug(this.results[this.nruns-run], "bw");
- // First run is a pilot test to decide what the largest image
- // that we can download is. All following runs only try to
- // download this image
- if(run === this.nruns) {
- images.start = i;
- }
- this.defer(this.iterate);
- } else {
- this.load_img(i+1, run, this.img_loaded);
- }
- },
+ /**
+ * Gets all variables of the specified priority
+ *
+ * @param {object} vars Variables (will be modified for pri -1 and 1)
+ * @param {number} pri Priority (-1, 0, or 1)
+ *
+ * @return {string[]} Array of URI-encoded vars
+ */
+ getVarsOfPriority: function(vars, pri) {
+ var name, url = [];
+
+ if (pri !== 0) {
+ // if we were given a priority, iterate over that list
+ for (name in impl.varPriority[pri]) {
+ if (impl.varPriority[pri].hasOwnProperty(name)) {
+ // if this var is set, add it to our URL array
+ if (vars.hasOwnProperty(name)) {
+ url.push(this.getUriEncodedVar(name, vars[name]));
+
+ // remove this name from vars so it isn't also added
+ // to the non-prioritized list when pri=0 is called
+ delete vars[name];
+ }
+ }
+ }
+ }
+ else {
+ // if we weren't given a priority, iterate over all of the vars
+ // that are left (from not being removed via earlier pri -1 or 1)
+ for (name in vars) {
+ if (vars.hasOwnProperty(name)) {
+ url.push(this.getUriEncodedVar(name, vars[name]));
+ }
+ }
+ }
- finish: function()
- {
- if(!this.latency) {
- this.latency = this.calc_latency();
- }
- var bw = this.calc_bw(),
- o = {
- bw: bw.median_corrected,
- bw_err: parseFloat(bw.stderr_corrected, 10),
- lat: this.latency.mean,
- lat_err: parseFloat(this.latency.stderr, 10),
- bw_time: Math.round(new Date().getTime()/1000)
- };
+ return url;
+ },
- BOOMR.addVar(o);
-
- // If we have an IP address we can make the BA cookie persistent for a while
- // because we'll recalculate it if necessary (when the user's IP changes).
- if(!isNaN(o.bw)) {
- BOOMR.utils.setCookie(this.cookie,
- {
- ba: Math.round(o.bw),
- be: o.bw_err,
- l: o.lat,
- le: o.lat_err,
- ip: this.user_ip,
- t: o.bw_time
- },
- (this.user_ip ? this.cookie_exp : 0),
- "/",
- null
+ /**
+ * Gets a URI-encoded name/value pair.
+ *
+ * @param {string} name Name
+ * @param {string} value Value
+ *
+ * @returns {string} URI-encoded string
+ */
+ getUriEncodedVar: function(name, value) {
+ var result = encodeURIComponent(name) +
+ "=" +
+ (
+ value === undefined || value === null ?
+ "" :
+ encodeURIComponent(value)
);
- }
- this.complete = true;
- BOOMR.sendBeacon();
- this.running = false;
- },
+ return result;
+ },
- iterate: function()
- {
- if(this.aborted) {
- return false;
+ /**
+ * Gets the latest ResourceTiming entry for the specified URL
+ * Default sort order is chronological startTime
+ * @param {string} url Resource URL
+ * @param {function} [sort] Sort the entries before returning the last one
+ * @returns {PerformanceEntry|undefined} Entry, or undefined if ResourceTiming is not
+ * supported or if the entry doesn't exist
+ */
+ getResourceTiming: function(url, sort) {
+ var entries;
+
+ try {
+ if (BOOMR.getPerformance() &&
+ typeof BOOMR.getPerformance().getEntriesByName === "function") {
+ entries = BOOMR.getPerformance().getEntriesByName(url);
+ if (entries && entries.length) {
+ if (typeof sort === "function") {
+ entries.sort(sort);
+ }
+ return entries[entries.length - 1];
+ }
+ }
+ }
+ catch (ignore) {
+ // empty
+ }
}
- 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(images.start, this.runs_left--, this.img_loaded);
- }
- },
-
- setVarsFromCookie: function(cookies) {
- var ba = parseInt(cookies.ba, 10),
- bw_e = parseFloat(cookies.be, 10),
- lat = parseInt(cookies.l, 10) || 0,
- lat_e = parseFloat(cookies.le, 10) || 0,
- c_sn = cookies.ip.replace(/\.\d+$/, '0'), // Note this is IPv4 only
- t = parseInt(cookies.t, 10),
- p_sn = this.user_ip.replace(/\.\d+$/, '0'),
-
- // We use the subnet instead of the IP address because some people
- // on DHCP with the same ISP may get different IPs on the same subnet
- // every time they log in
-
- t_now = Math.round((new Date().getTime())/1000); // seconds
-
- // If the subnet changes or the cookie is more than 7 days old,
- // then we recheck the bandwidth, else we just use what's in the cookie
- if(c_sn === p_sn && t >= t_now - this.cookie_exp) {
- this.complete = true;
- BOOMR.addVar({
- 'bw': ba,
- 'lat': lat,
- 'bw_err': bw_e,
- 'lat_err': lat_e
- });
+ };
- return true;
- }
+ delete BOOMR_start;
- return false;
+ if (typeof BOOMR_lstart === "number") {
+ boomr.t_lstart = BOOMR_lstart;
+ delete BOOMR_lstart;
+ }
+ else if (typeof BOOMR.window.BOOMR_lstart === "number") {
+ boomr.t_lstart = BOOMR.window.BOOMR_lstart;
}
-};
-
-BOOMR.plugins.BW = {
- init: function(config) {
- var cookies;
-
- BOOMR.utils.pluginConfig(impl, config, "BW",
- ["base_url", "timeout", "nruns", "cookie", "cookie_exp"]);
-
- if(config && config.user_ip) {
- impl.user_ip = config.user_ip;
- }
-
- images.start = 0;
- impl.runs_left = impl.nruns;
- impl.latency_runs = 10;
- impl.results = [];
- impl.latencies = [];
- impl.latency = null;
- impl.complete = false;
- impl.aborted = false;
-
- BOOMR.removeVar('ba', 'ba_err', 'lat', 'lat_err');
+ if (typeof BOOMR.window.BOOMR_onload === "number") {
+ boomr.t_onload = BOOMR.window.BOOMR_onload;
+ }
- cookies = BOOMR.utils.getSubCookies(BOOMR.utils.getCookie(impl.cookie));
+ (function() {
+ var make_logger;
- if(!cookies || !cookies.ba || !impl.setVarsFromCookie(cookies)) {
- BOOMR.subscribe("page_ready", this.run, null, this);
+ if (typeof console === "object" && console.log !== undefined) {
+ boomr.log = function(m, l, s) { console.log(s + ": [" + l + "] " + m); };
}
- return this;
- },
+ make_logger = function(l) {
+ return function(m, s) {
+ this.log(m, l, "boomerang" + (s ? "." + s : ""));
+ return this;
+ };
+ };
- run: function() {
- if(impl.running || impl.complete) {
- return this;
+ boomr.debug = make_logger("debug");
+ boomr.info = make_logger("info");
+ boomr.warn = make_logger("warn");
+ boomr.error = make_logger("error");
+ }());
+
+ // If the browser supports performance.now(), swap that in for BOOMR.now
+ try {
+ var p = boomr.getPerformance();
+ if (p &&
+ typeof p.now === "function" &&
+ /\[native code\]/.test(String(p.now)) && // #545 handle bogus performance.now from broken shims
+ p.timing &&
+ p.timing.navigationStart) {
+ boomr.now = function() {
+ return Math.round(p.now() + p.timing.navigationStart);
+ };
}
+ }
+ catch (ignore) {
+ // empty
+ }
- if(w.location.protocol === 'https:') {
- // we don't run the test for https because SSL stuff will mess up b/w
- // calculations we could run the test itself over HTTP, but then IE
- // will complain about insecure resources, so the best is to just bail
- // and hope that the user gets the cookie from some other page
-
- BOOMR.info("HTTPS detected, skipping bandwidth test", "bw");
- impl.complete = true;
- BOOMR.sendBeacon();
- return this;
+ (function() {
+ var ident;
+ for (ident in boomr) {
+ if (boomr.hasOwnProperty(ident)) {
+ BOOMR[ident] = boomr[ident];
+ }
}
-
- impl.running = true;
-
- setTimeout(this.abort, impl.timeout);
-
- impl.defer(impl.iterate);
-
- return this;
- },
-
- abort: function() {
- impl.aborted = true;
- if (impl.running) {
- impl.finish(); // we don't defer this call because it might be called from
- // onunload and we want the entire chain to complete
- // before we return
+ if (!BOOMR.xhr_excludes) {
+ //! URLs to exclude from automatic XHR instrumentation
+ BOOMR.xhr_excludes = {};
}
- return this;
- },
+ }());
- is_complete: function() { return impl.complete; }
-};
+ dispatchEvent("onBoomerangLoaded", { "BOOMR": BOOMR }, true);
}(window));
-// End of BW plugin
-
-
-/*jslint white: false, devel: true, onevar: true, browser: true, undef: true, nomen: true, regexp: false, continue: true, plusplus: false, bitwise: false, newcap: true, maxerr: 50, indent: 4 */
+// end of boomerang beaconing section
diff --git a/bower.json b/bower.json
new file mode 100644
index 000000000..92776387f
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,52 @@
+{
+ "name": "soasta-boomerang",
+ "private": true,
+ "main": "boomerang.js",
+ "version": "1.0.0",
+ "homepage": "https://github.com/SOASTA/boomerang/",
+ "authors": [
+ "Philip Tellis"
+ ],
+ "description": "End user oriented web performance testing and beaconing",
+ "keywords": [
+ "boomerang",
+ "performance",
+ "NavigationTiming",
+ "ResourceTiming",
+ "UserTiming",
+ "PerformanceTimeline"
+ ],
+ "license": "BSD",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "devDependencies": {
+ "angular-resource": "~1.3.16",
+ "angular-resource-1.4": "angular-resource#1.4.6",
+ "angular-resource-1.5": "angular-resource#1.5.0-beta.0",
+ "angular-route": "~1.3.16",
+ "angular-route-1.4": "angular-route#1.4.6",
+ "angular-route-1.5": "angular-route#1.5.0-beta.0",
+ "angular-ui-router": "~0.2.15",
+ "angular": "~1.3.16",
+ "angular-1.4": "angular#1.4.6",
+ "angular-1.5": "angular#1.5.0-beta.0",
+ "assertive-chai": "~2.0.0",
+ "backbone": "~1.2.0",
+ "ember": "~1.12.1",
+ "handlebars": "~3.0.3",
+ "jquery": "~2.1.4",
+ "json3": "~3.3.2",
+ "lodash": "~3.0.0",
+ "mocha": "~1.21.5",
+ "resourcetiming-compression": "^0.3.3",
+ "usertiming-compression": "~0.1.4"
+ },
+ "resolutions": {
+ "angular": "1.4.6"
+ }
+}
diff --git a/dns.js b/dns.js
deleted file mode 100644
index dd29b66be..000000000
--- a/dns.js
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (c) 2011, Yahoo! Inc. All rights reserved.
- * Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms.
- */
-
-/**
-\file dns.js
-Plugin to measure DNS latency.
-This code is based on Carlos Bueno's guide to DNS on the YDN blog:
-http://developer.yahoo.net/blog/archives/2009/11/guide_to_dns.html
-*/
-
-// w is the window object
-(function(w) {
-
-BOOMR = BOOMR || {};
-BOOMR.plugins = BOOMR.plugins || {};
-
-var impl = {
- complete: false,
- base_url: "",
- t_start: null,
- t_dns: null,
- t_http: null,
- img: null,
-
- gen_url: "",
-
- start: function() {
- var random = Math.floor(Math.random()*(2147483647)).toString(36),
- cache_bust = "" + (new Date().getTime()) + (Math.random());
-
- this.gen_url = this.base_url.replace(/\*/, random);
-
- impl.img = new Image();
- impl.img.onload = impl.A_loaded;
-
- impl.t_start = new Date().getTime();
- impl.img.src = this.gen_url + "image-l.gif?t=" + cache_bust;
- },
-
- A_loaded: function() {
- var cache_bust;
- impl.t_dns = new Date().getTime() - impl.t_start;
-
- cache_bust = "" + (new Date().getTime()) + (Math.random());
-
- impl.img = new Image();
- impl.img.onload = impl.B_loaded;
-
- impl.t_start = new Date().getTime();
- impl.img.src = impl.gen_url + "image-l.gif?t=" + cache_bust;
- },
-
- B_loaded: function() {
- impl.t_http = new Date().getTime() - impl.t_start;
-
- impl.img = null;
- impl.done();
- },
-
- done: function() {
- // DNS time is the time to load the image with uncached DNS
- // minus the time to load the image with cached DNS
-
- var dns = impl.t_dns - impl.t_http;
-
- BOOMR.addVar("dns", dns);
- this.complete = true;
- BOOMR.sendBeacon();
- },
-
- read_timing_api: function(t) {
- if(typeof t.domainLookupStart === "undefined"
- || typeof t.domainLookupEnd === "undefined") {
- return false;
- }
-
- // This will be 0 if we read DNS from cache, but that's what
- // we want because it's what the user experiences
- BOOMR.addVar("dns", t.domainLookupEnd - t.domainLookupStart);
-
- impl.complete = true;
-
- return true;
- }
-};
-
-BOOMR.plugins.DNS = {
- init: function(config) {
- BOOMR.utils.pluginConfig(impl, config, "DNS", ["base_url"]);
-
- // If this browser supports the WebTiming API, then we just
- // use that and don't bother doing the test
- if(w.performance && w.performance.timing) {
- if(impl.read_timing_api(w.performance.timing)) {
- return this;
- }
- }
-
- if(!impl.base_url) {
- BOOMR.warn("DNS.base_url is not set. Cannot run DNS test.", "dns");
- impl.complete = true; // set to true so that is_complete doesn't
- // block other plugins
- return this;
- }
-
- // make sure that dns test images use the same protocol as the host page
- if(w.location.protocol === 'https:') {
- impl.base_url = impl.base_url.replace(/^http:/, 'https:');
- }
- else {
- impl.base_url = impl.base_url.replace(/^https:/, 'http:');
- }
-
- BOOMR.subscribe("page_ready", impl.start, null, this);
-
- return this;
- },
-
- is_complete: function() {
- return impl.complete;
- }
-};
-
-}(window));
-
diff --git a/doc/TODO.md b/doc/TODO.md
new file mode 100644
index 000000000..e2bbb6cbf
--- /dev/null
+++ b/doc/TODO.md
@@ -0,0 +1,8 @@
+1. Add random sampling
+ This needs to be a little intelligent because plugins may have different
+ criteria for sampling. For example, the RT plugin requires two pages --
+ one for tstart and one for tend. Since tstart is a relatively inexpensive
+ operation, it makes sense for us to set tstart on all pages, but only set
+ tend based on the random sample.
+
+2. Rewrite bandwidth testing code to be pretty and clean
diff --git a/doc/api/AutoXHR.md b/doc/api/AutoXHR.md
new file mode 100644
index 000000000..350ea164b
--- /dev/null
+++ b/doc/api/AutoXHR.md
@@ -0,0 +1,92 @@
+AutoXHR - Instrument and measure AJAX requests and DOM manipulations triggering network requests
+
+With this plugin sites can not only monitor their page load but also the XMLHttpRequests after the page has been loaded
+as well as the DOM manipulations that trigger an asset to be fetched.
+
+### Configuring Boomerang with AutoXHR included
+
+You can enable the AutoXHR plugin with:
+
+```js
+BOOMR.init({
+ instrument_xhr: true
+})
+```
+
+Once this has been done and the site has been loaded and instrumented with this plugin, we have replaced the `XMLHttpRequest` object
+on the site with our very own!
+
+### Monitoring XHRs
+
+If you were to send an `XMLHttpRequest` on your page now you can see not only a page load beacon but also an XHR beacon:
+
+```js
+xhr = new XMLHttpRequest();
+xhr.open("GET", "support/script200.js"); //200, async
+xhr.send(null);
+```
+
+If you have boomerang with the auto_xhr.js plugin included in your page and someone visited the page, we are able
+to measure the time this asset takes to come down from the server. Measured performance data will include:
+
+ - `u`: the URL of the image that has been fetched
+ - `http.initiator`: The type of Beacon or Request that has been triggered in this case `xhr`
+
+### Using AutoXHR to monitor DOM events
+
+Say for example that you have button on the page that will insert a new picture, we can monitor this asset coming down and
+send timing information for that page:
+
+```html
+
+
+Click Me!
+
+```
+
+### Before Page Load XHR Beacons
+
+By default AutoXHR will wait until the pages page load beacon has been sent to enable itself to start sending beacons that will correspond with the pages
+XHRs. If you wish to enable AutoXHR Beacons sending before the site itself has loaded, you can set a config flag for the AutoXHR plugin to enable it to
+send beacons as soon as it has instrumented the page.
+
+```
+BOOMR.init({
+ instrument_xhr: true,
+ AutoXHR: {
+ alwaysSendXhr: true
+ }
+})
+```
+
+### Compatibility and Browser Support
+
+Currently supported Browsers and platforms that AutoXHR will work on:
+
+ - IE 9+ (not in quirks mode)
+ - Chrome 38+
+ - Firefox 25+
+
+In general we support all browsers that support `MutationObserver`, `XMLHttpRequest` with `addEventListener` on the XMLHttpRequest instance
+
+
diff --git a/doc/api/BOOMR.html b/doc/api/BOOMR.html
index df3d9d74c..d3c6fcb21 100644
--- a/doc/api/BOOMR.html
+++ b/doc/api/BOOMR.html
@@ -73,6 +73,30 @@ Configuration
There is no default value for this parameter. If not set, no beacon will be sent.
+beacon_type
+
+[optional]
+Specify the HTTP method for beacon requests, may be 'GET'
, 'POST'
or 'AUTO'
(the default).
+The AUTO setting will make a GET request unless the combined beacon URL plus query string
+exceeds 2000 characters, in which case it will issue a POST. The data will be
+application/x-www-form-urlencoded
.
+
+
+beacon_auth_key
+
+[optional]
+Specify the HTTP authentication key for beacon requests. This key will default to 'Authorization'
, but may be explicitly set using this
+setting. Some services use specialized authentication keys such as 'X-API-KEY'
.
+
+
+beacon_auth_token
+
+[optional]
+Specify the HTTP authentication token for beacon requests. This token is generally supplied by the service accepting the beacon request and will take
+a form similar to'SPLUNK 4F00X7AF-B3D3-4E07-8C6C-12345678901'
. The value supplied here will be contatenated with the beacon_auth_key to
+form a complete HTTP request header field such as 'Authorization':'SPLUNK 4F00X7AF-B3D3-4E07-8C6C-12345678901'
.
+
+
site_domain
[recommended]
@@ -81,7 +105,8 @@ Configuration
wrong. It's a good idea to set this to whatever part of your domain you'd like to
share bandwidth and performance measurements across.
If you have multiple domains, then you're out of luck. You'll just have to get
-separate measurements across them.
+separate measurements across them.
+Set this to a falsy value to disable all cookies.
user_ip
@@ -398,7 +423,7 @@ Beacon Parameters
-The latest code and docs is available on github.com/lognormal/boomerang
+The latest code and docs is available on github.com/SOASTA/boomerang
+ All Docs | Index
+ The Clicks Plugin
+ The GUID
Plugin adds a tracking cookie to the user that will be sent to the beacon-server as cookie
+ The GUID
API is encapsulated in the BOOMR.plugins.GUID
namespace
+
+ Configuration
+
+ The GUID plugin configuration is contained in the GUID
namespace in the initial Boomerang configuration Object.
+
+ See "Howto #6 — Configuring boomerang" for more information on how to configure Boomerang
+
+
+ - cookieName
+ -
+ [required]
+ The name of the cookie to be set in the browser session
+
+ - expires
+ -
+ [optional]
+ An expiry time for the cookie in seconds. By default 7 days.
+
+
+
+Methods
+
+
+
+ - init(oConfig)
+ -
+
+ Called by the BOOMR.init() method to configure the clicks plugin.
+
+
+ BOOMR.init({
+ GUID: {
+ cookieName: "boomerang-guid",
+ expires: 6000
+ }
+ });
+
+ Parameters
+
+ - oConfig
+ - The configuration object passed in via
BOOMR.init()
. See the Configuration section for details.
+
+ Returns
+
+ a reference to the BOOMR.plugins.GUID
object, so you can chain methods.
+
+
+
+
+ Beacon Parameters
+
+ None: no beacon parameters will be set cookies will be sent in the HTTP Header
+
+
+
+ The latest code and docs is available on github.com/SOASTA/boomerang
+
+
+
diff --git a/doc/api/BOOMR.utils.html b/doc/api/BOOMR.utils.html
index 3e068df0c..ef4e68c89 100644
--- a/doc/api/BOOMR.utils.html
+++ b/doc/api/BOOMR.utils.html
@@ -163,7 +163,7 @@
diff --git a/doc/api/BW.html b/doc/api/BW.html
index 834cdaf1b..c3022bd02 100644
--- a/doc/api/BW.html
+++ b/doc/api/BW.html
@@ -21,12 +21,11 @@
diff --git a/doc/api/DNS.html b/doc/api/DNS.html
index d3ea2fbd8..fb7e13a24 100644
--- a/doc/api/DNS.html
+++ b/doc/api/DNS.html
@@ -84,12 +84,12 @@
diff --git a/doc/api/GUID.html b/doc/api/GUID.html
new file mode 100644
index 000000000..5053a0408
--- /dev/null
+++ b/doc/api/GUID.html
@@ -0,0 +1,71 @@
+
+
+