diff --git a/snowplow-javascript-tracker/.babelrc b/snowplow-javascript-tracker/.babelrc new file mode 100644 index 0000000000..4fdf1115a5 --- /dev/null +++ b/snowplow-javascript-tracker/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [["@babel/preset-env", { + "targets": { + "chrome": 32, + "ie": 9, + "firefox": 27, + "safari": 8 + } + }]] +} diff --git a/snowplow-javascript-tracker/.gitignore b/snowplow-javascript-tracker/.gitignore new file mode 100644 index 0000000000..e204a2c1f9 --- /dev/null +++ b/snowplow-javascript-tracker/.gitignore @@ -0,0 +1,34 @@ +# Tests suite +ngrok + +# node.js +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log +node_modules + +**/lodash.js +tests/pages/*.js +tests/pages/integration.html +tests/local/serve/ +tags/tag.min.js + +# creds for Grunt +aws.json + +# Vagrant +.vagrant +VERSION + +src/js/lib_managed diff --git a/snowplow-javascript-tracker/.travis.yml b/snowplow-javascript-tracker/.travis.yml new file mode 100644 index 0000000000..7f4aca57bf --- /dev/null +++ b/snowplow-javascript-tracker/.travis.yml @@ -0,0 +1,58 @@ +node_js: +- 8.12.0 +language: node_js +install: +- npm install -g npm@6.4.1 +- npm install -g grunt-cli + +# install packages and build core +- cd core +- rm -rf node_modules +- npm install +- grunt ts +- grunt dtsGenerator + +# install packages for tracker +- cd .. +- rm -rf node_modules +- npm install + +# get ngrok +- wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip +- unzip ./ngrok-stable-linux-amd64.zip +before_script: +- export SUBDOMAIN=$RANDOM$RANDOM +- ./ngrok http -authtoken $NGROK_AUTH -subdomain $SUBDOMAIN -log=stdout 8500 > /dev/null & +script: +- cd core && grunt intern && cd .. +- grunt travis +deploy: +- provider: script + skip_cleanup: true + script: ./.travis/deploy.py + on: + condition: '"$(.travis/is_core_release_tag.sh $TRAVIS_TAG)" == "" && $? == 0' + tags: true +- provider: script + skip_cleanup: true + script: ./.travis/deploy.py + on: + condition: '"$(.travis/is_tracker_release_tag.sh $TRAVIS_TAG)" == "" && $? == 0' + tags: true +env: + global: + # SAUCE_USERNAME + - secure: S/6Ild1n9SmW8xYqarjkigDqmyFgWS4Hi0M07e/ryPeS1MNQdiS5+WuSmlSTevM/wU/oSCvzXnKhO9W/1tvz4qj/BMQYxYWFjwWAXJrkjJPvWoa5ogYkR3ETSYrqdhkT+8LVof8iLPlpHjS1y2fozyJbYf1YNSkcVCLxMdIwnk0= + # SAUCE_AUTH_KEY + - secure: bkmtL2PXFGdOB6YNfWw6thEyVAbX/l7Q99pfT2jHmfzhTDf4/FT1CprsAacf0JeYkwMg5UQDMqAcZSojjO6GeAY8rhC94Tor5hKoEwCL4wg5NatXBUUI+WWaoBSg2KWYG2MfkPxtGJyNk1LPQoV+nUUzwEGejV8lJvaHVxkBsqA= + # NGROK_AUTH + - secure: JwP5MuqZ6k8FlHg8AnzYTrCZByJLnlu0to2srEwfVOhW1efGTEmtWf/SLlplYHcdelsFakdiSbD3BkZyEqpe/2/AIb56KLAqtE2Ng3WS8Vz/80yknxII0xcSPzAZdhZYRUJlbIrA1Ua3XGgirBenjn7ILFXNX/qLgvwJ1uLKCPo= + # AWS_ACCESS_KEY + - secure: AEvIKW1H/DPU5yPKfRPHY9XpBCjXGtwVqpQERx0wYGwV8j9mzz8EMMrx6yPMaLOkeBMcaH/db3aXEePHWAlnxrTuUuhwwr9A7sihaaAwZ9GllUlUEJzfXeBcCM9rhsooY2roMzJM3Ener1W+yKHThagYxUZ4PyZrPJCMMzbc0qw= + # AWS_SECRET_KEY + - secure: C+PTon3PsELG2bFlAZ5qfinWVih8XxlZV45Gk2W5i2CK1AROJ60lnfhgYZM4YidIRcA/V/zbm8tDgZbA8i/MVKcO5f1qbD5Skyo2fEFRlXoSWhtwaScViihN338U/SsEviKThIGSSLgdfsQ0CW7TYxvetFSwXFyZPPrYrenF3dk= + # NPM_AUTH_TOKEN + - secure: Dtc3aFnUfquuC+E3ZOvs57SZy3Sp+3QdXQHR0xjekvpF4FgaKvDpISBfLTCs1ODuHZ+K1pPn9/i4yGNE7ei8z3VTbRY4nYPM5VG7jlqwP1iaFqRopnQmpV9c7yhMSHKhXrSWj/Gwo/9UHpia6muQUSK4hbPvhZMf0Rd3s06Z07M= + - AWS_S3_BUCKET=snowplow-static-js + - AWS_REGION=eu-west-1 + - AWS_CF_DISTRIBUTION=E1RICD7QD0F2KE diff --git a/snowplow-javascript-tracker/.travis/deploy.py b/snowplow-javascript-tracker/.travis/deploy.py new file mode 100755 index 0000000000..97619b0eab --- /dev/null +++ b/snowplow-javascript-tracker/.travis/deploy.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python + +""" +Script responsible for deploying both snowplow-tracker-core and snowplow-tracker +Need to be executed in Travis CI environment by tag (core/x.y.z OR x.y.z) +Publish core locally, build tracker using locally publiched core and then +publish altogether +""" + +from contextlib import contextmanager +import os +import sys +import subprocess + + +# Initial setup + +if 'TRAVIS_TAG' in os.environ: + TRAVIS_TAG = os.environ.get('TRAVIS_TAG') +else: + sys.exit("Environment variable TRAVIS_TAG is unavailable") + +if 'TRAVIS_BUILD_DIR' in os.environ: + TRAVIS_BUILD_DIR = os.environ.get('TRAVIS_BUILD_DIR') +else: + sys.exit("Environment variable TRAVIS_BUILD_DIR is unavailable") + +if 'NPM_AUTH_TOKEN' in os.environ: + NPM_AUTH_TOKEN = os.environ.get('NPM_AUTH_TOKEN') +else: + sys.exit("Environment variable NPM_AUTH_TOKEN is unavailable") + +if TRAVIS_TAG.startswith('core/'): + PROJECT = 'core' + VERSION = TRAVIS_TAG[5:] +else: + PROJECT = 'tracker' + VERSION = TRAVIS_TAG + + if 'AWS_ACCESS_KEY' in os.environ: + AWS_ACCESS_KEY = os.environ.get('AWS_ACCESS_KEY') + else: + sys.exit("Environment variable AWS_ACCESS_KEY is unavailable (required for tracker publishing)") + + if 'AWS_SECRET_KEY' in os.environ: + AWS_SECRET_KEY = os.environ.get('AWS_SECRET_KEY') + else: + sys.exit("Environment variable AWS_SECRET_KEY is unavailable (required for tracker publishing)") + + if 'AWS_S3_BUCKET' in os.environ: + AWS_S3_BUCKET = os.environ.get('AWS_S3_BUCKET') + else: + sys.exit("Environment variable AWS_S3_BUCKET is unavailable (required for tracker publishing)") + + if 'AWS_REGION' in os.environ: + AWS_REGION = os.environ.get('AWS_REGION') + else: + sys.exit("Environment variable AWS_REGION is unavailable (required for tracker publishing)") + + if 'AWS_CF_DISTRIBUTION' in os.environ: + AWS_CF_DISTRIBUTION = os.environ.get('AWS_CF_DISTRIBUTION') + else: + sys.exit("Environment variable AWS_CF_DISTRIBUTION is unavailable (required for tracker publishing)") + + +# Helper functions + +def output_if_error(sbt_output): + """Callback to print stderr and fail deploy if exit status not successful""" + (stdout, stderr) = sbt_output.communicate() + if sbt_output.returncode != 0: + print("Process has been failed.\n" + stdout) + sys.exit(stderr) + + +def execute(command, callback=output_if_error): + """Execute shell command with optional callback""" + formatted_command = " ".join(command) if (type(command) == list) else command + print("Executing [{0}]".format(formatted_command)) + output = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if hasattr(callback, '__call__'): + return callback(output) + else: + return output + + +def check_version(): + """Fail deploy if tag version doesn't match SBT version""" + if PROJECT == 'core': + get_version = """var fs=require('fs'); fs.readFile('./core/package.json', 'utf8', function(e,d) { console.log(JSON.parse(d)['version']) });""" + elif PROJECT == 'tracker': + get_version = """var fs=require('fs'); fs.readFile('./package.json', 'utf8', function(e,d) { console.log(JSON.parse(d)['version']) });""" + else: + sys.exit("Unknown project " + str(PROJECT)) + + node_output = execute(['node', '-e', get_version], None) + print(node_output.stderr.read()) + for line in node_output.stdout.read().split("\n"): + print(line) + if line: + if line != VERSION: + sys.exit("Version extracted from TRAVIS_TAG [{0}] doesn't conform declared in package.json [{1}]".format(VERSION, line)) + else: + return + + sys.exit("Cannot find version in core output:\n" + str(node_output)) + + +@contextmanager +def npm_credentials(): + """Context manager allowing to use different credentials and delete them after use""" + npmrc = os.path.expanduser("~/.npmrc") + + if os.path.isfile(npmrc): + os.remove(npmrc) + print("WARNING! ~/.npmrc already exists. It should be deleted after each use") + print("Overwriting existing ~/.npmrc") + else: + print("Creating ~/.npmrc") + + with open(npmrc, 'a') as f: + f.write("registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=" + NPM_AUTH_TOKEN) + + yield + + print("Deleting ~/.npmrc") + os.remove(npmrc) + + +@contextmanager +def aws_credentials(): + """Context manager allowing to use different credentials and delete them after use""" + awsjson = os.path.join(TRAVIS_BUILD_DIR, "aws.json") + + if os.path.isfile(awsjson): + sys.exit("aws.json already exists. It should be deleted after each use") + else: + print("Creating aws.json") + with open(awsjson, 'a') as f: + f.write( + '{"key":"%(key)s","secret":"%(secret)s","bucket":"%(bucket)s","region":"%(region)s", "distribution":"%(distribution)s"}' % \ + {'key': AWS_ACCESS_KEY, 'secret': AWS_SECRET_KEY, 'bucket': AWS_S3_BUCKET, 'distribution': AWS_CF_DISTRIBUTION, 'region': AWS_REGION} + ) + + yield + + print("Deleting aws.json") + os.remove(awsjson) + + +def publish_core(): + os.chdir(os.path.join(TRAVIS_BUILD_DIR, "core")) + with npm_credentials(): + execute(['npm', 'publish']) + + +def publish_tracker(): + os.chdir(TRAVIS_BUILD_DIR) + with aws_credentials(): + execute(['grunt', 'publish']) + + +def publish(): + if PROJECT == 'core': + publish_core() + elif PROJECT == 'tracker': + publish_tracker() + else: + sys.exit("Unknown project " + str(PROJECT)) + +if __name__ == "__main__": + # Publish locally all dependencies + check_version() + publish() diff --git a/snowplow-javascript-tracker/.travis/is_core_release_tag.sh b/snowplow-javascript-tracker/.travis/is_core_release_tag.sh new file mode 100755 index 0000000000..7fac811a3b --- /dev/null +++ b/snowplow-javascript-tracker/.travis/is_core_release_tag.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +tag=$1 +cicd=${tag:0:4} +release=${tag:5} + +if [ "${cicd}" == "core" ]; then + if [ "${release}" == "" ]; then + echo "Warning! No release specified! Ignoring." + exit 2 + fi + exit 0 +else + exit 1 +fi \ No newline at end of file diff --git a/snowplow-javascript-tracker/.travis/is_tracker_release_tag.sh b/snowplow-javascript-tracker/.travis/is_tracker_release_tag.sh new file mode 100755 index 0000000000..2c7c43507f --- /dev/null +++ b/snowplow-javascript-tracker/.travis/is_tracker_release_tag.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +tag=$1 +release=$tag + +if [ "${release}" == "" ]; then + echo "Warning! No release specified! Ignoring." + exit 2 +else + if [ "${release:0:4}" == "core" ]; then + echo "Core relase, not tracker" + fi +fi +exit 0 diff --git a/snowplow-javascript-tracker/CHANGELOG b/snowplow-javascript-tracker/CHANGELOG new file mode 100644 index 0000000000..6d0d7962a5 --- /dev/null +++ b/snowplow-javascript-tracker/CHANGELOG @@ -0,0 +1,548 @@ +Version 2.10.0 (2019-01-17) +-------------------------- +Transpile helpers.js and detectors.js (#693) +Allow dynamic context callbacks for link and form tracking (#585) +Fix default configOptOutCookie value (#672) +Remove outdated addClickListener method (#667) +Error-handling for tracker methods (#675) +Beacon API option for sending events to collector (#674) +Tracking click events on forms (#579) +Update tracker script banner (#684) +Add new local testing workflow (#686) +Clean up indentation of integration test template (#691) +Update outdated dependencies (#685) +Fix typo in sesname variable (#671) +Add global contexts features (#405) +Add Babel to build process (#665) +Replace YUI Compressor with UglifyJS (#687) +Fix log output for failed integration tests (#689) +Use modularized imports for lodash (#502) +Update npm steps in .travis.yml (#690) +Consolidate request method API (#700) +Fix Beacon API support (#702) + +Version 2.9.3 (2019-01-09) +-------------------------- +Add option to set a custom POST request path (#696) +Refresh npm authentication token (#688) + +Version 2.9.2 (2018-07-24) +-------------------------- +Remove wheel and passive event listener feature flags (#661) + +Version 2.9.1 (2018-06-20) +-------------------------- +Keep node type definitions at version 9.6.7 (#649) +Update page ping context on every call to trackPageView (#612) +Core: fix type incompatibility in consent methods (#652) +Added src/js/lib_managed to .gitignore (#650) +Use passive event listeners for mouse wheel event (#478) +Check if browser has full support of Performance Timing API (#539) +Add Java 10 to Ansible playbooks (#657) + +Version 2.9.0 (2018-02-27) +-------------------------- +Add ability to change field content before sending form tracking (#465) +Add a method to start a new session (#515) +Make newDocumentTitle variable local (#580) +Enforce that geolocation.timestamp is an integer (#602) +Remove respectOptOutCookie from the Tracker function comments (#605) +Add jsDelivr hits badge (#611) +Add identifyUser as alias for setUserId (#621) +Add trackConsentGranted method (#623) +Add trackConsentWithdrawn method (#624) +Bump semver to 4.3.2 (#625) +LocalStorage domain user ID is not persisted properly (#627) +Core: add trackConsentGranted method (#629) +Core: add trackConsentWithdrawn method (#630) +Update OS X test targets for Saucelabs (#632) +Install dependencies and update shrinkwrap before deployment (#633) + +Version 2.8.2 (2017-08-21) +-------------------------- +Fix opt-out cookie check (#604) + +Version 2.8.1 (2017-07-26) +-------------------------- +Use trackCallback for all events (#589) +Fix grunt ts in CI/CD (#598) +Fix tech docs icon (#596) +Fix string ids in Optimizely Summary context (#591) +Fix race condition with pageViewId creation (#592) + +Version 2.8.0 (2017-05-18) +-------------------------- +Add support for users opting out of state tracking using cookie or localStorage (#459) +Add support for recording state in localStorage rather than cookies (#506) +Exclude password fields from form tracking (#521) +Add Parrable context (#525) +Add support for OptimizelyX context (#551) +Update README markdown in according with CommonMark (#561) +Add support for not recording any state (#563) +Deprecate useCookies (#565) +Deprecate useLocalStorage (#566) +Prevent multiple setInterval from being created (#571) +Guard against non-integer values for minimumVisitLength and heartBeatDelay (#572) +Provide read-only access to domainSessionIndex (#573) +Provide read-only access to cookieName (#574) +Provide read-only access to pageViewId (#575) + +Version 2.7.2 (2017-03-07) +-------------------------- +Add defensive check for window.optimizely.data in getOptimizelyStateContexts (#555) + +Version 2.7.1 (2017-03-06) +-------------------------- +Add defensive check for window.optimizely.data (#553) + +Version 2.7.0 (2017-01-09) +-------------------------- +Add CI/CD (#529) +Add ngrok credentials (#527) +Add Sauce Labs credentials (#528) +Add npm credentials to .travis.yml (#536) +Add AWS credentials to .travis.yml (#537) +Remove random upload path (#538) +Add an option to change life of the visitor cookie, or disable, on tracker creation (#504) +Make tracker Content Security Policy (CSP) compliant (#490) +Add Optimizely summary context (#466) +Add the option to regenerate the page view ID with each page view event (#436) +Add automatic & manual tracking of JS errors to JavaScript tracker (#16) +Get Code Climate badge to green (#152) +Add ability to set true timestamp (#484) +Add timestamp argument for tracking methods (#532) +Remove deprecated visibility state check (#470) +Update Selenium version to 2.48.0 (#487) +Add date for release 2.6.2 to CHANGELOG (#520) +Add trackSelfDescribingEvent method (#471) +Fix integration tests suite (#523) +Bump Core version to 0.5.0 (#301) +Restrict package versions (#522) +Core: only send custom contexts if non-empty array (#540) +Core: add support of true timestamp (#531) +Core: add trackSelfDescribing method (#533) +Core: port to TypeScript (#526) + +Version 2.6.2 (2016-07-14) +-------------------------- +Add date for release 2.6.1 to CHANGELOG (#495) +Don't send invalid viewport dimensions (#488) +Check whether elements of navigator.plugins array are undefined (#492) +Only call navigator.javaEnabled on instances of Navigator (#491) +Stop using String.startsWith (#493) +Ensure page view ID is passed to synchronous tracker (#485) + +Version 2.6.1 (2016-04-14) +-------------------------- +Prevent CSS class listing from failing for elements with no CSS classes (#473) +Explicitly set suites field in Intern config (#475) + +Version 2.6.0 (2016-03-03) +-------------------------- +Bumped ngrok version used in CI to 2.x (#460) +Allowed random upload path in grunt task (#461) +Ensured that PerformanceTiming context doesn't contain properties inherited from Object.prototype (#458) +Added `forceUnsecureTracker` Tracker argument, thanks @bloodyowl! (#374) +Added subset of Augur data as a new context (#386) +Added deprecation warning to setSessionCookieTimeout (#394) +Added setting to automatically use top-level domain for duid (#409) +Added Optimizely contexts (#448) +Added trackEnhancedEcommerceAction() method (#452) +Added addEnhancedEcommerceActionContext() method (#453) +Added addEnhancedEcommerceImpressionContext() method (#454) +Added addEnhancedEcommerceProductContext() method (#455) +Added addEnhancedEcommercePromoContext() method (#456) +Made domainUserId a UUID (#274) +Attached device sent timestamp (stm) to events at last possible moment (#355) +Attempting to create a new tracker using an existing namespace should do nothing (#411) +Using a different library to publish to S3 (#422) +Prevented error running grunt-cloudfront (#426) +Respected doNotTrack in IE 11 and Safari 7.1.3+, thanks @grzegorzewald! (#440) + +Version 2.5.3 (2015-11-10) +-------------------------- +Bumped Node version to 4.1.2 in .travis.yml (#420) +Bumped Intern version to 3.0.6 (#370) +Bumped version of temporary to 0.0.8 (#425) +Bumped grunt-yui-compressor to 0.4.0 (#424) +Bumped grunt-browserify to 3.28.1 (#427) +Fixed jstimezonedetect version at 1.0.5 (#429) +Removed Browserify from devDependencies (#428) +Made extraction of DOM element classes compatible with IE9 (#418) +Stopped dereferencing undefined nodes when setting up form tracking (#423) + +Version 2.5.2 (2015-08-13) +-------------------------- +Prevented the tracker from setting cookies on initialization if "useCookies" is disabled (#403) +Remove tests for unsupported environments (#406) + +Version 2.5.1 (2015-07-27) +-------------------------- +Fixed prerender detection (#391) +Made page title tracking dynamic (#392) +Added warning about using a file URL to example pages (#397) + +Version 2.5.0 (2015-07-22) +-------------------------- +Generated a unique session ID for each new session (#347) +Added a page view UUID (#369) +Maintained visit count when cookies are disabled (#388) +Bumped payload_data schema to 1-0-3 (#385) +Added Grunt task to build the tracker skipping the lodash and minification tasks (#382) +Added ability to configure the session cookie timeout in the argmap (#383) +Removed deprecated performanceTiming argument to trackPageView (#375) +Added ability to pass a context-generating function to trackPageView (#372) +Removed configWriteCookies setting (#390) +Updated browser feature detection tests (#378) + +Version 2.4.3 (2015-04-15) +-------------------------- +Added License button to README (#357) +Set the ID cookie as soon as the tracker loads (#358) +Updated the session count as soon as the tracker loads (#361) +Made single events exceeding the maximum POST request size attempt to fire exactly once (#359) +Fixed querystring decoration for links with inner elements (#360) + +Version 2.4.2 (2015-04-07) +-------------------------- +Set a maximum size for POST requests (#353) +Fixed QuotaExceededError bug (#352) + +Version 2.4.1 (2015-03-27) +-------------------------- +Counted any 2xx or 3xx collector response to a POST request as successful (#343) +Counted any 4xx or 5xx collector response to a POST request as failed (#344) +Prevented the localStorage event buffer from being flushed more than once simultaneously (#345) +Cancelled the XMLHttpRequest timeout callback when the request fails (#348) +Stopped adding null PerformanceTiming context (#354) + +Version 2.4.0 (2015-03-16) +-------------------------- +Added ability to modify links allowing cross-domain tracking (#109) +Added timing event (#320) +Increased safety of document size detection (#334) +Started randomly generating ngrok subdomain in integration tests (#333) +Fixed Vagrant setup to use latest Peru version (#336) +Stopped caching page URL and referrer URL (#337) +Stopped caching PerformanceTiming context (#339) +Added common contexts to link_click, change_form, and submit_form events (#340) + +Version 2.3.0 (2015-03-03) +------------------------- +Added support for sending events via POST (#168) +Removed dependency on fblundun fork of grunt-yui-compressor (#172) +Added support for batching events (#186) +Started sending PerformanceTiming context with all events (#317) +Added ability to send geolocation context with all events (#191) +Added ability to send context containing all GA cookies with all events (#253) +Improved document height detection (#236) +Added integration tests (#154) +Added functional tests for document size detection and browser features (#270) +Added ability to whitelist or blacklist specific forms and specific form fields (#287) +Added dedicated Vagrant setup (#312) +Added Vagrant "push core" to build and publish Tracker Core (#315) +Added Vagrant "push tracker" to build and deploy JavaScript Tracker (#313) +Renamed deploy to dist (#319) +Stopped sending NaN for page scroll offsets (#324) + +Version 2.2.2 (2015-03-03) +-------------------------- +Corrected time at which minimum and maximum scroll offsets are reset (#325) + +Version 2.2.1 (2015-01-28) +-------------------------- +Fixed QuotaExceededError bug with localStorage in Safari (#308) +Stopped sending empty PerformanceTiming context (#306) +Prevented PerformanceTiming context being sent multiple times (#309) +Stopped automatically setting Cloudfront URL in synchronous tracker, thanks @vassilevsky! (#311) +Fixed lodash-cli version at 2.4.2 (#314) +Prevented accidental publication of snowplow-tracker to npm (#300) +Added missing tid ticket to CHANGELOG under 2.1.0 (#302) + +Version 2.2.0 (2014-12-15) +-------------------------- +Made trackerDictionary object available in onload callbacks, thanks @murphybob! (#294) +Ensured all page offsets are integers (#291) +Added public method to get duid (#289) +Added public method to get user fingerprint (#288) +Added bundle.js to deploy/.gitignore (#281) +Started using grunt-cloudfront for cache invalidation (#276) +Added ability to disable use of localStorage (#181) +Added ability to disable cookies (#140) +URL encoded custom contexts if base 64 encoding is disabled (#299) + +Version 2.1.2 (2014-11-15) +-------------------------- +Removed requestEnd field from PerformanceTiming context (#285) + +Version 2.1.1 (2014-11-06) +-------------------------- +Rounded chromeFirstPaint field to an integer (#282) + +Version 2.1.0 (2014-11-05) +-------------------------- +Added automated form submission tracking (#252) +Stopped outbound queue from triggering multiple times per event (#251) +Added PerformanceTiming context using HTML5 Web Performance API (#248) +Added ability to execute a custom callback once sp.js is loaded (#246) +Added internal site search event (#241) +Started using grunt-cloudfront-clear for CloudFront cache invalidation (#230) +Renamed /dist to /deploy (#216) +Moved context querystring to end of JS-generated beacons (#204) +Added guard to wait until outbound queue is empty before unloading (#202) +Added event_id generation (#190) +Stopped sending tid (#218) +Added content field to link click events (#187) +Replaced "Getting started" with Vagrant-using "Contributing quickstart" (#169) +Added async-large.html (#162) +Improved F rating for tracker.js in CodeClimate (#150) +Added trackAddToCart and trackRemoveFromCart events (#97) +Added further Intern unit tests (#76) +Added social tracking features (#12) +Improved efficiency of enableLinkClickTracking (#254) +Integrated the Tracker Core (#255) +Removed deprecated trackImpression method (#256) +Added forceSecureTracker boolean option to the argmap, thanks @kujo4pmZ! (#247) +Moved link click tracking into its own file (#266) +Made IP address regex more strict (#267) +Updated expected browser fingerprints in functional detectors test (#275) +Added check to ensure outQueue is an array, thanks @kevinsimper! (#277) + +Version 2.0.2 (2014-10-20) +-------------------------- +Changed default configCookiePath to "/" (#250) + +Version 2.0.1 (2014-10-12) +-------------------------- +Made error logging compatible with Internet Explorer (#264) +Fixed SauceLabs red status (#235) + +Version 2.0.0 (2014-07-03) +-------------------------- +Moved fixUpUrl into its own file, called lib/proxies.js (#112) +Fixed duplication of querystring parameter lookup (#111) +Added tests for helpers.js (#96) +Added tests for detectors.js (#95) +Replaced cookie.js with browser-cookie-lite (#88) +Added ad conversion tracking (#60) +Added ad click tracking (#59) +Added initial localStorage support for intermittent offline beacons, thanks @rcs! (#24) +Added new trackAdImpression, mapping to unstructured event (#13) +Removed references to referral cookie (#118) +Implemented enableLinkTracking support (#51) +Replaced hard-coded version with template value (#120) +Added Sauce Labs small button at top of README (#123) +Added Sauce full test summary widget (long bar) at bottom of README (#124) +Added support for namespacing (#4) +Passed tracker namespace through to collector in Tracker Protocol (#126) +Moved to argmap-style tracker creation with 'newTracker' (#132) +Added support for cookie namespacing (#131) +Added new tag which allows queue to be renamed (#130) +Started rigorously checking whether a page is cached by Yahoo (#142) +Upgraded Intern to 1.5.0 (#119) +Fixed link to code climate button in README.md (#149) +Added examples of tracker namespacing (#159) +Split async.html into async-small.html, async-medium.html (#160) +Linked the Technical Docs and Setup Guide images to the appropriate pages (#164) +Made JS invocation tag part of the build process (#158) +Fixed warnings generated by the Closure Compiler, thanks @steve-gh! (#170) +Added untracked files which should be ignored to .gitignore (#173) +Removed ads/sync.html (#182) +Updated ads/async.html (#183) +Added pageUnloadTimer option to argmap (#171) +Removed type hints from unstructured events and custom contexts (#163) +Added hardcoded schema to custom context arrays (#199) +Added hardcoded schema to unstructured events (#196) +Changed trackUnstructEvent to take a JSON containing schema and data fields (#197) + +Version 1.0.3 (2014-06-27) +-------------------------- +Changed Base64 encoding function to prevent character encoding errors, thanks @shermozle! (#231) + +Version 1.0.2 (2014-06-24) +-------------------------- +Added guard to prevent document size field from being set as "NaNxNaN" (#220) +Fixed Grunt publish tasks to build sp.js as well as upload it to S3 (#224) +Added cache control to Grunt upload for full semantic version (#225) + +Version 1.0.1 (2014-04-09) +-------------------------- +Fixed lodash.js to work in the presence of AMD modules (#165) +Added missing variable declarations (#166) + +Version 1.0.0 (2014-03-27) +-------------------------- +Added extra meta-data to package.json (#83) +Moved part of banner.js into Gruntfile with grunt-concat's banner option so its values are based on package.json (#82) +Started using Browserify for modules (#74) +Replaced some/all of lib/.js with modules (#7) +Added user fingerprinting on/off switch and configurable hash seed (#7) +Deprecated trackImpression (#66) +Removed attachUserId as fully deprecated now (#64) +Removed setSiteId as fully deprecated now (#63) +Removed getVisitor-Id, -Info as fully deprecated now (#62) +Removed trackEvent as fully deprecated now (#61) +Tightened public API for SnowPlow (#29) +Renamed SnowPlow everywhere to Snowplow (#69) +Prepended window. or SnowPlow.windowAlias. onto _snaq everywhere (#39) +Removed legacy Piwik plugin framework (#56) +Moved hasSessionStorage and hasLocalStorage into detectors.js (#91) +Wrote tests for AsyncQueueProxy (#100) +Added Travis CI to the project (#103) +Added a built with Grunt button to the README (#102) +Added codeclimate button to README (#137) +Added named Grunt tasks (#86) +Added Intern unit tests for payload.js (#5) +Replaced all functions in identifiers.js which are directly available from lodash (#85) +Moved functions from identifers.js into payload.js and wrote Intern tests for them (#108) +Added getting started info for developers to README, thanks @pkallos! (#129) + +Version 0.14.1 (2014-03-12) +--------------------------- +Fixed bug where fromQuerystring was matching fragments instead of just the querystring (#116) + +Version 0.14.0 (2014-02-12) +--------------------------- +Bumped version to 0.14.0 +Removed all DEBUG blocks from codebase (#65) +Renamed requestStringBuilder to payloadBuilder and moved it into its own file, payload.js (#55) +Introduced gzipped sp.js library (#48) +Updated grunt and intern dependencies (#54) +Replaced snowpak.sh with Grunt and grunt-yui-compressor (#53) +Added setUserIdFromReferrer and setUserIdFromLocation (#57) +Added ability to pass a referrer to Snowplow from an IFRAME (#1) +Tested setDoNotTrack and renamed it to respectDoNotTrack (#28) +Moved detect...() functions into new file context.js (#37) +Moved cookie-related functionality into new file cookie.js (#77) +Removed getLegacyCookieName as no longer needed for migrating cookie IDs (#50) +Switched deployment to use Grunt (#58) +Added setUserIdFromCookie (#78) + +Version 0.13.1 (2014-01-28) +--------------------------- +Fixed bug where non-String values are not being added to our payload (#71) + +Version 0.13.0 (2014-01-26) +--------------------------- +Added fully retrospective CHANGELOG (#20) +Added setPlatform support, thanks @rcs! (#25) +Added currency field to ecommerce transactions (#34) +Added custom unstructured contexts (#49) +Added base64decode to Tracker (#36) +Added null check to requestStringBuilder() (#40) +Added array helpers (#41) +Fixed (harmless) bug in base64.js (#35) +Update .gitignore to be node-friendly .gitignore (#52) +Switched to Semantic versioning & only put MAJOR version in hosted path to snowplow.js (#47) +Added package.json (#38) +Added retrospective tags back in (#22) +Restructured folders (#21) + +Version 0.12.0 (2013-07-07) +--------------------------- +Fixed document reference to use documentAlias (#247) +Fixed bug with setCustomUrl (#267) +Changed ev_ to se_ for structured events (#197) +Fixed Firefox failure when "Always ask" set for cookies (#163) +Fixed bug in page ping functionality detected in IE 8 (#260) +Replaced forEach as not supported in IE 6-8 (#295) + +Version 0.11.2 (2013-05-14) +--------------------------- +Added unstructured events, thanks @rgabo, @tarsolya, @lackac (#198) +Remove leading ampersand in querystring (#188) + +Version 0.11.1 (2013-02-25) +--------------------------- +Fixed bug with cookie secure flag killing user ID cookies (#181) + +Version 0.11.0 (2013-02-22) +--------------------------- +Introduced setAppId() and deprecated setSiteId() (#168) +1st party user ID now transmitted as duid (domain uid) (part of #150) +Now sends dtm - the client timestamp (#149) +Deprecated and disabled attachUserId() +Deprecated getVisitorId() and getVisitorInfo() - use getDomainUserId() and getDomainUserInfo() instead +Added setUserId which sets the uid field (#167) +Snowplow cookies no longer tied to site ID (#148) + +Version 0.10.0 (2013-02-15) +--------------------------- +Updated copyright notices +Removed deprecated setAccount(), setTracker(), setHeartBeatTimer() - BREAKING CHANGE (#86) +Added document charset to querystring (#138) +Page ping no longer killed by 1 heartbeat w/o activity (#132) +Added document & viewport dimensions (#94) +Introduced trackStructEvent and deprecated trackEvent (#143) +Cleaned up getRequest code to use improved requestStringBuilder +Fixed logImpression (was using wrong argument names) (#162) +Added scroll offsets to page ping (#127) + +Version 0.9.1 (2013-01-29) +-------------------------- +Fixed bug where secure flag not being set on cookies sent via HTTPS + +Version 0.9.0 (2012-12-26) +-------------------------- +Each event now sent with an event type `e` (#63) +Refactoring of event definition code +Added attachUserId(boolean) method (#92) +Removed configCustomData from logImpression (#115) +Cleaned up activity tracking (page pings) +Added a combine only option to snowpak.sh + +Version 0.8.2 (2012-12-18) +-------------------------- +Fixed regressions from splitting JS into multiple files (#103) + +Version 0.8.1 (2012-11-29) +-------------------------- +Fixed bug with trailing comma (#102) +Removed console.log when not debugging (#101) +Removed minified sp.js from version control (added .gitignore to keep it out) + +Version 0.8.0 (2012-11-28) +-------------------------- +Rename ice.png to i - BREAKING CHANGE (#29) +Added setCollectorCf() and deprecated setAccount() (#32) +Tracker constructor now supports Cf or Url (part of #44) +getTrackerCf() and -Url() added, getTracker() deprecated (part of #44) +Added tracker version (`tv`) to querystring (#41) +Added color depth tracking (part of #69) +Added timezone tracking (part of #69) +Added user fingerprinting (#70) +Broke out .js into multiple files (#55) + +Version 0.7.0 (2012-10-01) +-------------------------- +Renamed said to aid for application ID + +Version 0.6 (2012-09-05) +------------------------ +Added setSiteId functionality +Added ecommerce tracking + +Version 0.5 (2012-08-18) +-------------------------- +Changed header comments from Doxygen format to JsDoc +Added support for specifying collectorUrl directly +Added versioning into header comment (so survives minification) +Took f_ off res and cookie; added url onto end, renamed rdm to tid + +Version 0.4 (2012-05-30) +------------------------ +Improved names of querystring params +Added page-url to querystring as fallback + +Version 0.3 (2012-05-18) +------------------------ +Updated to prepend f_ to browser features +Revised the querystring name-value pairs to make them more user-friendly + +Version 0.2 (2012-05-08) +------------------------ +Formalised minification process + +Version 0.1 (2012-03-21) +------------------------ +Initial release diff --git a/snowplow-javascript-tracker/Gruntfile.js b/snowplow-javascript-tracker/Gruntfile.js new file mode 100644 index 0000000000..4056f3e185 --- /dev/null +++ b/snowplow-javascript-tracker/Gruntfile.js @@ -0,0 +1,279 @@ +/* +* JavaScript tracker for Snowplow: Gruntfile.js +* +* Significant portions copyright 2010 Anthon Pang. Remainder copyright +* 2012-2014 Snowplow Analytics Ltd. All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are +* met: +* +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* +* * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the +* names of their contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +var semver = require('semver'); + +/*global module:false*/ +module.exports = function(grunt) { + + var pkg = grunt.file.readJSON('package.json'); + var semVer = semver.parse(pkg.version); + pkg.pinnedVersion = semVer.major; + var banner = "/*\n" + + " * Snowplow - The world's most powerful web analytics platform\n" + + " *\n" + + " * @description <%= pkg.description %>\n" + + " * @version <%= pkg.version %>\n" + + " * @author " + pkg.contributors.join(', ') +"\n" + + " * @copyright Anthon Pang, Snowplow Analytics Ltd\n" + + " * @license <%= pkg.license %>\n" + + " *\n" + + " * For technical documentation:\n" + + " * https://github.com/snowplow/snowplow/wiki/javascript-tracker\n" + + " *\n" + + " * For the setup guide:\n" + + " * https://github.com/snowplow/snowplow/wiki/javascript-tracker-setup\n" + + " *\n" + + " * Minimum supported browsers:\n" + + " * - Firefox 27 \n" + + " * - Chrome 32 \n" + + " * - IE 9 \n" + + " * - Safari 8 \n" + + " */\n"; + + grunt.initConfig({ + + banner: banner, + + pkg: pkg, + + subdomain: process.env.SUBDOMAIN, + + browserify: { + main: { + files: { + 'dist/bundle.js': ['src/js/init.js'] + } + }, + test: { + files: { + 'tests/pages/helpers.bundle.js': ['tests/scripts/helpers.js'], + 'tests/pages/detectors.bundle.js': ['tests/scripts/detectors.js'], + 'tests/pages/bundle.js': ['src/js/init.js'] + } + } + }, + + babel: { + options: { + presets: ['@babel/preset-env'] + }, + dist: { + files: { + 'dist/bundle-postbabel.js': 'dist/bundle.js' + } + }, + test: { + files: { + 'tests/pages/helpers.js': 'tests/pages/helpers.bundle.js', + 'tests/pages/detectors.js': 'tests/pages/detectors.bundle.js', + 'tests/pages/snowplow.js': 'tests/pages/bundle.js' + } + }, + local: { + files: { + 'tests/local/serve/snowplow.js': 'tests/pages/bundle.js' + } + } + }, + + concat: { + deploy: { + options: { + 'report': 'gzip', + 'process': true + }, + src: ['dist/bundle-postbabel.js'], + dest: 'dist/snowplow.js' + }, + tag: { + options: { + banner: ';' + }, + src: ['tags/tag.min.js'], + dest: 'tags/tag.min.js' + }, + test: { + options: { + 'process': true + }, + src: ['tests/pages/integration-template.html'], + dest: 'tests/pages/integration.html' + }, + local: { + options: { + 'process': function(src, filepath) { + return src.replace(/'\<\%= subdomain \%\>' \+ '\.ngrok\.io'/g, '\'127.0.0.1:8000\''); + } + }, + src: ['tests/pages/integration-template.html'], + dest: 'tests/local/serve/integration.html' + } + }, + + uglify: { + deploy: { + options: { + 'banner': '<%= banner %>' + }, + files: { + 'dist/sp.js': ['dist/snowplow.js'] + } + }, + tag: { + files: { + 'tags/tag.min.js': ['tags/tag.js'] + } + } + }, + + intern: { + // Common + options: { + config: 'tests/intern.js' + }, + + nonfunctional: { + options: { + runType: 'client', + suites: [ + 'tests/nonfunctional/helpers.js', + 'tests/nonfunctional/in_queue.js', + 'tests/nonfunctional/proxies.js' + ] + } + }, + functional: { + options: { + runType: 'runner', + functionalSuites: [ + 'tests/functional/detectors.js', + 'tests/functional/helpers.js' + ] + } + }, + integration: { + options: { + runType: 'runner', + functionalSuites: [ + 'tests/integration/setup.js', // required prior to integration.js + 'tests/integration/integration.js' // request_recorder and ngrok need to be running + ] + } + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-aws'); + grunt.loadNpmTasks('grunt-browserify'); + grunt.loadNpmTasks('intern'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-babel'); + + grunt.registerTask('upload_setup', 'Read aws.json and configure upload tasks', function() { + var aws = grunt.file.readJSON('aws.json'); + + grunt.config('aws', aws); + + grunt.config('s3', { + options: { + accessKeyId: '<%= aws.key %>', + secretAccessKey: '<%= aws.secret %>', + bucket: '<%= aws.bucket %>', + access: 'public-read', + region: '<%= aws.region %>', + gzip: true, + cache: false + }, + not_pinned: { + options: { + headers: { + CacheControl: "max-age=315360000" + } + }, + files: [ + { + src: ["dist/sp.js"], + dest: "<%= pkg.version %>/sp.js" + } + ] + }, + pinned: { + options: { + headers: { + CacheControl: "max-age=3600" + } + }, + files: [ + { + src: ["dist/sp.js"], + dest: "<%= pkg.pinnedVersion %>/sp.js" + } + ] + } + }); + + grunt.config('cloudfront', { + options: { + accessKeyId: '<%= aws.key %>', + secretAccessKey: '<%= aws.secret %>', + distributionId: '<%= aws.distribution %>' + }, + not_pinned: { + options: { + invalidations: [ + '/<%= pkg.version %>/sp.js' + ] + } + }, + pinned: { + options: { + invalidations: [ + '/<%= pkg.pinnedVersion %>/sp.js' + ] + } + } + }); + }); + + grunt.registerTask('default', 'Build Browserify, add banner, and minify', ['browserify:main', 'babel:dist', 'concat:deploy', 'uglify:deploy']); + grunt.registerTask('publish', 'Upload to S3 and invalidate Cloudfront (full semantic version only)', ['upload_setup', 'browserify:main', 'babel:dist', 'concat:deploy', 'uglify:deploy', 's3:not_pinned', 'cloudfront:not_pinned']); + grunt.registerTask('publish-pinned', 'Upload to S3 and invalidate Cloudfront (full semantic version and semantic major version)', ['upload_setup', 'browserify:main', 'babel:dist', 'concat:deploy', 'uglify:deploy', 's3', 'cloudfront']); + grunt.registerTask('quick', 'Build snowplow.js, skipping building and minifying', ['browserify:main', 'babel:dist', 'concat:deploy']); + grunt.registerTask('test', 'Intern tests', ['browserify:test', 'babel:test', 'intern']); + grunt.registerTask('travis', 'Intern tests for Travis CI', ['concat:test', 'browserify:test', 'babel:test', 'intern']); + grunt.registerTask('tags', 'Minifiy the Snowplow invocation tag', ['uglify:tag', 'concat:tag']); + grunt.registerTask('local', 'Builds and places files read to serve and test locally', ['browserify:test', 'concat:local', 'babel:local']); +}; diff --git a/snowplow-javascript-tracker/LICENSE.txt b/snowplow-javascript-tracker/LICENSE.txt new file mode 100644 index 0000000000..1a6b140d71 --- /dev/null +++ b/snowplow-javascript-tracker/LICENSE.txt @@ -0,0 +1,29 @@ +Significant portions copyright 2010 Anthon Pang. Remainder copyright +2012 Snowplow Analytics Ltd. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + names of their contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/snowplow-javascript-tracker/README.md b/snowplow-javascript-tracker/README.md new file mode 100644 index 0000000000..00d2f4f667 --- /dev/null +++ b/snowplow-javascript-tracker/README.md @@ -0,0 +1,88 @@ +# JavaScript web analytics for Snowplow + +[![Build Status][travis-image]][travis] +[![Selenium Test Status][saucelabs-button-image]][saucelabs] +[![Code Climate][codeclimate-image]][codeclimate] +[![Built with Grunt][grunt-image]][grunt] +[![License][license-image]][bsd] +[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/gh/snowplow/snowplow-javascript-tracker/badge?style=rounded)](https://www.jsdelivr.com/package/gh/snowplow/snowplow-javascript-tracker) + +## Overview + +Add analytics to your websites and web apps with the [Snowplow][snowplow] event tracker for +JavaScript. + +With this tracker you can collect user event data (page views, e-commerce transactions etc) from the +client-side tier of your websites and web apps. + +## Find out more + +| Technical Docs | Setup Guide | Roadmap & Contributing | +|-------------------------------------|------------------------------|--------------------------------------| +| [![i1][techdocs-image]][tech-docs] | [ ![i2][setup-image]][setup] | ![i3][roadmap-image] | +| [Technical Docs][tech-docs] | [Setup Guide][setup] | _coming soon_ | + + +## Developers + +### Contributing quickstart + +Assuming git, [Vagrant][vagrant-install] and [VirtualBox][virtualbox-install] installed: + +``` + host$ git clone https://github.com/snowplow/snowplow-javascript-tracker.git + host$ cd snowplow-javascript-tracker + host$ vagrant up && vagrant ssh +guest$ cd /vagrant +guest$ sudo npm install +guest$ cd core +guest$ sudo npm install +``` + +Set up an `./aws.json` file using the example `./aws.sample.json`. If you just want to concat + +minify without uploading then you don't need to fill out the `aws.json` file with valid credentials. + +Build the package (default task concatenates and minifies) using `grunt`. + +## Testing + +[![Selenium Test Status][saucelabs-matrix-image]][saucelabs] + +## Copyright and license + +The Snowplow JavaScript Tracker is based on Anthon Pang's [`piwik.js`][piwikjs], the JavaScript +tracker for the open-source [Piwik][piwik] project, and is distributed under the same license +([Simplified BSD][bsd]). + +Significant portions of the Snowplow JavaScript Tracker copyright 2010 Anthon Pang. Remainder +copyright 2012-14 Snowplow Analytics Ltd. + +Licensed under the [Simplified BSD][bsd] license. + +[snowplow]: http://snowplowanalytics.com/ + +[vagrant-install]: http://docs.vagrantup.com/v2/installation/index.html +[virtualbox-install]: https://www.virtualbox.org/wiki/Downloads + +[piwik]: http://piwik.org/ +[piwikjs]: https://github.com/piwik/piwik/blob/master/js/piwik.js +[piwikphp]: https://github.com/piwik/piwik/blob/master/piwik.php +[bsd]: http://www.opensource.org/licenses/bsd-license.php +[integrating]: /snowplow/snowplow/blob/master/docs/03_integrating_snowplowjs.md +[selfhosting]: /snowplow/snowplow/blob/master/docs/04_selfhosting_snowplow.md +[setup]: https://github.com/snowplow/snowplow/wiki/javascript-tracker-setup +[integrating-js-on-website]: https://github.com/snowplow/snowplow/wiki/integrating-javascript-tags-onto-your-website +[tech-docs]: https://github.com/snowplow/snowplow/wiki/javascript-tracker +[techdocs-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/techdocs.png +[setup-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/setup.png +[roadmap-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/roadmap.png +[grunt-image]: https://cdn.gruntjs.com/builtwith.png +[grunt]: http://gruntjs.com/ +[travis-image]: https://travis-ci.org/snowplow/snowplow-javascript-tracker.png?branch=master +[travis]: http://travis-ci.org/snowplow/snowplow-javascript-tracker +[codeclimate-image]: https://codeclimate.com/github/snowplow/snowplow-javascript-tracker.png +[codeclimate]: https://codeclimate.com/github/snowplow/snowplow-javascript-tracker +[saucelabs]: https://saucelabs.com/u/snowplow +[saucelabs-button-image]: https://saucelabs.com/buildstatus/snowplow +[saucelabs-matrix-image]: https://saucelabs.com/browser-matrix/snowplow.svg +[license-image]: http://img.shields.io/badge/license-simplified--bsd-blue.svg?style=flat diff --git a/snowplow-javascript-tracker/Vagrantfile b/snowplow-javascript-tracker/Vagrantfile new file mode 100644 index 0000000000..8d19a0ef3b --- /dev/null +++ b/snowplow-javascript-tracker/Vagrantfile @@ -0,0 +1,29 @@ +Vagrant.configure("2") do |config| + + config.vm.box = "ubuntu/trusty64" + config.vm.hostname = "snowplow-javascript-tracker" + config.ssh.forward_agent = true + + config.vm.provider :virtualbox do |vb| + vb.name = Dir.pwd().split("/")[-1] + "-" + Time.now.to_f.to_i.to_s + vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] + vb.customize [ "guestproperty", "set", :id, "--timesync-threshold", 10000 ] + # node is not particularly memory hungry + vb.memory = 1024 + end + + config.vm.provision :shell do |sh| + sh.path = "vagrant/up.bash" + end + + # Requires Vagrant 1.7.0+ + config.push.define "push-tracker", strategy: "local-exec" do |push| + push.script = "vagrant/push/publish-tracker.bash" + end + + # Requires Vagrant 1.7.0+ + config.push.define "push-core", strategy: "local-exec" do |push| + push.script = "vagrant/push/publish-core.bash" + end + +end diff --git a/snowplow-javascript-tracker/aws.sample.json b/snowplow-javascript-tracker/aws.sample.json new file mode 100644 index 0000000000..6045e257aa --- /dev/null +++ b/snowplow-javascript-tracker/aws.sample.json @@ -0,0 +1,7 @@ +{ + "key": "ADD HERE", + "secret": "ADD HERE", + "bucket": "ADD HERE", + "region": "ADD HERE", + "distribution": "ADD HERE" +} diff --git a/snowplow-javascript-tracker/core/.gitignore b/snowplow-javascript-tracker/core/.gitignore new file mode 100644 index 0000000000..9190bf4660 --- /dev/null +++ b/snowplow-javascript-tracker/core/.gitignore @@ -0,0 +1,4 @@ +index.js +lib/*.js +lib/*.js.map +.tscache diff --git a/snowplow-javascript-tracker/core/.npmignore b/snowplow-javascript-tracker/core/.npmignore new file mode 100644 index 0000000000..8f33598edf --- /dev/null +++ b/snowplow-javascript-tracker/core/.npmignore @@ -0,0 +1,2 @@ +tests/ +Gruntfile.js diff --git a/snowplow-javascript-tracker/core/CHANGELOG b/snowplow-javascript-tracker/core/CHANGELOG new file mode 100644 index 0000000000..4652668569 --- /dev/null +++ b/snowplow-javascript-tracker/core/CHANGELOG @@ -0,0 +1,28 @@ +Version 0.4.0 (2014-12-03) +-------------------------- +Updated contexts schema to 1-0-1 (#292) +Added ability to send an empty contexts array (#296) + +Version 0.3.0 (2014-11-04) +-------------------------- +Applied callback to the Payload for an event rather than the event dictionary (#259) +Added page scroll parameters to trackPagePing (#257) +Added social tracking (#258) +Added trackAddToCart and trackRemoveFromCart methods (#260) +Added trackFormChange and trackFormSubmission methods (#261) +Added trackSiteSearch method (#263) +Added content field to trackLinkClick (#262) + +Version 0.2.0 (2014-08-07) +-------------------------- +Added UUID to payload (#244) +Added automatic timestamp generation (#243) +Added setter methods (#242) +Added timestamp parameter to tracker methods (#240) +Added dependendencies field to package.json (#239) +Added npm button to README (#238) +Updated README (#237) + +Version 0.1.0 (2014-08-01) +-------------------------- +Initial release diff --git a/snowplow-javascript-tracker/core/Gruntfile.js b/snowplow-javascript-tracker/core/Gruntfile.js new file mode 100644 index 0000000000..497955d154 --- /dev/null +++ b/snowplow-javascript-tracker/core/Gruntfile.js @@ -0,0 +1,58 @@ +/* +* JavaScript tracker core for Snowplow: Gruntfile.js +* + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +/*global module:false*/ +module.exports = function (grunt) { + + grunt.initConfig({ + + ts: { + default: { + tsconfig: true + } + }, + + dtsGenerator: { + default: { + options: { + name: 'snowplow-tracker', + project: '.', + out: 'main.d.ts' + } + } + }, + + intern: { + unit: { + options: { + runType: 'client', + config: 'tests/intern.js', + suites: [ + 'tests/unit/base64.js', + 'tests/unit/payload.js', + 'tests/unit/core.js', + 'tests/unit/contexts.js' + ] + } + } + } + }); + + grunt.loadNpmTasks('intern'); + grunt.loadNpmTasks('grunt-ts'); + grunt.loadNpmTasks('dts-generator'); + + grunt.registerTask('default', 'Compile and test', ['ts', 'dtsGenerator', 'intern']); +}; diff --git a/snowplow-javascript-tracker/core/README.md b/snowplow-javascript-tracker/core/README.md new file mode 100644 index 0000000000..912415f844 --- /dev/null +++ b/snowplow-javascript-tracker/core/README.md @@ -0,0 +1,132 @@ +# Snowplow JavaScript Tracker Core [![npm version][npm-image]][npm-url] + +Core module to be used by all Snowplow JavaScript trackers. + +## Installation + +With npm: + +```bash +npm install snowplow-tracker-core +``` + +## Example + +```js +var core = require('snowplow-tracker-core'); + +// Create an instance with base 64 encoding set to false (it defaults to true) +var coreInstance = core(false); + +// Add a name-value pair to all payloads +coreInstance.addPayloadPair('vid', 2); + +// Add each name-value pair in a dictionary to all payloads +coreInstance.addPayloadDict({ + 'ds': '1160x620', + 'fp': 4070134789 +}); + +// Add name-value pairs to all payloads using convenience methods +coreInstance.setTrackerVersion('js-3.0.0'); +coreInstance.setPlatform('web'); +coreInstance.setUserId('user-321'); +coreInstance.setColorDepth(24); +coreInstance.setViewport(600, 400); + +// Track a page view with URL and title +var pageViewPayload = coreInstance.trackPageView('http://www.example.com', 'landing page'); + +console.log(pageViewPayload); +/* +{ + 'e': 'pv', + 'url': 'http://www.example.com', + 'page': 'landing page', + 'uid': 'user-321', + 'vd': 2, + 'ds': '1160x620', + 'fp': 4070134789 + 'tv': 'js-3.0.0', + 'p': 'web', + 'cd': 24, + 'vp': '600x400', + 'dtm': 1406879959702, // timestamp + 'eid': '0718a85a-45dc-4f71-a949-27870442ed7d' // UUID +} +*/ + +// Stop automatically adding tv, p, and dtm to the payload +coreInstance.resetPayloadPairs(); + +// Track an unstructured event +var unstructEventPayload = coreInstance.trackUnstructEvent({ + 'schema': 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-0', + 'data': { + 'targetUrl': 'http://www.destination.com', + 'elementId': 'bannerLink' + } +}); + +console.log(unstructEventPayload); +/* +{ + 'e': 'ue', + 'ue_pr': { + 'schema': 'iglu:com.snowplowanalytics.snowplow/unstruct_even/jsonschema/1-0-0', + 'data': { + 'schema': 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-0', + 'data': { + 'targetUrl': 'http://www.destination.com', + 'elementId': 'bannerLink' + } + } + }, + 'dtm': 1406879973439, + 'eid': '956c6670-cbf6-460b-9f96-143e0320fdf6' +} +*/ +``` + +## Other features + +Core instances can be initialized with two parameters. The first is a boolean and determines whether custom contexts and unstructured events will be base 64 encoded. The second is an optional callback function which gets applied to every payload created by the instance. + +```js +var coreInstance = core(true, console.log); +``` + +The above example would base 64 encode all unstructured events and custom contexts and would log each payload to the console. + +Use the `setBase64Encoding` method to turn base 64 encoding on or off after initializing a core instance: + +```js +var core = require('snowplow-tracker-core'); + +var coreInstance = core(); // Base 64 encoding on by default + +coreInstance.setBase64Encoding(false); // Base 64 encoding is now off +``` + +## Documentation + +For more information on the Snowplow JavaScript Tracker Core's API, view its [wiki page][wiki]. + +## Copyright and license + +The Snowplow JavaScript Tracker Core is copyright 2014 Snowplow Analytics Ltd. + +Licensed under the [Apache License, Version 2.0][apache-license] (the "License"); +you may not use this software except in compliance with the License. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +[apache-license]: http://www.apache.org/licenses/LICENSE-2.0 + +[npm-url]: http://badge.fury.io/js/snowplow-tracker-core +[npm-image]: https://badge.fury.io/js/snowplow-tracker-core.svg +[wiki]: https://github.com/snowplow/snowplow/wiki/Javascript-Tracker-Core diff --git a/snowplow-javascript-tracker/core/index.ts b/snowplow-javascript-tracker/core/index.ts new file mode 100644 index 0000000000..56cc0e8bfd --- /dev/null +++ b/snowplow-javascript-tracker/core/index.ts @@ -0,0 +1,16 @@ +/* + * JavaScript tracker core for Snowplow: index.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +export {trackerCore} from "./lib/core"; diff --git a/snowplow-javascript-tracker/core/lib/base64.ts b/snowplow-javascript-tracker/core/lib/base64.ts new file mode 100644 index 0000000000..53ade43415 --- /dev/null +++ b/snowplow-javascript-tracker/core/lib/base64.ts @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) + * and Contributors (http://phpjs.org/authors) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Deprecated and removed in TypeScript +declare function unescape(s: string): string; + +export function base64urldecode(data: string): string { + if (!data) { + return data; + } + var padding = 4 - data.length % 4; + switch (padding) { + case 2: + data += "=="; + break; + case 3: + data += "="; + break; + } + var b64Data = data.replace(/-/g, '+').replace(/_/g, '/'); + return base64decode(b64Data); +} + +/** + * Encode string as base64. + * Any type can be passed, but will be stringified + * + * @param data string to encode + * @returns base64-encoded string + */ +export function base64encode(data: string): string { + // discuss at: http://phpjs.org/functions/base64_encode/ + // original by: Tyler Akins (http://rumkin.com) + // improved by: Bayron Guevara + // improved by: Thunder.m + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Rafał Kukawski (http://kukawski.pl) + // bugfixed by: Pellentesque Malesuada + // example 1: base64_encode('Kevin van Zonneveld'); + // returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // example 2: base64_encode('a'); + // returns 2: 'YQ==' + // example 3: base64_encode('✓ à la mode'); + // returns 3: '4pyTIMOgIGxhIG1vZGU=' + + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc: string, + tmp_arr: Array = []; + + if (!data) { + return data; + } + + data = unescape(encodeURIComponent(data)); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + var r = data.length % 3; + + return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); +} + +export function base64decode(encodedData:string): string { + // discuss at: http://locutus.io/php/base64_decode/ + // original by: Tyler Akins (http://rumkin.com) + // improved by: Thunder.m + // improved by: Kevin van Zonneveld (http://kvz.io) + // improved by: Kevin van Zonneveld (http://kvz.io) + // input by: Aman Gupta + // input by: Brett Zamir (http://brett-zamir.me) + // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman) + // bugfixed by: Pellentesque Malesuada + // bugfixed by: Kevin van Zonneveld (http://kvz.io) + // improved by: Indigo744 + // example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==') + // returns 1: 'Kevin van Zonneveld' + // example 2: base64_decode('YQ==') + // returns 2: 'a' + // example 3: base64_decode('4pyTIMOgIGxhIG1vZGU=') + // returns 3: '✓ à la mode' + + // decodeUTF8string() + // Internal function to decode properly UTF8 string + // Adapted from Solution #1 at https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + var decodeUTF8string = function (str) { + // Going backwards: from bytestream, to percent-encoding, to original string. + return decodeURIComponent(str.split('').map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }).join('')) + }; + + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + dec = '', + tmpArr: Array = []; + + if (!encodedData) { + return encodedData; + } + + encodedData += ''; + + do { + // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(encodedData.charAt(i++)); + h2 = b64.indexOf(encodedData.charAt(i++)); + h3 = b64.indexOf(encodedData.charAt(i++)); + h4 = b64.indexOf(encodedData.charAt(i++)); + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + + o1 = bits >> 16 & 0xff; + o2 = bits >> 8 & 0xff; + o3 = bits & 0xff; + + if (h3 === 64) { + tmpArr[ac++] = String.fromCharCode(o1); + } else if (h4 === 64) { + tmpArr[ac++] = String.fromCharCode(o1, o2); + } else { + tmpArr[ac++] = String.fromCharCode(o1, o2, o3); + } + } while (i < encodedData.length); + + dec = tmpArr.join(''); + + return decodeUTF8string(dec.replace(/\0+$/, '')); +} + diff --git a/snowplow-javascript-tracker/core/lib/contexts.ts b/snowplow-javascript-tracker/core/lib/contexts.ts new file mode 100644 index 0000000000..90ea1595af --- /dev/null +++ b/snowplow-javascript-tracker/core/lib/contexts.ts @@ -0,0 +1,492 @@ +/* + * JavaScript tracker core for Snowplow: contexts.js + * + * Copyright (c) 2014-2018 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +import { PayloadData, isNonEmptyJson } from "./payload"; +import { SelfDescribingJson } from "./core"; +import { base64urldecode } from "./base64"; +import isEqual = require('lodash/isEqual'); +import has = require('lodash/has'); +import get = require('lodash/get'); +import isPlainObject = require('lodash/isPlainObject'); +import every = require('lodash/every'); +import compact = require('lodash/compact'); +import map = require('lodash/map'); + +/** + * Datatypes (some algebraic) for representing context types + */ + +/** + * A context generator is a callback that returns a self-describing JSON + * @param {Object} args - Object that contains: event, eventType, eventSchema + * @return {SelfDescribingJson} A self-describing JSON + */ +export type ContextGenerator = (args?: Object) => SelfDescribingJson; + +/** + * A context primitive is either a self-describing JSON or a context generator + */ +export type ContextPrimitive = SelfDescribingJson | ContextGenerator; + +/** + * A context filter is a user-supplied callback that is evaluated for each event + * to determine if the context associated with the filter should be attached to the event + */ +export type ContextFilter = (args?: Object) => boolean; + +/** + * A filter provider is an array that has two parts: a context filter and context primitives + * If the context filter evaluates to true, the tracker will attach the context primitive(s) + */ +export type FilterProvider = [ContextFilter, Array | ContextPrimitive]; + +/** + * A ruleset has accept or reject properties that contain rules for matching Iglu schema URIs + */ +export interface RuleSet { + accept?: string[] | string; + reject?: string[] | string; +} + +/** + * A ruleset provider is an array that has two parts: a ruleset and context primitives + * If the ruleset allows the current event schema URI, the tracker will attach the context primitive(s) + */ +export type RuleSetProvider = [RuleSet, Array | ContextPrimitive]; + +/** + * Conditional context providers are two element arrays used to decide when to attach contexts, where: + * - the first element is some conditional criterion + * - the second element is any number of context primitives + */ +export type ConditionalContextProvider = FilterProvider | RuleSetProvider; + +export function getSchemaParts(input: string): Array | undefined { + let re = new RegExp('^iglu:([a-zA-Z0-9-_\.]+)\/([a-zA-Z0-9-_]+)\/jsonschema\/([1-9][0-9]*)\-(0|[1-9][0-9]*)\-(0|[1-9][0-9]*)$'); + let matches = re.exec(input); + if (matches !== null) + return matches.slice(1, 6); + return undefined; +} + +export function validateVendorParts(parts: Array): boolean { + if (parts[0] === '*' || parts[1] === '*') { + return false; // no wildcard in first or second part + } + if (parts.slice(2).length > 0) { + let asterisk = false; + for (let part of parts.slice(2)) { + if (part === '*') // mark when we've found a wildcard + asterisk = true; + else if (asterisk) // invalid if alpha parts come after wildcard + return false; + } + return true; + } else if (parts.length == 2) + return true; + + return false; +} + +export function validateVendor(input: string): boolean { + let parts = input.split('.'); + if (parts && parts.length > 1) + return validateVendorParts(parts); + return false; +} + +export function getRuleParts(input: string): Array | undefined { + const re = new RegExp('^iglu:((?:(?:[a-zA-Z0-9-_]+|\\*)\.)+(?:[a-zA-Z0-9-_]+|\\*))\/([a-zA-Z0-9-_.]+|\\*)\/jsonschema\/([1-9][0-9]*|\\*)-(0|[1-9][0-9]*|\\*)-(0|[1-9][0-9]*|\\*)$'); + let matches = re.exec(input); + if (matches !== null && validateVendor(matches[1])) + return matches.slice(1,6); + return undefined; +} + +export function isValidRule(input: string): boolean { + let ruleParts = getRuleParts(input); + if (ruleParts) { + let vendor = ruleParts[0]; + return ruleParts.length === 5 && validateVendor(vendor); + } + return false; +} + +export function isStringArray(input: any): boolean { + return Array.isArray(input) && input.every((x) => { return typeof x === 'string' }); +} + +export function isValidRuleSetArg(input: any): boolean { + if (isStringArray(input)) + return input.every((x) => { return isValidRule(x) }); + else if (typeof input === 'string') + return isValidRule(input); + return false; +} + +export function isSelfDescribingJson(input: any) : boolean { + if (isNonEmptyJson(input)) + if ('schema' in input && 'data' in input) + return (typeof(input.schema) === 'string' && typeof(input.data) === 'object'); + return false; +} + +export function isEventJson(input: any) : boolean { + if (isNonEmptyJson(input) && ('e' in input)) + return (typeof(input.e) === 'string'); + return false; +} + +export function isRuleSet(input: any) : boolean { + let ruleCount = 0; + if (isPlainObject(input)) { + if (has(input, 'accept')) { + if (isValidRuleSetArg(input['accept'])) { + ruleCount += 1; + } else { + return false; + } + } + if (has(input, 'reject')) { + if (isValidRuleSetArg(input['reject'])) { + ruleCount += 1; + } else { + return false; + } + } + // if either 'reject' or 'accept' or both exists, + // we have a valid ruleset + return ruleCount > 0 && ruleCount <= 2; + } + return false; +} + +export function isContextGenerator(input: any) : boolean { + return typeof(input) === 'function' && input.length <= 1; +} + +export function isContextFilter(input: any) : boolean { + return typeof(input) === 'function' && input.length <= 1; +} + +export function isContextPrimitive(input: any) : boolean { + return (isContextGenerator(input) || isSelfDescribingJson(input)); +} + +export function isFilterProvider(input: any) : boolean { + if (Array.isArray(input)) { + if (input.length === 2) { + if (Array.isArray(input[1])) { + return isContextFilter(input[0]) && every(input[1], isContextPrimitive); + } + return isContextFilter(input[0]) && isContextPrimitive(input[1]); + } + } + return false; +} + +export function isRuleSetProvider(input: any) : boolean { + if (Array.isArray(input) && input.length === 2) { + if (!isRuleSet(input[0])) + return false; + if (Array.isArray(input[1])) + return every(input[1], isContextPrimitive); + return isContextPrimitive(input[1]); + } + return false; +} + +export function isConditionalContextProvider(input: any) : boolean { + return isFilterProvider(input) || isRuleSetProvider(input); +} + +export function matchSchemaAgainstRule(rule: string, schema: string) : boolean { + if (!isValidRule(rule)) + return false; + let ruleParts = getRuleParts(rule); + let schemaParts = getSchemaParts(schema); + if (ruleParts && schemaParts) { + if (!matchVendor(ruleParts[0], schemaParts[0])) + return false; + for (let i=1; i<5; i++) { + if (!matchPart(ruleParts[i], schemaParts[i])) + return false; + } + return true; // if it hasn't failed, it passes + } + return false; +} + +export function matchVendor(rule: string, vendor: string): boolean { + // rule and vendor must have same number of elements + let vendorParts = vendor.split('.'); + let ruleParts = rule.split('.'); + if (vendorParts && ruleParts) { + if (vendorParts.length !== ruleParts.length) + return false; + for (let i=0; i).some((rule) => (matchSchemaAgainstRule(rule, schema)))) { + acceptCount++; + } + } else if (typeof(acceptRules) === 'string') { + if (matchSchemaAgainstRule(acceptRules, schema)) { + acceptCount++; + } + } + + let rejectRules = get(ruleSet, 'reject'); + if (Array.isArray(rejectRules)) { + if ((ruleSet.reject as Array).some((rule) => (matchSchemaAgainstRule(rule, schema)))) { + rejectCount++; + } + } else if (typeof(rejectRules) === 'string') { + if (matchSchemaAgainstRule(rejectRules, schema)) { + rejectCount++; + } + } + + if (acceptCount > 0 && rejectCount === 0) { + return true; + } else if (acceptCount === 0 && rejectCount > 0) { + return false; + } + + return false; +} + +// Returns the "useful" schema, i.e. what would someone want to use to identify events. +// The idea being that you can determine the event type from 'e', so getting the schema from +// 'ue_px.schema'/'ue_pr.schema' would be redundant - it'll return the unstructured event schema. +// Instead the schema nested inside the unstructured event is more useful! +// This doesn't decode ue_px, it works because it's called by code that has already decoded it +export function getUsefulSchema(sb: SelfDescribingJson): string { + if (typeof get(sb, 'ue_px.data.schema') === 'string') + return get(sb, 'ue_px.data.schema'); + else if (typeof get(sb, 'ue_pr.data.schema') === 'string') + return get(sb, 'ue_pr.data.schema'); + else if (typeof get(sb, 'schema') === 'string') + return get(sb, 'schema'); + return ''; +} + +export function getDecodedEvent(sb: SelfDescribingJson): SelfDescribingJson { + let decodedEvent = {...sb}; // spread operator, instantiates new object + try { + if (has(decodedEvent, 'ue_px')) { + decodedEvent['ue_px'] = JSON.parse(base64urldecode(get(decodedEvent, ['ue_px']))); + } + } catch(e) {} + return decodedEvent; +} + +export function getEventType(sb: {}): string { + return get(sb, 'e', ''); +} + +export function buildGenerator(generator: ContextGenerator, + event: SelfDescribingJson, + eventType: string, + eventSchema: string) : SelfDescribingJson | Array | undefined { + let contextGeneratorResult : SelfDescribingJson | Array | undefined = undefined; + try { + // try to evaluate context generator + let args = { + event: event, + eventType: eventType, + eventSchema: eventSchema + }; + contextGeneratorResult = generator(args); + // determine if the produced result is a valid SDJ + if (isSelfDescribingJson(contextGeneratorResult)) { + return contextGeneratorResult; + } else if (Array.isArray(contextGeneratorResult) && every(contextGeneratorResult, isSelfDescribingJson)) { + return contextGeneratorResult; + } else { + return undefined; + } + } catch (error) { + contextGeneratorResult = undefined; + } + return contextGeneratorResult; +} + +export function normalizeToArray(input: any) : Array { + if (Array.isArray(input)) { + return input; + } + return Array.of(input); +} + +export function generatePrimitives(contextPrimitives: Array | ContextPrimitive, + event: SelfDescribingJson, + eventType: string, + eventSchema: string) : Array { + let normalizedInputs : Array = normalizeToArray(contextPrimitives); + let partialEvaluate = (primitive) => { + let result = evaluatePrimitive(primitive, event, eventType, eventSchema); + if (result && result.length !== 0) { + return result; + } + }; + let generatedContexts = map(normalizedInputs, partialEvaluate); + return [].concat(...compact(generatedContexts)); +} + +export function evaluatePrimitive(contextPrimitive: ContextPrimitive, + event: SelfDescribingJson, + eventType: string, + eventSchema: string) : Array | undefined { + if (isSelfDescribingJson(contextPrimitive)) { + return [contextPrimitive as SelfDescribingJson]; + } else if (isContextGenerator(contextPrimitive)) { + let generatorOutput = buildGenerator(contextPrimitive as ContextGenerator, event, eventType, eventSchema); + if (isSelfDescribingJson(generatorOutput)) { + return [generatorOutput as SelfDescribingJson]; + } else if (Array.isArray(generatorOutput)) { + return generatorOutput; + } + } + return undefined; +} + +export function evaluateProvider(provider: ConditionalContextProvider, + event: SelfDescribingJson, + eventType: string, + eventSchema: string): Array { + if (isFilterProvider(provider)) { + let filter : ContextFilter = (provider as FilterProvider)[0]; + let filterResult = false; + try { + let args = { + event: event, + eventType: eventType, + eventSchema: eventSchema + }; + filterResult = filter(args); + } catch(error) { + filterResult = false; + } + if (filterResult === true) { + return generatePrimitives((provider as FilterProvider)[1], event, eventType, eventSchema); + } + } else if (isRuleSetProvider(provider)) { + if (matchSchemaAgainstRuleSet((provider as RuleSetProvider)[0], eventSchema)) { + return generatePrimitives((provider as RuleSetProvider)[1], event, eventType, eventSchema); + } + } + return []; +} + +export function generateConditionals(providers: Array | ConditionalContextProvider, + event: SelfDescribingJson, + eventType: string, + eventSchema: string) : Array { + let normalizedInput : Array = normalizeToArray(providers); + let partialEvaluate = (provider) => { + let result = evaluateProvider(provider, event, eventType, eventSchema); + if (result && result.length !== 0) { + return result; + } + }; + let generatedContexts = map(normalizedInput, partialEvaluate); + return [].concat(...compact(generatedContexts)); +} + +export function contextModule() { + let globalPrimitives : Array = []; + let conditionalProviders : Array = []; + + function assembleAllContexts(event: SelfDescribingJson) : Array { + let eventSchema = getUsefulSchema(event); + let eventType = getEventType(event); + let contexts : Array = []; + let generatedPrimitives = generatePrimitives(globalPrimitives, event, eventType, eventSchema); + contexts.push(...generatedPrimitives); + + let generatedConditionals = generateConditionals(conditionalProviders, event, eventType, eventSchema); + contexts.push(...generatedConditionals); + + return contexts; + } + + return { + getGlobalPrimitives: function () { + return globalPrimitives; + }, + + getConditionalProviders: function () { + return conditionalProviders; + }, + + addGlobalContexts: function (contexts: Array) { + let acceptedConditionalContexts : ConditionalContextProvider[] = []; + let acceptedContextPrimitives : ContextPrimitive[] = []; + for (let context of contexts) { + if (isConditionalContextProvider(context)) { + acceptedConditionalContexts.push(context); + } else if (isContextPrimitive(context)) { + acceptedContextPrimitives.push(context); + } + } + globalPrimitives = globalPrimitives.concat(acceptedContextPrimitives); + conditionalProviders = conditionalProviders.concat(acceptedConditionalContexts); + }, + + clearGlobalContexts: function () { + conditionalProviders = []; + globalPrimitives = []; + }, + + removeGlobalContexts: function (contexts: Array) { + for (let context of contexts) { + if (isConditionalContextProvider(context)) { + conditionalProviders = conditionalProviders.filter(item => !isEqual(item, context)); + } else if (isContextPrimitive(context)) { + globalPrimitives = globalPrimitives.filter(item => !isEqual(item, context)); + } else { + // error message here? + } + } + }, + + getApplicableContexts: function (event: PayloadData) : Array { + const builtEvent = event.build(); + if (isEventJson(builtEvent)) { + const decodedEvent = getDecodedEvent(builtEvent as SelfDescribingJson); + return assembleAllContexts(decodedEvent); + } else { + return []; + } + } + }; +} diff --git a/snowplow-javascript-tracker/core/lib/core.ts b/snowplow-javascript-tracker/core/lib/core.ts new file mode 100644 index 0000000000..974bcb7e56 --- /dev/null +++ b/snowplow-javascript-tracker/core/lib/core.ts @@ -0,0 +1,1013 @@ +/* + * JavaScript tracker core for Snowplow: core.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +import uuid = require('uuid'); + +import * as payload from './payload'; +import {PayloadData} from "./payload"; +import { + contextModule as contextConstructor +} from "./contexts"; + +/** + * Interface common for any Self-Describing JSON such as custom context or + * Self-describing (ex-unstuctured) event + */ +export interface SelfDescribingJson extends Object { + schema: string + data: Object +} + +/** + * Algebraic datatype representing possible timestamp type choice + */ +export type Timestamp = TrueTimestamp | DeviceTimestamp | number; +export interface TrueTimestamp { readonly type: "ttm"; readonly value: number } +export interface DeviceTimestamp { readonly type: "dtm"; readonly value: number } + +/** + * Pair of timestamp type ready to be included to payload + */ +type TimestampPayload = TrueTimestamp | DeviceTimestamp + +/** + * Transform optional/old-behavior number timestamp into + * `TrackerTimestamp` ADT + * + * @param tstamp optional number or correct timestamp object + * @returns {Timestamp} correct timestamp object + */ +function getTimestamp(tstamp?: Timestamp): TimestampPayload { + if (tstamp == null) { + return {type: 'dtm', value: new Date().getTime() } + } else if (typeof tstamp === 'number') { + return { type: 'dtm', value: tstamp } + } else if (tstamp.type === 'ttm') { // We can return tstamp here, but this is safer fallback + return { type: 'ttm', value: tstamp.value } + } else { + return { type: 'dtm', value: (tstamp.value || new Date().getTime()) } + } +} + +/** + * Create a tracker core object + * + * @param base64 Whether to base 64 encode contexts and unstructured event JSONs + * @param callback Function applied to every payload dictionary object + * @return Tracker core + */ +export function trackerCore(base64: boolean, callback?: (PayloadData) => void) { + + // base 64 encoding should default to true + if (typeof base64 === 'undefined') { + base64 = true; + } + + // Dictionary of key-value pairs which get added to every payload, e.g. tracker version + var payloadPairs = {}; + + let contextModule = contextConstructor(); + + /** + * Gets all global contexts to be added to events + * + * @param event + */ + function getAllContexts(event: PayloadData) : Array { + return contextModule.getApplicableContexts(event); + } + + /** + * Set a persistent key-value pair to be added to every payload + * + * @param key Field name + * @param value Field value + */ + function addPayloadPair(key: string, value: string) { + payloadPairs[key] = value; + } + + /** + * Returns a copy of a JSON with undefined and null properties removed + * + * @param eventJson JSON object to clean + * @param exemptFields Set of fields which should not be removed even if empty + * @return A cleaned copy of eventJson + */ + function removeEmptyProperties(eventJson: Object, exemptFields?: Object) { + var ret = {}; + exemptFields = exemptFields || {}; + for (var k in eventJson) { + if (exemptFields[k] || (eventJson[k] !== null && typeof eventJson[k] !== 'undefined')) { + ret[k] = eventJson[k]; + } + } + return ret; + } + + /** + * Wraps an array of custom contexts in a self-describing JSON + * + * @param contexts Array of custom context self-describing JSONs + * @return Outer JSON + */ + function completeContexts(contexts?: Array): Object | undefined { // TODO: can return nothing + if (contexts && contexts.length) { + return { + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: contexts + }; + } + } + + /** + * Adds all global contexts to a contexts array + * + * @param sb PayloadData + * @param contexts Array + */ + function attachGlobalContexts(sb: PayloadData, + contexts?: Array): Array { + let globalContexts : Array = getAllContexts(sb); + let returnedContexts : Array = []; + if (contexts && contexts.length) { + returnedContexts.push(...contexts); + } + if (globalContexts && globalContexts.length) { + returnedContexts.push(...globalContexts); + } + return returnedContexts; + } + + /** + * Gets called by every trackXXX method + * Adds context and payloadPairs name-value pairs to the payload + * Applies the callback to the built payload + * + * @param sb Payload + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload after the callback is applied + */ + function track(sb: PayloadData, context?: Array, tstamp?: Timestamp): PayloadData { + sb.addDict(payloadPairs); + sb.add('eid', uuid.v4()); + var timestamp = getTimestamp(tstamp); + sb.add(timestamp.type, timestamp.value.toString()); + var allContexts = attachGlobalContexts(sb, context); + var wrappedContexts = completeContexts(allContexts); + if (wrappedContexts !== undefined) { + sb.addJson('cx', 'co', wrappedContexts); + } + + if (typeof callback === 'function') { + callback(sb); + } + + return sb; + } + + /** + * Log an self-describing (previously unstruct) event + * + * @param properties Contains the properties and schema location for the event + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload + */ + function trackSelfDescribingEvent(properties: Object, context?: Array, tstamp?: Timestamp): PayloadData { + var sb = payload.payloadBuilder(base64); + var ueJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', + data: properties + }; + + sb.add('e', 'ue'); + sb.addJson('ue_px', 'ue_pr', ueJson); + + return track(sb, context, tstamp); + } + + return { + + /** + * Turn base 64 encoding on or off + * + * @param encode Whether to encode payload + */ + setBase64Encoding: function (encode: boolean) { + base64 = encode; + }, + + addPayloadPair: addPayloadPair, + + /** + * Merges a dictionary into payloadPairs + * + * @param dict Dictionary to add + */ + addPayloadDict: function (dict: Object) { + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + payloadPairs[key] = dict[key]; + } + } + }, + + /** + * Replace payloadPairs with a new dictionary + * + * @param dict object New dictionary + */ + resetPayloadPairs: function (dict: Object) { + payloadPairs = payload.isJson(dict) ? dict : {}; + }, + + /** + * Set the tracker version + * + * @param version string + */ + setTrackerVersion: function (version: string) { + addPayloadPair('tv', version); + }, + + /** + * Set the tracker namespace + * + * @param name string + */ + setTrackerNamespace: function (name: string) { + addPayloadPair('tna', name); + }, + + /** + * Set the application ID + * + * @param appId string + */ + setAppId: function (appId: string) { + addPayloadPair('aid', appId); + }, + + /** + * Set the platform + * + * @param value string + */ + setPlatform: function (value: string) { + addPayloadPair('p', value); + }, + + /** + * Set the user ID + * + * @param userId string + */ + setUserId: function (userId: string) { + addPayloadPair('uid', userId); + }, + + /** + * Set the screen resolution + * + * @param width number + * @param height number + */ + setScreenResolution: function (width: string, height: string) { + addPayloadPair('res', width + 'x' + height); + }, + + /** + * Set the viewport dimensions + * + * @param width number + * @param height number + */ + setViewport: function (width: string, height: string) { + addPayloadPair('vp', width + 'x' + height); + }, + + /** + * Set the color depth + * + * @param depth number + */ + setColorDepth: function (depth: string) { + addPayloadPair('cd', depth); + }, + + /** + * Set the timezone + * + * @param timezone string + */ + setTimezone: function (timezone: string) { + addPayloadPair('tz', timezone); + }, + + /** + * Set the language + * + * @param lang string + */ + setLang: function (lang: string) { + addPayloadPair('lang', lang); + }, + + /** + * Set the IP address + * + * @param ip string + */ + setIpAddress: function (ip: string) { + addPayloadPair('ip', ip); + }, + + trackUnstructEvent: trackSelfDescribingEvent, + + trackSelfDescribingEvent: trackSelfDescribingEvent, + + /** + * Log the page view / visit + * + * @param pageUrl Current page URL + * @param pageTitle The user-defined page title to attach to this page view + * @param referrer URL users came from + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload + */ + trackPageView: function ( + pageUrl: string, + pageTitle: string, + referrer: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var sb = payload.payloadBuilder(base64); + sb.add('e', 'pv'); // 'pv' for Page View + sb.add('url', pageUrl); + sb.add('page', pageTitle); + sb.add('refr', referrer); + + return track(sb, context, tstamp); + }, + /** + * Log that a user is still viewing a given page + * by sending a page ping + * + * @param pageUrl Current page URL + * @param pageTitle The page title to attach to this page ping + * @param referrer URL users came from + * @param minXOffset Minimum page x offset seen in the last ping period + * @param maxXOffset Maximum page x offset seen in the last ping period + * @param minYOffset Minimum page y offset seen in the last ping period + * @param maxYOffset Maximum page y offset seen in the last ping period + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return object Payload + */ + trackPagePing: function ( + pageUrl: string, + pageTitle: string, + referrer: string, + minXOffset: number, + maxXOffset: number, + minYOffset: number, + maxYOffset: number, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var sb = payload.payloadBuilder(base64); + sb.add('e', 'pp'); // 'pp' for Page Ping + sb.add('url', pageUrl); + sb.add('page', pageTitle); + sb.add('refr', referrer); + sb.add('pp_mix', minXOffset.toString()); + sb.add('pp_max', maxXOffset.toString()); + sb.add('pp_miy', minYOffset.toString()); + sb.add('pp_may', maxYOffset.toString()); + + return track(sb, context, tstamp); + }, + /** + * Track a structured event + * + * @param category The name you supply for the group of objects you want to track + * @param action A string that is uniquely paired with each category, and commonly used to define the type of user interaction for the web object + * @param label (optional) An optional string to provide additional dimensions to the event data + * @param property (optional) Describes the object or the action performed on it, e.g. quantity of item added to basket + * @param value (optional) An integer that you can use to provide numerical data about the user event + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload + */ + trackStructEvent: function ( + category: string, + action: string, + label: string, + property: string, + value?: number, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var sb = payload.payloadBuilder(base64); + sb.add('e', 'se'); // 'se' for Structured Event + sb.add('se_ca', category); + sb.add('se_ac', action); + sb.add('se_la', label); + sb.add('se_pr', property); + sb.add('se_va', (value == null ? undefined : value.toString())); + + return track(sb, context, tstamp); + }, + + /** + * Track an ecommerce transaction + * + * @param orderId Required. Internal unique order id number for this transaction. + * @param affiliation Optional. Partner or store affiliation. + * @param totalValue Required. Total amount of the transaction. + * @param taxValue Optional. Tax amount of the transaction. + * @param shipping Optional. Shipping charge for the transaction. + * @param city Optional. City to associate with transaction. + * @param state Optional. State to associate with transaction. + * @param country Optional. Country to associate with transaction. + * @param currency Optional. Currency to associate with this transaction. + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return Payload + */ + trackEcommerceTransaction: function ( + orderId: string, + affiliation: string, + totalValue: string, + taxValue?: string, + shipping?: string, + city?: string, + state?: string, + country?: string, + currency?: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var sb = payload.payloadBuilder(base64); + sb.add('e', 'tr'); // 'tr' for Transaction + sb.add("tr_id", orderId); + sb.add("tr_af", affiliation); + sb.add("tr_tt", totalValue); + sb.add("tr_tx", taxValue); + sb.add("tr_sh", shipping); + sb.add("tr_ci", city); + sb.add("tr_st", state); + sb.add("tr_co", country); + sb.add("tr_cu", currency); + + return track(sb, context, tstamp); + }, + + /** + * Track an ecommerce transaction item + * + * @param orderId Required Order ID of the transaction to associate with item. + * @param sku Required. Item's SKU code. + * @param name Optional. Product name. + * @param category Optional. Product category. + * @param price Required. Product price. + * @param quantity Required. Purchase quantity. + * @param currency Optional. Product price currency. + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return object Payload + */ + trackEcommerceTransactionItem: function ( + orderId: string, + sku: string, + name: string, + category: string, + price: string, + quantity: string, + currency?: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var sb = payload.payloadBuilder(base64); + sb.add("e", "ti"); // 'tr' for Transaction Item + sb.add("ti_id", orderId); + sb.add("ti_sk", sku); + sb.add("ti_nm", name); + sb.add("ti_ca", category); + sb.add("ti_pr", price); + sb.add("ti_qu", quantity); + sb.add("ti_cu", currency); + + return track(sb, context, tstamp); + }, + + /** + * Track a screen view unstructured event + * + * @param name The name of the screen + * @param id The ID of the screen + * @param context Optional. Contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload + */ + trackScreenView: function ( + name: string, + id: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0', + data: removeEmptyProperties({ + name: name, + id: id + }) + }, context, tstamp); + }, + + /** + * Log the link or click with the server + * + * @param targetUrl + * @param elementId + * @param elementClasses + * @param elementTarget + * @param elementContent innerHTML of the link + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload + */ + trackLinkClick: function ( + targetUrl: string, + elementId: string, + elementClasses: Array, + elementTarget: string, + elementContent: string, + context?: Array, + tstamp?: Timestamp) { + + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: removeEmptyProperties({ + targetUrl: targetUrl, + elementId: elementId, + elementClasses: elementClasses, + elementTarget: elementTarget, + elementContent: elementContent + }) + }; + + return trackSelfDescribingEvent(eventJson, context, tstamp); + }, + + /** + * Track an ad being served + * + * @param impressionId Identifier for a particular ad impression + * @param costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param cost Cost + * @param targetUrl URL ad pointing to + * @param bannerId Identifier for the ad banner displayed + * @param zoneId Identifier for the ad zone + * @param advertiserId Identifier for the advertiser + * @param campaignId Identifier for the campaign which the banner belongs to + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return object Payload + */ + trackAdImpression: function ( + impressionId: string, + costModel: string, + cost: number, + targetUrl: string, + bannerId: string, + zoneId: string, + advertiserId: string, + campaignId: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0', + data: removeEmptyProperties({ + impressionId: impressionId, + costModel: costModel, + cost: cost, + targetUrl: targetUrl, + bannerId: bannerId, + zoneId: zoneId, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackSelfDescribingEvent(eventJson, context, tstamp); + }, + + /** + * Track an ad being clicked + * + * @param targetUrl (required) The link's target URL + * @param clickId Identifier for the ad click + * @param costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param cost Cost + * @param bannerId Identifier for the ad banner displayed + * @param zoneId Identifier for the ad zone + * @param impressionId Identifier for a particular ad impression + * @param advertiserId Identifier for the advertiser + * @param campaignId Identifier for the campaign which the banner belongs to + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return object Payload + */ + trackAdClick: function ( + targetUrl: string, + clickId: string, + costModel: string, + cost: number, + bannerId: string, + zoneId: string, + impressionId: string, + advertiserId: string, + campaignId: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0', + data: removeEmptyProperties({ + targetUrl: targetUrl, + clickId: clickId, + costModel: costModel, + cost: cost, + bannerId: bannerId, + zoneId: zoneId, + impressionId: impressionId, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackSelfDescribingEvent(eventJson, context, tstamp); + }, + + /** + * Track an ad conversion event + * + * @param conversionId Identifier for the ad conversion event + * @param costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param cost Cost + * @param category The name you supply for the group of objects you want to track + * @param action A string that is uniquely paired with each category + * @param property Describes the object of the conversion or the action performed on it + * @param initialValue Revenue attributable to the conversion at time of conversion + * @param advertiserId Identifier for the advertiser + * @param campaignId Identifier for the campaign which the banner belongs to + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return object Payload + * + * @todo make costModel enum + */ + trackAdConversion: function ( + conversionId: string, + costModel: string, + cost: number, + category: string, + action: string, + property: string, + initialValue: number, + advertiserId: string, + campaignId: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_conversion/jsonschema/1-0-0', + data: removeEmptyProperties({ + conversionId: conversionId, + costModel: costModel, + cost: cost, + category: category, + action: action, + property: property, + initialValue: initialValue, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackSelfDescribingEvent(eventJson, context, tstamp); + }, + + /** + * Track a social event + * + * @param action Social action performed + * @param network Social network + * @param target Object of the social action e.g. the video liked, the tweet retweeted + * @param context Custom contexts relating to the event + * @param tstamp TrackerTimestamp of the event + * @return Payload + */ + trackSocialInteraction: function ( + action: string, + network: string, + target: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/social_interaction/jsonschema/1-0-0', + data: removeEmptyProperties({ + action: action, + network: network, + target: target + }) + }; + + return trackSelfDescribingEvent(eventJson, context, tstamp); + }, + + /** + * Track an add-to-cart event + * + * @param sku Required. Item's SKU code. + * @param name Optional. Product name. + * @param category Optional. Product category. + * @param unitPrice Optional. Product price. + * @param quantity Required. Quantity added. + * @param currency Optional. Product price currency. + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return Payload + */ + trackAddToCart: function ( + sku: string, + name: string, + category: string, + unitPrice: string, + quantity: string, + currency?: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/add_to_cart/jsonschema/1-0-0', + data: removeEmptyProperties({ + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + }) + }, context, tstamp); + }, + + /** + * Track a remove-from-cart event + * + * @param sku Required. Item's SKU code. + * @param name Optional. Product name. + * @param category Optional. Product category. + * @param unitPrice Optional. Product price. + * @param quantity Required. Quantity removed. + * @param currency Optional. Product price currency. + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return Payload + */ + trackRemoveFromCart: function ( + sku: string, + name: string, + category: string, + unitPrice: string, + quantity: string, + currency?: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/remove_from_cart/jsonschema/1-0-0', + data: removeEmptyProperties({ + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + }) + }, context, tstamp); + }, + + /** + * Track the value of a form field changing or receiving focus + * + * @param schema The schema type of the event + * @param formId The parent form ID + * @param elementId ID of the changed element + * @param nodeName "INPUT", "TEXTAREA", or "SELECT" + * @param type Type of the changed element if its type is "INPUT" + * @param elementClasses List of classes of the changed element + * @param value The new value of the changed element + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return Payload + * + * @todo make `nodeName` enum + */ + trackFormFocusOrChange: function ( + schema: string, + formId: string, + elementId: string, + nodeName: string, + type: string, + elementClasses: Array, + value: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + let event_schema = ''; + if (schema === 'change_form'){ + event_schema = 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0'; + } else if (schema === 'focus_form') { + event_schema = 'iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0'; + } + return trackSelfDescribingEvent({ + schema: event_schema, + data: removeEmptyProperties({ + formId: formId, + elementId: elementId, + nodeName: nodeName, + type: type, + elementClasses: elementClasses, + value: value + }, {value: true}) + }, context, tstamp); + }, + + /** + * Track a form submission event + * + * @param formId ID of the form + * @param formClasses Classes of the form + * @param elements Mutable elements within the form + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return Payload + */ + trackFormSubmission: function( + formId: string, + formClasses: Array, + elements: Array, + context?: Array, + tstamp?: Timestamp): PayloadData { + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0', + data: removeEmptyProperties({ + formId: formId, + formClasses: formClasses, + elements: elements + }) + }, context, tstamp); + }, + + /** + * Track an internal search event + * + * @param terms Search terms + * @param filters Search filters + * @param totalResults Number of results + * @param pageResults Number of results displayed on page + * @param context Optional. Context relating to the event. + * @param tstamp Optional. TrackerTimestamp of the event + * @return Payload + */ + trackSiteSearch: function( + terms: Array, + filters: Object, + totalResults: number, + pageResults: number, + context?: Array, + tstamp?: Timestamp): PayloadData { + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/site_search/jsonschema/1-0-0', + data: removeEmptyProperties({ + terms: terms, + filters: filters, + totalResults: totalResults, + pageResults: pageResults + }) + }, context, tstamp); + }, + + /** + * Track a consent withdrawn event + * + * @param {boolean} all - Indicates user withdraws consent for all documents. + * @param {string} [id] - ID number associated with document. + * @param {string} [version] - Version number of document. + * @param {string} [name] - Name of document. + * @param {string} [description] - Description of document. + * @param {Array} [context] - Context relating to the event. + * @param {Timestamp} [tstamp] - TrackerTimestamp of the event. + * @return Payload + */ + trackConsentWithdrawn: function( + all: boolean, + id?: string, + version?: string, + name?: string, + description?: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + let documentJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0', + data: removeEmptyProperties({ + id: id, + version: version, + name: name, + description: description + }) + }; + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/consent_withdrawn/jsonschema/1-0-0', + data: removeEmptyProperties({ + all: all + }) + }, documentJson.data && context ? context.concat([documentJson]) : context, tstamp); + }, + + /** + * Track a consent granted event + * + * @param {string} id - ID number associated with document. + * @param {string} version - Version number of document. + * @param {string} [name] - Name of document. + * @param {string} [description] - Description of document. + * @param {string} [expiry] - Date-time when consent expires. + * @param {Array} [context] - Context relating to the event. + * @param {Timestamp} [tstamp] - TrackerTimestamp of the event. + * @return Payload + */ + trackConsentGranted: function( + id: string, + version: string, + name?: string, + description?: string, + expiry?: string, + context?: Array, + tstamp?: Timestamp): PayloadData { + + let documentJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0', + data: removeEmptyProperties({ + id: id, + version: version, + name: name, + description: description, + }) + }; + + return trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/consent_granted/jsonschema/1-0-0', + data: removeEmptyProperties({ + expiry: expiry, + }) + }, context ? context.concat([documentJson]) : [documentJson], tstamp); + }, + + addGlobalContexts: function(contexts: Array) { + contextModule.addGlobalContexts(contexts); + }, + + clearGlobalContexts: function() { + contextModule.clearGlobalContexts(); + }, + + removeGlobalContexts: function(contexts: Array) { + contextModule.removeGlobalContexts(contexts); + } + }; +} diff --git a/snowplow-javascript-tracker/core/lib/payload.ts b/snowplow-javascript-tracker/core/lib/payload.ts new file mode 100644 index 0000000000..92dec70de4 --- /dev/null +++ b/snowplow-javascript-tracker/core/lib/payload.ts @@ -0,0 +1,112 @@ +/* + * JavaScript tracker core for Snowplow: payload.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +import * as base64 from './base64'; + +/** + * Interface for mutable object encapsulating tracker payload + */ +export interface PayloadData { + add: (key: string, value?: string) => void, + addDict: (dict: Object) => void, + addJson: (keyIfEncoded: string, keyIfNotEncoded: string, json: Object) => void, + build: () => Object; +} + +/** + * Bas64 encode data with URL and Filename Safe Alphabet (base64url) + * + * See: http://tools.ietf.org/html/rfc4648#page-7 + */ +function base64urlencode(data: string): string { + if (!data) { + return data; + } + + var enc = base64.base64encode(data); + return enc.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); +} + +/** + * Is property a non-empty JSON? + */ +export function isNonEmptyJson(property): boolean { + if (!isJson(property)) { + return false; + } + for (var key in property) { + if (property.hasOwnProperty(key)) { + return true; + } + } + return false; +} + +/** + * Is property a JSON? + */ +export function isJson(property: Object): boolean { + return (typeof property !== 'undefined' && property !== null && + (property.constructor === {}.constructor || property.constructor === [].constructor)); +} + + +/** + * A helper to build a Snowplow request string from an + * an optional initial value plus a set of individual + * name-value pairs, provided using the add method. + * + * @param base64Encode boolean Whether or not JSONs should be + * Base64-URL-safe-encoded + * + * @return object The request string builder, with add, addRaw and build methods + */ +export function payloadBuilder(base64Encode: boolean): PayloadData { + var dict = {}; + + var add = function (key: string, value?: string): void { + if (value != null && value !== '') { // null also checks undefined + dict[key] = value; + } + }; + + var addDict = function (dict: Object) { + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + add(key, dict[key]); + } + } + }; + + var addJson = function (keyIfEncoded: string, keyIfNotEncoded: string, json?: Object) { + if (isNonEmptyJson(json)) { + var str = JSON.stringify(json); + if (base64Encode) { + add(keyIfEncoded, base64urlencode(str)); + } else { + add(keyIfNotEncoded, str); + } + } + }; + + return { + add: add, + addDict: addDict, + addJson: addJson, + build: function () { + return dict; + } + }; +} diff --git a/snowplow-javascript-tracker/core/license.txt b/snowplow-javascript-tracker/core/license.txt new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/snowplow-javascript-tracker/core/license.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/snowplow-javascript-tracker/core/package.json b/snowplow-javascript-tracker/core/package.json new file mode 100644 index 0000000000..120ac15209 --- /dev/null +++ b/snowplow-javascript-tracker/core/package.json @@ -0,0 +1,37 @@ +{ + "name": "snowplow-tracker-core", + "version": "0.7.0", + "devDependencies": { + "@types/es6-shim": "0.31.34", + "@types/node": "^9.6.7", + "dts-generator": "^2.0.0", + "grunt": "0.4.5", + "grunt-ts": "5.5.1", + "intern": "3.3.2", + "typescript": "2.2.2" + }, + "dependencies": { + "@types/uuid": "^2.0.29", + "lodash": "^4.17.11", + "uuid": "2.0.3" + }, + "contributors": [ + "Alex Dean", + "Simon Andersson", + "Fred Blundun" + ], + "types": "./main.d.ts", + "description": "Core functionality for the Snowplow JavaScript trackers (browser JavaScript; Node.js; Segment.io server-side)", + "repository": { + "type": "git", + "url": "https://github.com/snowplow/snowplow-javascript-tracker.git" + }, + "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues", + "keywords": [ + "tracking", + "web analytics", + "events", + "open source" + ], + "license": "Apache-2.0" +} diff --git a/snowplow-javascript-tracker/core/tests/intern.js b/snowplow-javascript-tracker/core/tests/intern.js new file mode 100644 index 0000000000..b0ebd23ff2 --- /dev/null +++ b/snowplow-javascript-tracker/core/tests/intern.js @@ -0,0 +1,20 @@ +/* + * JavaScript tracker core for Snowplow: tests/intern.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +define({ + + excludeInstrumentation: /^(?:tests|node_modules)\// + +}); diff --git a/snowplow-javascript-tracker/core/tests/unit/base64.js b/snowplow-javascript-tracker/core/tests/unit/base64.js new file mode 100644 index 0000000000..ddedce0bf1 --- /dev/null +++ b/snowplow-javascript-tracker/core/tests/unit/base64.js @@ -0,0 +1,32 @@ +/* + * JavaScript tracker core for Snowplow: tests/base64.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +define([ + "intern!object", + "intern/chai!assert", + "intern/dojo/node!../../lib/base64.js" +], function (registerSuite, assert, base64) { + + registerSuite({ + name: "Base 64 encoding test", + "Encode a string": function () { + assert.strictEqual(base64.base64encode('my_string'), 'bXlfc3RyaW5n', 'Base64-encode a string'); + }, + + "Encode a string containing special characters": function () { + assert.strictEqual(base64.base64encode('™®字'), '4oSiwq7lrZc=', 'Base64-encode a containing TM, Registered Trademark, and Chinese characters'); + } + }); +}); diff --git a/snowplow-javascript-tracker/core/tests/unit/contexts.js b/snowplow-javascript-tracker/core/tests/unit/contexts.js new file mode 100644 index 0000000000..b35307169e --- /dev/null +++ b/snowplow-javascript-tracker/core/tests/unit/contexts.js @@ -0,0 +1,327 @@ +/* + * JavaScript tracker core for Snowplow: tests/integration.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +define([ + "intern!object", + "intern/chai!assert", + "intern/chai!expect", + "intern/dojo/node!../../lib/contexts.js", + "intern/dojo/node!../../lib/payload.js" +], function (registerSuite, assert, expect, contexts, payload) { + registerSuite({ + name: "Global context tests", + + "Identify context primitives": function () { + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1'; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['deviceModel'] = String(args[eventType]); + return context; + } + + assert.isTrue(contexts.isContextPrimitive(geolocationContext), 'A context primitive should be identified'); + assert.isTrue(contexts.isContextPrimitive(eventTypeContextGenerator), 'A context primitive should be identified'); + }, + + "Validating vendors": function () { + // Vendor validation + assert.isTrue(contexts.validateVendor('com.acme.marketing'), 'A valid vendor without wildcard is accepted'); + assert.isTrue(contexts.validateVendor('com.acme.*'), 'A valid vendor with wildcard is accepted'); + assert.isFalse(contexts.validateVendor('*.acme.*'), 'A vendor starting with asterisk is rejected'); + assert.isFalse(contexts.validateVendor('com.acme.*.marketing'), 'A vendor with asterisk out of order is rejected'); + }, + + "Identify rule sets": function () { + const acceptRuleSet = { + accept: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'] + }; + + const rejectRuleSet = { + reject: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/1-0-1'] + }; + + const bothRuleSet = { + accept: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/1-*-1'], + reject: ['iglu:com.snowplowanalytics.snowplow/some_event/jsonschema/1-*-*'] + }; + + const pageview_schema = 'iglu:com.snowplowanalytics.snowplow/pageview/jsonschema/1-0-1'; + + + + assert.isTrue(contexts.isValidRule(acceptRuleSet.accept[0]), 'All rule elements are correctly identified as valid rules'); + assert.isTrue(contexts.isValidRuleSetArg(acceptRuleSet.accept), 'A rule set arg is correctly identified'); + assert.isTrue(contexts.isRuleSet(acceptRuleSet), 'An accept rule set is identified'); + assert.isTrue(contexts.isRuleSet(rejectRuleSet), 'A reject rule set is identified'); + assert.isTrue(contexts.isRuleSet(bothRuleSet), 'A rule set with both elements is identified'); + assert.deepEqual(contexts.getSchemaParts(pageview_schema), ['com.snowplowanalytics.snowplow', 'pageview', '1', '0', '1'], 'Gets correct parts for schema'); + assert.isTrue(contexts.matchSchemaAgainstRule('iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*', pageview_schema), 'Matches schema against wildcarded rule'); + assert.isTrue(contexts.matchSchemaAgainstRuleSet(acceptRuleSet, pageview_schema), 'Accept rule set accepts matching schema'); + assert.isFalse(contexts.matchSchemaAgainstRuleSet(rejectRuleSet, pageview_schema), 'Reject rule set rejects matching schema'); + }, + + "Identify filter function": function () { + let filterFunction = function (args) { + return args['eventType'] === 'ue'; + }; + + assert.isTrue(contexts.isContextFilter(filterFunction), 'A valid filter function is identified'); + }, + + "Identify rule set provider": function () { + let bothRuleSet = { + accept: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'], + reject: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'] + }; + + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1'; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['deviceModel'] = String(args[eventType]); + return context; + } + + let ruleSetProvider = [bothRuleSet, [geolocationContext, eventTypeContextGenerator]]; + assert.isTrue(contexts.isRuleSetProvider(ruleSetProvider), 'Valid rule set provider is correctly identified'); + }, + + "Identify filter provider": function () { + let filterFunction = function (args) { + return args['eventType'] === 'ue'; + }; + + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1'; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['deviceModel'] = String(args[eventType]); + return context; + } + + let filterProvider = [filterFunction, [geolocationContext, eventTypeContextGenerator]]; + assert.isTrue(contexts.isFilterProvider(filterProvider), 'A valid filter provider is identified'); + }, + + "Add global contexts": function () { + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1'; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['deviceModel'] = String(args[eventType]); + return context; + } + + let bothRuleSet = { + accept: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'], + reject: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'] + }; + + let filterFunction = function (args) { + return args['eventType'] === 'ue'; + }; + + let filterProvider = [filterFunction, [geolocationContext, eventTypeContextGenerator]]; + let ruleSetProvider = [bothRuleSet, [geolocationContext, eventTypeContextGenerator]]; + + let contextArray = [filterProvider, ruleSetProvider, geolocationContext, eventTypeContextGenerator]; + let module = contexts.contextModule(); + + module.addGlobalContexts(contextArray); + assert.strictEqual(module.getGlobalPrimitives().length, 2, + 'Correct number of primitives added'); + assert.strictEqual(module.getConditionalProviders().length, 2, + 'Correct number of conditional providers added'); + }, + + "Remove global contexts": function () { + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1'; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['deviceModel'] = String(args[eventType]); + return context; + } + + let bothRuleSet = { + accept: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'], + reject: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'] + }; + + let filterFunction = function (args) { + return args['eventType'] === 'ue'; + }; + + let filterProvider = [filterFunction, [geolocationContext, eventTypeContextGenerator]]; + let ruleSetProvider = [bothRuleSet, [geolocationContext, eventTypeContextGenerator]]; + let module = contexts.contextModule(); + + module.addGlobalContexts([filterProvider, ruleSetProvider, geolocationContext, eventTypeContextGenerator]); + module.removeGlobalContexts([filterProvider, geolocationContext, eventTypeContextGenerator]); + assert.strictEqual(module.getGlobalPrimitives().length, 0, + 'Correct number of primitives added'); + assert.strictEqual(module.getConditionalProviders().length, 1, + 'Correct number of conditional providers added'); + }, + + "Clear global contexts": function () { + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1'; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['deviceModel'] = String(args[eventType]); + return context; + } + + let bothRuleSet = { + accept: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'], + reject: ['iglu:com.snowplowanalytics.snowplow/*/jsonschema/*-*-*'] + }; + + let filterFunction = function (args) { + return args['eventType'] === 'ue'; + }; + + let filterProvider = [filterFunction, [geolocationContext, eventTypeContextGenerator]]; + let ruleSetProvider = [bothRuleSet, [geolocationContext, eventTypeContextGenerator]]; + + let contextArray = [filterProvider, ruleSetProvider, geolocationContext, eventTypeContextGenerator]; + let module = contexts.contextModule(); + module.addGlobalContexts(contextArray); + module.clearGlobalContexts(); + assert.strictEqual(module.getGlobalPrimitives().length, 0, + 'Correct number of primitives added'); + assert.strictEqual(module.getConditionalProviders().length, 0, + 'Correct number of conditional providers added'); + }, + + "Get applicable contexts": function () { + let geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude' : 55.1 + } + }; + + function eventTypeContextGenerator(args) { + let context = {}; + context['schema'] = 'iglu:com.snowplowanalytics.snowplow/some_context/jsonschema/1-0-1'; + context['data'] = {}; + context['data']['osType'] = 'ubuntu'; + context['data']['osVersion'] = '2018.04'; + context['data']['deviceManufacturer'] = 'ASUS'; + context['data']['eventType'] = String(args['eventType']); + return context; + } + + let unstructuredEventRuleset = { + accept: ['iglu:com.acme_company/*/jsonschema/*-*-*'] + }; + + let filterFunction = function (args) { + return args['eventType'] === 'ue'; + }; + + let filterProvider = [filterFunction, [geolocationContext, eventTypeContextGenerator]]; + let ruleSetProvider = [unstructuredEventRuleset, [geolocationContext, eventTypeContextGenerator]]; + + let eventJson = { + e: 'ue', + ue_px: { + schema: 'iglu:com.snowplowanalytics.snowplow/unstructured_event/jsonschema/1-0-0', + data: { + schema: 'iglu:com.acme_company/some_event/jsonschema/1-0-0', + data: {} + } + } + }; + let contextArray = [filterProvider, ruleSetProvider, geolocationContext, eventTypeContextGenerator]; + let module = contexts.contextModule(); + let event = payload.payloadBuilder(false); + for (let property in eventJson) { + if (eventJson.hasOwnProperty(property)) { + event.add(property, eventJson[property]); + } + } + + module.addGlobalContexts(contextArray); + assert.strictEqual(module.getApplicableContexts(event).length, 6, + 'Correct number of contexts returned'); + } + }); + +}); diff --git a/snowplow-javascript-tracker/core/tests/unit/core.js b/snowplow-javascript-tracker/core/tests/unit/core.js new file mode 100644 index 0000000000..21f22173ca --- /dev/null +++ b/snowplow-javascript-tracker/core/tests/unit/core.js @@ -0,0 +1,747 @@ +/* + * JavaScript tracker core for Snowplow: tests/integration.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +define([ + "intern!object", + "intern/chai!assert", + "intern/chai!expect", + "intern/dojo/node!../../lib/core.js" +], function (registerSuite, assert, expect, core) { + + var unstructEventSchema = 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0'; + var tracker = core.trackerCore(false); + + // Doesn't check true timestamp + function compare(result, expected, message) { + result = result.build(); + assert.ok(result['eid'], 'A UUID should be attached to all events'); + delete result['eid']; + assert.ok(result['dtm'], 'A timestamp should be attached to all events'); + delete result['dtm']; + assert.deepEqual(result, expected, message); + } + + registerSuite({ + name: "Tracking events", + + "Track a page view": function () { + var url = 'http://www.example.com'; + var page = 'title page'; + var expected = { + e: 'pv', + url: url, + page: page + }; + compare(tracker.trackPageView(url, page), expected, 'A page view should be tracked correctly'); + }, + + "Track a page ping": function () { + var url = 'http://www.example.com'; + var referer = 'http://www.google.com'; + var expected = { + e: 'pp', + url: url, + refr: referer, + pp_mix: '1', + pp_max: '2', + pp_miy: '3', + pp_may: '4' + }; + + compare(tracker.trackPagePing(url, null, referer, 1, 2, 3, 4), expected, 'A page ping should be tracked correctly'); + }, + + "Track a structured event": function () { + var expected = { + e: 'se', + se_ca: 'cat', + se_ac: 'act', + se_la: 'lab', + se_pr: 'prop', + se_va: 'val' + }; + + compare(tracker.trackStructEvent('cat', 'act', 'lab', 'prop', 'val'), expected, 'A structured event should be tracked correctly'); + }, + + "Track an ecommerce transaction event": function () { + var orderId = 'ak0008'; + var totalValue = 50; + var taxValue = 6; + var shipping = 0; + var city = 'Phoenix'; + var state = 'Arizona'; + var country = 'USA'; + var currency = 'USD'; + var expected = { + e: 'tr', + tr_id: orderId, + tr_tt: totalValue, + tr_tx: taxValue, + tr_sh: shipping, + tr_ci: city, + tr_st: state, + tr_co: country, + tr_cu: currency + }; + + compare(tracker.trackEcommerceTransaction(orderId, null, totalValue, taxValue, shipping, city, state, country, currency), expected, 'A transaction event should be tracked correctly'); + }, + + "Track an ecommerce transaction item event": function () { + var orderId = 'ak0008'; + var sku = '4q345'; + var price = 17; + var quantity = 2; + var name = 'red shoes'; + var category = 'clothing'; + var currency = 'USD'; + var expected = { + e: 'ti', + ti_id: orderId, + ti_sk: sku, + ti_nm: name, + ti_ca: category, + ti_pr: price, + ti_qu: quantity, + ti_cu: currency + }; + + compare(tracker.trackEcommerceTransactionItem(orderId, sku, name, category, price, quantity, currency), expected, 'A transaction item event should be tracked correctly'); + }, + + "Track an unstructured event": function () { + var inputJson = { + schema: 'iglu:com.acme/user/jsonschema/1-0-1', + data: { + name: 'Eric' + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackUnstructEvent(inputJson), expected, 'An unstructured event should be tracked correctly'); + }, + + "Track a self-describing event": function () { + var inputJson = { + schema: 'iglu:com.acme/user/jsonschema/1-0-1', + data: { + name: 'Eric' + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackSelfDescribingEvent(inputJson), expected, 'A self-describing event should be tracked correctly'); + }, + + "Track a link click": function () { + var targetUrl = 'http://www.example.com'; + var elementId = 'first header'; + var elementClasses = ['header']; + var elementContent = 'link'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: { + targetUrl: targetUrl, + elementId: elementId, + elementClasses: elementClasses, + elementContent: elementContent + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackLinkClick(targetUrl, elementId, elementClasses, null, elementContent), expected, 'A link click should be tracked correctly'); + }, + + "Track a screen view": function () { + var name = 'intro'; + var id = '7398-4352-5345-1950'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0', + data: { + name: name, + id: id + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackScreenView(name, id), expected, 'A screen view should be tracked correctly'); + }, + + "Track an ad impression": function () { + var impressionId = 'a0e8f8780ab3'; + var costModel = 'cpc'; + var cost = 0.5; + var targetUrl = 'http://adsite.com'; + var bannerId = '123'; + var zoneId = 'zone-14'; + var advertiserId = 'ad-company'; + var campaignId = 'campaign-7592'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0', + data: { + impressionId: impressionId, + costModel: costModel, + cost: cost, + targetUrl: targetUrl, + bannerId: bannerId, + zoneId: zoneId, + advertiserId: advertiserId, + campaignId: campaignId + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackAdImpression(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId), expected, 'An ad impression should be tracked correctly'); + }, + + "Track an ad click": function () { + var targetUrl = 'http://adsite.com'; + var clickId = 'click-321'; + var costModel = 'cpc'; + var cost = 0.5; + var bannerId = '123'; + var zoneId = 'zone-14'; + var impressionId = 'a0e8f8780ab3'; + var advertiserId = 'ad-company'; + var campaignId = 'campaign-7592'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0', + data: { + targetUrl: targetUrl, + clickId: clickId, + costModel: costModel, + cost: cost, + bannerId: bannerId, + zoneId: zoneId, + impressionId: impressionId, + advertiserId: advertiserId, + campaignId: campaignId + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackAdClick(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId), expected, 'An ad click should be tracked correctly'); + }, + + "Track an ad conversion": function () { + var conversionId = 'conversion-59'; + var costModel = 'cpc'; + var cost = 0.5; + var category = 'cat'; + var action = 'act'; + var property = 'prop'; + var initialValue = 7; + var advertiserId = 'ad-company'; + var campaignId = 'campaign-7592'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_conversion/jsonschema/1-0-0', + data: { + conversionId: conversionId, + costModel: costModel, + cost: cost, + category: category, + action: action, + property: property, + initialValue: initialValue, + advertiserId: advertiserId, + campaignId: campaignId + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackAdConversion(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId), expected, 'An ad conversion should be tracked correctly'); + }, + + "Track a social interaction": function () { + var action = 'like'; + var network = 'facebook'; + var target = 'status-0000345345'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/social_interaction/jsonschema/1-0-0', + data: { + action: action, + network: network, + target: target + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackSocialInteraction(action, network, target), expected); + }, + + + "Track an add-to-cart event": function () { + var sku = '4q345'; + var unitPrice = 17; + var quantity = 2; + var name = 'red shoes'; + var category = 'clothing'; + var currency = 'USD'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/add_to_cart/jsonschema/1-0-0', + data: { + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackAddToCart(sku, name, category, unitPrice, quantity, currency), expected); + }, + + "Track a remove-from-cart event": function () { + var sku = '4q345'; + var unitPrice = 17; + var quantity = 2; + var name = 'red shoes'; + var category = 'clothing'; + var currency = 'USD'; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/remove_from_cart/jsonschema/1-0-0', + data: { + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackRemoveFromCart(sku, name, category, unitPrice, quantity, currency), expected); + }, + + "Track a form focus event": function () { + var formId = "parent"; + var elementId = "child"; + var nodeName = "INPUT"; + var type = "text"; + var elementClasses = ["important"]; + var value = "male"; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/focus_form/jsonschema/1-0-0', + data: { + formId: formId, + elementId: elementId, + nodeName: nodeName, + type: type, + elementClasses: elementClasses, + value: value + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackFormFocusOrChange('focus_form', formId, elementId, nodeName, type, elementClasses, value), expected); + }, + + "Track a form change event": function () { + var formId = "parent"; + var elementId = "child"; + var nodeName = "INPUT"; + var type = "text"; + var elementClasses = ["important"]; + var value = "male"; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0', + data: { + formId: formId, + elementId: elementId, + nodeName: nodeName, + type: type, + elementClasses: elementClasses, + value: value + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackFormFocusOrChange('change_form', formId, elementId, nodeName, type, elementClasses, value), expected); + }, + + "Track a form submission event": function () { + var formId = "parent"; + var formClasses = ["formclass"]; + var elements = [{ + name: "gender", + value: "male", + nodeName: "INPUT", + type: "text" + }]; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0', + data: { + formId: formId, + formClasses: formClasses, + elements: elements + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackFormSubmission(formId, formClasses, elements), expected); + }, + + "Track a site seach event": function () { + var terms = ["javascript", "development"]; + var filters = { + "safeSearch": true, + "category": "books" + }; + var totalResults = 35; + var pageResults = 10; + + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/site_search/jsonschema/1-0-0', + data: { + terms: terms, + filters: filters, + totalResults: totalResults, + pageResults: pageResults + } + }; + + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }) + }; + + compare(tracker.trackSiteSearch(terms, filters, totalResults, pageResults), expected); + }, + + "Track a consent withdrawn event": function () { + var all = false; + var id = 1234; + var version = 2; + var name = "consent_form"; + var description = "user withdraws consent for form"; + var tstamp = 1000000000000; + var inputContext = [{ + schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0', + data: { + id: id, + version: version, + name: name, + description: description + } + }]; + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/consent_withdrawn/jsonschema/1-0-0', + data: { + all: all + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }), + co: JSON.stringify({ + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: inputContext + }) + }; + compare(tracker.trackConsentWithdrawn(all, id, version, name, description, [], tstamp), expected); + }, + + "Track a consent granted event": function () { + var id = 1234; + var version = 2; + var name = "consent_form"; + var description = "user grants consent for form"; + var tstamp = 1000000000000; + var expiry = "01 January, 1970 00:00:00 Universal Time (UTC)"; + var inputContext = [{ + schema: 'iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0', + data: { + id: id, + version: version, + name: name, + description: description + } + }]; + var inputJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/consent_granted/jsonschema/1-0-0', + data: { + expiry: expiry + } + }; + var expected = { + e: 'ue', + ue_pr: JSON.stringify({ + schema: unstructEventSchema, + data: inputJson + }), + co: JSON.stringify({ + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: inputContext + }) + }; + compare(tracker.trackConsentGranted(id, version, name, description, expiry, [], tstamp), expected); + }, + + "Track a page view with custom context": function () { + var url = 'http://www.example.com'; + var page = 'title page'; + var inputContext = [{ + schema: 'iglu:com.acme/user/jsonschema/1-0-0', + data: { + userType: 'tester', + userName: 'Jon' + } + }]; + var expected = { + e: 'pv', + url: url, + page: page, + co: JSON.stringify({ + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: inputContext + }) + }; + compare(tracker.trackPageView(url, page, null, inputContext), expected, 'A custom context should be attached correctly'); + }, + + "Track a page view with a timestamp": function () { + var tstamp = 1000000000000; + + assert.strictEqual(tracker.trackPageView('http://www.example.com', null, null, null, tstamp).build()['dtm'], '1000000000000', 'A timestamp should be attached correctly'); + }, + + "Add individual name-value pairs to the payload": function () { + var tracker = core.trackerCore(false); + var url = 'http://www.example.com'; + var expected = { + e: 'pv', + url: url, + tna: 'cf', + tv: 'js-2.0.0' + }; + tracker.addPayloadPair('tna', 'cf'); + tracker.addPayloadPair('tv', 'js-2.0.0'); + + compare(tracker.trackPageView(url), expected, 'Payload name-value pairs should be set correctly'); + }, + + "Add a dictionary of name-value pairs to the payload": function () { + var tracker = core.trackerCore(false); + var url = 'http://www.example.com'; + var expected = { + e: 'pv', + url: url, + tv: 'js-2.0.0', + tna: 'cf', + aid: 'cf325' + }; + tracker.addPayloadPair('tv', 'js-2.0.0'); + tracker.addPayloadDict({ + tna: 'cf', + aid: 'cf325' + }); + + compare(tracker.trackPageView(url), expected, 'Payload name-value pairs should be set correctly'); + }, + + "Reset payload name-value pairs": function () { + var tracker = core.trackerCore(false); + var url = 'http://www.example.com'; + var expected = { + e: 'pv', + url: url, + tna: 'cf' + }; + tracker.addPayloadPair('tna', 'mistake'); + tracker.resetPayloadPairs({'tna': 'cf'}); + + compare(tracker.trackPageView(url), expected, 'Payload name-value pairs should be reset correctly'); + }, + + "Execute a callback": function () { + var callbackTarget; + var tracker = core.trackerCore(false, function (payload) { + callbackTarget = payload; + }); + var url = 'http://www.example.com'; + var expected = { + e: 'pv', + url: url + }; + tracker.trackPageView(url); + + compare(callbackTarget, expected, 'The callback should be executed correctly'); + }, + + "Use setter methods": function () { + var tracker = core.trackerCore(false); + tracker.setTrackerVersion('js-3.0.0'); + tracker.setTrackerNamespace('cf1'); + tracker.setAppId('my-app'); + tracker.setPlatform('web'); + tracker.setUserId('jacob'); + tracker.setScreenResolution(400, 200); + tracker.setViewport(500, 800); + tracker.setColorDepth(24); + tracker.setTimezone('Europe London'); + tracker.setIpAddress('37.151.33.154'); + var url = 'http://www.example.com'; + var page = 'title page'; + var expected = { + e: 'pv', + url: url, + page: page, + tna: 'cf1', + tv: 'js-3.0.0', + aid: 'my-app', + p: 'web', + uid: 'jacob', + res: '400x200', + vp: '500x800', + cd: 24, + tz: 'Europe London', + ip: '37.151.33.154' + }; + + compare(tracker.trackPageView(url, page), expected, 'setXXX methods should work correctly'); + }, + + "Set true timestamp": function () { + var url = 'http://www.example.com'; + var page = 'title page'; + var result = tracker.trackPageView(url, page, null, null, { type: 'ttm', value: 1477403862 }).build(); + assert('ttm' in result, 'ttm should be attached'); + assert.strictEqual(result['ttm'] , '1477403862', 'ttm should be attached as is'); + assert(!('dtm' in result), 'dtm should absent'); + }, + + "Set device timestamp as ADT": function () { + + var inputJson = { + schema: 'iglu:com.acme/user/jsonschema/1-0-1', + data: { + name: 'Eric' + } + }; + + // Object structure should be enforced by typesystem + var result = tracker.trackSelfDescribingEvent(inputJson, [inputJson], {type: 'dtm', value: 1477403869}).build(); + assert('dtm' in result, 'dtm should be attached'); + assert.strictEqual(result['dtm'] , '1477403869', 'dtm should be attached as is'); + assert(!('ttm' in result), 'ttm should absent'); + } + }); + +}); diff --git a/snowplow-javascript-tracker/core/tests/unit/payload.js b/snowplow-javascript-tracker/core/tests/unit/payload.js new file mode 100644 index 0000000000..6b2a654a07 --- /dev/null +++ b/snowplow-javascript-tracker/core/tests/unit/payload.js @@ -0,0 +1,121 @@ +/* + * JavaScript tracker for Snowplow: tests/payload.js + * + * Copyright (c) 2014-2016 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +define([ + 'intern!object', + 'intern/chai!assert', + 'intern/dojo/node!../../lib/payload.js' +], function (registerSuite, assert, payload) { + + + var sampleJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: [ + { + schema: "iglu:com.example_company/page/jsonschema/1-2-1", + data: { + pageType: 'test', + lastUpdated: new Date(Date.UTC(2014, 1, 26)) + } + }, + { + schema: "iglu:com.example_company/user/jsonschema/2-0-0", + data: { + userType: 'tester' + } + } + ] + }; + + var expectedPayloads = [ + {co: '{"schema":"iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0","data":[{"schema":"iglu:com.example_company/page/jsonschema/1-2-1","data":{"pageType":"test","lastUpdated":"2014-02-26T00:00:00.000Z"}},{"schema":"iglu:com.example_company/user/jsonschema/2-0-0","data":{"userType":"tester"}}]}'}, + {cx: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy9jb250ZXh0cy9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6W3sic2NoZW1hIjoiaWdsdTpjb20uZXhhbXBsZV9jb21wYW55L3BhZ2UvanNvbnNjaGVtYS8xLTItMSIsImRhdGEiOnsicGFnZVR5cGUiOiJ0ZXN0IiwibGFzdFVwZGF0ZWQiOiIyMDE0LTAyLTI2VDAwOjAwOjAwLjAwMFoifX0seyJzY2hlbWEiOiJpZ2x1OmNvbS5leGFtcGxlX2NvbXBhbnkvdXNlci9qc29uc2NoZW1hLzItMC0wIiwiZGF0YSI6eyJ1c2VyVHlwZSI6InRlc3RlciJ9fV19'} + ]; + + registerSuite({ + + name: 'Payload test', + + 'Identify a JSON': function () { + var json = { + 'name': 'john', + 'properties': { + 'age': 30, + 'languages': ['English', 'French'] + } + }; + + assert.strictEqual(payload.isJson(json), true, 'JSON should be identified'); + }, + + 'Identify a non-JSON': function () { + var nonJson = function () {}; + + assert.strictEqual(payload.isJson(nonJson), false, 'Non-JSON should be rejected'); + }, + + 'Identify an empty JSON': function () { + var emptyJson = {}; + + assert.strictEqual(payload.isNonEmptyJson(emptyJson), false, '{} should be identified as empty'); + }, + + 'Build a payload': function () { + var sb = payload.payloadBuilder(false); + sb.add('e', 'pv'); + sb.add('tv', 'js-2.0.0'); + + assert.deepEqual(sb.build(), {e: 'pv', tv: 'js-2.0.0'}, 'Individual name-value pairs should be added to the payload'); + }, + + 'Do not add undefined values to a payload': function () { + var sb = payload.payloadBuilder(false); + sb.add('e', undefined); + + assert.deepEqual(sb.build(), {}, 'Undefined values should not be added to the payload'); + }, + + 'Do not add null values to a payload': function () { + var sb = payload.payloadBuilder(false); + sb.add('e', null); + + assert.deepEqual(sb.build(), {}, 'Null values should not be added to the payload'); + }, + + 'Add a dictionary of name-value pairs to the payload': function () { + var sb = payload.payloadBuilder(false); + sb.addDict({ + 'e': 'pv', + 'tv': 'js-2.0.0' + }); + + assert.deepEqual(sb.build(), {e: 'pv', tv: 'js-2.0.0'}, 'A dictionary of name-value pairs should be added to the payload'); + }, + + 'Add a JSON to the payload': function () { + var sb = payload.payloadBuilder(false); + sb.addJson('cx', 'co', sampleJson); + + assert.deepEqual(sb.build(), expectedPayloads[0], 'JSON should be added correctly'); + }, + + 'Add a base 64 encoded JSON to the payload': function () { + var sb = payload.payloadBuilder(true); + sb.addJson('cx', 'co', sampleJson); + + assert.deepEqual(sb.build(), expectedPayloads[1], 'JSON should be encoded correctly'); + } + }); +}); diff --git a/snowplow-javascript-tracker/core/tsconfig.json b/snowplow-javascript-tracker/core/tsconfig.json new file mode 100644 index 0000000000..5269c93927 --- /dev/null +++ b/snowplow-javascript-tracker/core/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "isolatedModules": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "noImplicitAny": false, + "noImplicitUseStrict": false, + "strictNullChecks": true, + "removeComments": true, + "noLib": false, + "preserveConstEnums": true, + "suppressImplicitAnyIndexErrors": true + }, + "exclude": [ + "node_modules" + ], + "compileOnSave": true, + "buildOnSave": false, + "atom": { + "rewriteTsconfig": false + } +} diff --git a/snowplow-javascript-tracker/core/tslint.json b/snowplow-javascript-tracker/core/tslint.json new file mode 100644 index 0000000000..61b62f2a38 --- /dev/null +++ b/snowplow-javascript-tracker/core/tslint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "indent": [true, "tabs"] + } +} diff --git a/snowplow-javascript-tracker/dist/.gitignore b/snowplow-javascript-tracker/dist/.gitignore new file mode 100644 index 0000000000..9170fd96c9 --- /dev/null +++ b/snowplow-javascript-tracker/dist/.gitignore @@ -0,0 +1,4 @@ +# Assembled files +bundle.js +snowplow.js +sp.js diff --git a/snowplow-javascript-tracker/examples/ads/async.html b/snowplow-javascript-tracker/examples/ads/async.html new file mode 100644 index 0000000000..643d3cf0db --- /dev/null +++ b/snowplow-javascript-tracker/examples/ads/async.html @@ -0,0 +1,164 @@ + + + + + Asynchronous ad tracking examples for snowplow.js + + +

For each banner, viewing that banner fires an ad impression event and clicking it fires an ad click event.

+ +

Click the first banner and on the button that appears to fire an ad conversion event.

+ + +

Asynchronous ad tracking examples for snowplow.js

+ +

+ + Banner ID 23, zone ID 7, campaign ID 12, advertiser ID 201 + + + +

+ +

+ + Banner ID 127, no campaign or advertiser ID set + + + +

+ +

+ + Banner ID 56ea2819ffc7da75f7e, no campaign, advertiser or user ID set + + + +

+ + + diff --git a/snowplow-javascript-tracker/examples/ads/images/banner-1.png b/snowplow-javascript-tracker/examples/ads/images/banner-1.png new file mode 100644 index 0000000000..897cdfdbe3 Binary files /dev/null and b/snowplow-javascript-tracker/examples/ads/images/banner-1.png differ diff --git a/snowplow-javascript-tracker/examples/ads/images/banner-2.png b/snowplow-javascript-tracker/examples/ads/images/banner-2.png new file mode 100644 index 0000000000..689d0ee063 Binary files /dev/null and b/snowplow-javascript-tracker/examples/ads/images/banner-2.png differ diff --git a/snowplow-javascript-tracker/examples/ads/images/banner-3.png b/snowplow-javascript-tracker/examples/ads/images/banner-3.png new file mode 100644 index 0000000000..6c235173b5 Binary files /dev/null and b/snowplow-javascript-tracker/examples/ads/images/banner-3.png differ diff --git a/snowplow-javascript-tracker/examples/web/async-large.html b/snowplow-javascript-tracker/examples/web/async-large.html new file mode 100644 index 0000000000..6bcbb21496 --- /dev/null +++ b/snowplow-javascript-tracker/examples/web/async-large.html @@ -0,0 +1,277 @@ + + + + + Large asynchronous website/webapp examples for snowplow.js + + + + + + + + + + + + + + + +

Large_asynchronous_examples_for_snowplow.js

+

This shows how two users can simultaneously use Snowplow on the same page without any clash

+

Warning: if your browser's Do Not Track feature is enabled and respectDoNotTrack is enabled, all tracking will be prevented.

+

If you are viewing the page using a file URL, you must edit the script URL in the Snowplow tag to include an http scheme. Otherwise a file scheme will be inferred and the page will attempt to load sp.js from the local filesystem..

+

Press the buttons below to trigger individual tracking events:
+
+
+
+
+
+
+
+ +

+

+ Middle click the links to trigger link click tracking:
+ + Link +
+ Link ignored by one user +

+ +

+ Fill in the form fields and click the button to trigger form tracking: +

+
+ Male
+ Female
+
+
+
+
+ Add me to the mailing list +
+ +
+

+ + + + diff --git a/snowplow-javascript-tracker/examples/web/async-medium.html b/snowplow-javascript-tracker/examples/web/async-medium.html new file mode 100644 index 0000000000..2a320eb5bb --- /dev/null +++ b/snowplow-javascript-tracker/examples/web/async-medium.html @@ -0,0 +1,191 @@ + + + + + Medium asynchronous website/webapp examples for snowplow.js + + + + + + + + + + + +

Medium_asynchronous_examples_for_snowplow.js

+

Two tracker namespaces are instantiated on this page.

+

Warning: if your browser's Do Not Track feature is enabled and respectDoNotTrack is enabled, all tracking will be prevented.

+

If you are viewing the page using a file URL, you must edit the script URL in the Snowplow tag to include an http scheme. Otherwise a file scheme will be inferred and the page will attempt to load sp.js from the local filesystem..

+

Press the buttons below to trigger individual tracking events:
+
+
+
+ +

+ Both trackers will track this link +
+ Only the cf tracker will track this link +
+ Neither tracker will track this link + + + + diff --git a/snowplow-javascript-tracker/examples/web/async-small.html b/snowplow-javascript-tracker/examples/web/async-small.html new file mode 100644 index 0000000000..5770d43162 --- /dev/null +++ b/snowplow-javascript-tracker/examples/web/async-small.html @@ -0,0 +1,168 @@ + + + + + Small asynchronous website/webapp examples for snowplow.js + + + + + + + + + + + +

Small_asynchronous_examples_for_snowplow.js

+ +

Warning: if your browser's Do Not Track feature is enabled and respectDoNotTrack is enabled, all tracking will be prevented.

+

If you are viewing the page using a file URL, you must edit the script URL in the Snowplow tag to include an http scheme. Otherwise a file scheme will be inferred and the page will attempt to load sp.js from the local filesystem..

+ +

Press the buttons below to trigger individual tracking events:
+
+
+
+ +

+ Link +
+ Ignored link + + + + diff --git a/snowplow-javascript-tracker/examples/web/sync.html b/snowplow-javascript-tracker/examples/web/sync.html new file mode 100644 index 0000000000..7a43cd53ce --- /dev/null +++ b/snowplow-javascript-tracker/examples/web/sync.html @@ -0,0 +1,74 @@ + + + + + Synchronous website/webapp examples for snowplow.js + + + + + + + + + + + + +

Synchronous examples for snowplow.js

+

Warning: if your browser's Do Not Track feature is enabled and respectDoNotTrack is enabled, all tracking will be prevented.

+

If you are viewing the page using a file URL, you must edit the script URL in the Snowplow tag to include an http scheme. Otherwise a file scheme will be inferred and the page will attempt to load sp.js from the local filesystem..

+

Press the buttons below to trigger individual tracking events:
+
+
+ +

+ + + diff --git a/snowplow-javascript-tracker/local_test.sh b/snowplow-javascript-tracker/local_test.sh new file mode 100755 index 0000000000..12839b5379 --- /dev/null +++ b/snowplow-javascript-tracker/local_test.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +cd core +grunt +cd .. +grunt local +if [[ "$(uname)" == "Darwin" ]]; then + (sleep 1 && open http:127.0.0.1:8000/integration.html)& +elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then + (sleep 1 && xdg-open http:127.0.0.1:8000/integration.html)& +elif [[ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]]; then + : +elif [[ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]]; then + : +fi +python3 ./tests/local/http-server.py diff --git a/snowplow-javascript-tracker/npm-shrinkwrap.json b/snowplow-javascript-tracker/npm-shrinkwrap.json new file mode 100644 index 0000000000..3b45c13f79 --- /dev/null +++ b/snowplow-javascript-tracker/npm-shrinkwrap.json @@ -0,0 +1,6046 @@ +{ + "name": "snowplow-tracker", + "version": "2.9.3", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.1.6.tgz", + "integrity": "sha512-Hz6PJT6e44iUNpAn8AoyAs6B3bl60g7MJQaI0rZEar6ECzh6+srYO1xlIdssio34mPaUtAb1y+XlkkSJzok3yw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.1.6", + "@babel/helpers": "^7.1.5", + "@babel/parser": "^7.1.6", + "@babel/template": "^7.1.2", + "@babel/traverse": "^7.1.6", + "@babel/types": "^7.1.6", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.10", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.6.tgz", + "integrity": "sha512-brwPBtVvdYdGxtenbQgfCdDPmtkmUBZPjUoK5SXJEBuHaA5BCubh9ly65fzXz7R6o5rA76Rs22ES8Z+HCc0YIQ==", + "dev": true, + "requires": { + "@babel/types": "^7.1.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.10", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz", + "integrity": "sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.0.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-define-map": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz", + "integrity": "sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/types": "^7.0.0", + "lodash": "^4.17.10" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz", + "integrity": "sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", + "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.1.0.tgz", + "integrity": "sha512-0JZRd2yhawo79Rcm4w0LwSMILFmFXjugG3yqf+P/UsKsRS1mJCmMwwlHDlMg7Avr9LrvSpp4ZSULO9r8jpCzcw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0", + "lodash": "^4.17.10" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz", + "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz", + "integrity": "sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", + "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-wrap-function": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.1.0.tgz", + "integrity": "sha512-R6HU3dete+rwsdAfrOzTlE9Mcpk4RjU3aX3gi9grtmugQY0u79X7eogUvfXA5sI81Mfq1cn6AgxihfN33STjJA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helpers": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.1.5.tgz", + "integrity": "sha512-2jkcdL02ywNBry1YNFAH/fViq4fXG0vdckHqeJk+75fpQ2OH+Az6076tX/M0835zA45E0Cqa6pV5Kiv9YOqjEg==", + "dev": true, + "requires": { + "@babel/template": "^7.1.2", + "@babel/traverse": "^7.1.5", + "@babel/types": "^7.1.5" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.6.tgz", + "integrity": "sha512-dWP6LJm9nKT6ALaa+bnL247GHHMWir3vSlZ2+IHgHgktZQx0L3Uvq2uAWcuzIe+fujRsYWBW2q622C5UvGK9iQ==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz", + "integrity": "sha512-Fq803F3Jcxo20MXUSDdmZZXrPe6BWyGcWBPPNB/M7WaUYESKDeKMOGIxEzQOjGSmW/NWb6UaPZrtTB2ekhB/ew==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.0.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz", + "integrity": "sha512-kfVdUkIAGJIVmHmtS/40i/fg/AGnw/rsZBCaapY5yjeO5RA9m165Xbw9KMOu2nqXP5dTFjEjHdfNdoVcHv133Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.0.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz", + "integrity": "sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.0.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0.tgz", + "integrity": "sha512-JPqAvLG1s13B/AuoBjdBYvn38RqW6n1TzrQO839/sIpqLpbnXKacsAgpZHzLD83Sm8SDXMkkrAvEnJ25+0yIpw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.0.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz", + "integrity": "sha512-tM3icA6GhC3ch2SkmSxv7J/hCWKISzwycub6eGsDrFDgukD4dZ/I+x81XgW0YslS6mzNuQ1Cbzh5osjIMgepPQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0", + "regexpu-core": "^4.2.0" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0.tgz", + "integrity": "sha512-im7ged00ddGKAjcZgewXmp1vxSZQQywuQXe2B1A7kajjZmDeY/ekMPmWr9zJgveSaQH0k7BcGrojQhcK06l0zA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz", + "integrity": "sha512-UlSfNydC+XLj4bw7ijpldc1uZ/HB84vw+U6BTuqMdIEmz/LDe63w/GHtpQMdXWdqQZFeAI9PjnHe/vDhwirhKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0.tgz", + "integrity": "sha512-5A0n4p6bIiVe5OvQPxBnesezsgFJdHhSs3uFSvaPdMqtsovajLZ+G2vZyvNe10EzJBWWo3AcHGKhAFUxqwp2dw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0.tgz", + "integrity": "sha512-Wc+HVvwjcq5qBg1w5RG9o9RVzmCaAg/Vp0erHCKpAYV8La6I94o4GQAmFYNmkzoMO6gzoOSulpKeSSz6mPEoZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz", + "integrity": "sha512-2EZDBl1WIO/q4DIkIp4s86sdp4ZifL51MoIviLY/gG/mLSuOIEg7J8o6mhbxOTvUJkaN50n+8u41FVsr5KLy/w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.1.0.tgz", + "integrity": "sha512-rNmcmoQ78IrvNCIt/R9U+cixUHeYAzgusTFgIAv+wQb9HJU4szhpDD6e5GCACmj/JP5KxuCwM96bX3L9v4ZN/g==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0.tgz", + "integrity": "sha512-AOBiyUp7vYTqz2Jibe1UaAWL0Hl9JUXEgjFvvvcSc9MVDItv46ViXFw2F7SVt1B5k+KWjl44eeXOAk3UDEaJjQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.1.5.tgz", + "integrity": "sha512-jlYcDrz+5ayWC7mxgpn1Wj8zj0mmjCT2w0mPIMSwO926eXBRxpEgoN/uQVRBfjtr8ayjcmS+xk2G1jaP8JjMJQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.10" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.1.0.tgz", + "integrity": "sha512-rNaqoD+4OCBZjM7VaskladgqnZ1LO6o2UxuWSDzljzW21pN1KXkB7BstAVweZdxQkHAujps5QMNOTWesBciKFg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-define-map": "^7.1.0", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0.tgz", + "integrity": "sha512-ubouZdChNAv4AAWAgU7QKbB93NU5sHwInEWfp+/OzJKA02E6Woh9RVoX4sZrbRwtybky/d7baTUqwFx+HgbvMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.1.3.tgz", + "integrity": "sha512-Mb9M4DGIOspH1ExHOUnn2UUXFOyVTiX84fXCd+6B5iWrQg/QMeeRmSwpZ9lnjYLSXtZwiw80ytVMr3zue0ucYw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0.tgz", + "integrity": "sha512-00THs8eJxOJUFVx1w8i1MBF4XH4PsAjKjQ1eqN/uCH3YKwP21GCKfrn6YZFZswbOk9+0cw1zGQPHVc1KBlSxig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0", + "regexpu-core": "^4.1.3" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0.tgz", + "integrity": "sha512-w2vfPkMqRkdxx+C71ATLJG30PpwtTpW7DDdLqYt2acXU7YjztzeWW2Jk1T6hKqCLYCcEA5UQM/+xTAm+QCSnuQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.1.0.tgz", + "integrity": "sha512-uZt9kD1Pp/JubkukOGQml9tqAeI8NkE98oZnHZ2qHRElmeKCodbTZgOEUtujSCSLhHSBWbzNiFSDIMC4/RBTLQ==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz", + "integrity": "sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.1.0.tgz", + "integrity": "sha512-VxOa1TMlFMtqPW2IDYZQaHsFrq/dDoIjgN098NowhexhZcz3UGlvPgZXuE1jEvNygyWyxRacqDpCZt+par1FNg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0.tgz", + "integrity": "sha512-1NTDBWkeNXgpUcyoVFxbr9hS57EpZYXpje92zv0SUzjdu3enaRwF/l3cmyRnXLtIdyJASyiS6PtybK+CgKf7jA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.1.0.tgz", + "integrity": "sha512-wt8P+xQ85rrnGNr2x1iV3DW32W8zrB6ctuBkYBbf5/ZzJY99Ob4MFgsZDFgczNU76iy9PWsy4EuxOliDjdKw6A==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.1.0.tgz", + "integrity": "sha512-wtNwtMjn1XGwM0AXPspQgvmE6msSJP15CX2RVfpTSTNPLhKhaOjaIfBaVfj4iUZ/VrFSodcFedwtPg/NxwQlPA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.1.3.tgz", + "integrity": "sha512-PvTxgjxQAq4pvVUZF3mD5gEtVDuId8NtWkJsZLEJZMZAW3TvgQl1pmydLLN1bM8huHFVVU43lf0uvjQj9FRkKw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.1.0.tgz", + "integrity": "sha512-enrRtn5TfRhMmbRwm7F8qOj0qEYByqUvTttPEGimcBH4CJHphjyK1Vg7sdU7JjeEmgSpM890IT/efS2nMHwYig==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz", + "integrity": "sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.1.0.tgz", + "integrity": "sha512-/O02Je1CRTSk2SSJaq0xjwQ8hG4zhZGNjE8psTsSNPXyLRCODv7/PBozqT5AmQMzp7MI3ndvMhGdqp9c96tTEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.1.0.tgz", + "integrity": "sha512-vHV7oxkEJ8IHxTfRr3hNGzV446GAb+0hgbA7o/0Jd76s+YzccdWuTU296FOCOl/xweU4t/Ya4g41yWz80RFCRw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.1.0", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz", + "integrity": "sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==", + "dev": true, + "requires": { + "regenerator-transform": "^0.13.3" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0.tgz", + "integrity": "sha512-g/99LI4vm5iOf5r1Gdxq5Xmu91zvjhEG5+yZDJW268AZELAu4J1EiFLnkSG3yuUsZyOipVOVUKoGPYwfsTymhw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0.tgz", + "integrity": "sha512-L702YFy2EvirrR4shTj0g2xQp7aNwZoWNCkNu2mcoU0uyzMl0XRwDSwzB/xp6DSUFiBmEXuyAyEN16LsgVqGGQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0.tgz", + "integrity": "sha512-LFUToxiyS/WD+XEWpkx/XJBrUXKewSZpzX68s+yEOtIbdnsRjpryDw9U06gYc6klYEij/+KQVRnD3nz3AoKmjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0.tgz", + "integrity": "sha512-vA6rkTCabRZu7Nbl9DfLZE1imj4tzdWcg5vtdQGvj+OH9itNNB6hxuRMHuIY8SGnEt1T9g5foqs9LnrHzsqEFg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0.tgz", + "integrity": "sha512-1r1X5DO78WnaAIvs5uC48t41LLckxsYklJrZjNKcevyz83sF2l4RHbw29qrCPr/6ksFsdfRpT/ZgxNWHXRnffg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz", + "integrity": "sha512-uJBrJhBOEa3D033P95nPHu3nbFwFE9ZgXsfEitzoIXIwqAZWk7uXcg06yFKXz9FSxBH5ucgU/cYdX0IV8ldHKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0", + "regexpu-core": "^4.1.3" + } + }, + "@babel/preset-env": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.1.6.tgz", + "integrity": "sha512-YIBfpJNQMBkb6MCkjz/A9J76SNCSuGVamOVBgoUkLzpJD/z8ghHi9I42LQ4pulVX68N/MmImz6ZTixt7Azgexw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.1.0", + "@babel/plugin-proposal-json-strings": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.0.0", + "@babel/plugin-syntax-async-generators": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.0.0", + "@babel/plugin-transform-arrow-functions": "^7.0.0", + "@babel/plugin-transform-async-to-generator": "^7.1.0", + "@babel/plugin-transform-block-scoped-functions": "^7.0.0", + "@babel/plugin-transform-block-scoping": "^7.1.5", + "@babel/plugin-transform-classes": "^7.1.0", + "@babel/plugin-transform-computed-properties": "^7.0.0", + "@babel/plugin-transform-destructuring": "^7.0.0", + "@babel/plugin-transform-dotall-regex": "^7.0.0", + "@babel/plugin-transform-duplicate-keys": "^7.0.0", + "@babel/plugin-transform-exponentiation-operator": "^7.1.0", + "@babel/plugin-transform-for-of": "^7.0.0", + "@babel/plugin-transform-function-name": "^7.1.0", + "@babel/plugin-transform-literals": "^7.0.0", + "@babel/plugin-transform-modules-amd": "^7.1.0", + "@babel/plugin-transform-modules-commonjs": "^7.1.0", + "@babel/plugin-transform-modules-systemjs": "^7.0.0", + "@babel/plugin-transform-modules-umd": "^7.1.0", + "@babel/plugin-transform-new-target": "^7.0.0", + "@babel/plugin-transform-object-super": "^7.1.0", + "@babel/plugin-transform-parameters": "^7.1.0", + "@babel/plugin-transform-regenerator": "^7.0.0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0", + "@babel/plugin-transform-spread": "^7.0.0", + "@babel/plugin-transform-sticky-regex": "^7.0.0", + "@babel/plugin-transform-template-literals": "^7.0.0", + "@babel/plugin-transform-typeof-symbol": "^7.0.0", + "@babel/plugin-transform-unicode-regex": "^7.0.0", + "browserslist": "^4.1.0", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.2.tgz", + "integrity": "sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.1.2", + "@babel/types": "^7.1.2" + } + }, + "@babel/traverse": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.6.tgz", + "integrity": "sha512-CXedit6GpISz3sC2k2FsGCUpOhUqKdyL0lqNrImQojagnUMXf8hex4AxYFRuMkNGcvJX5QAFGzB5WJQmSv8SiQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.1.6", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.0.0", + "@babel/parser": "^7.1.6", + "@babel/types": "^7.1.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.10" + } + }, + "@babel/types": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.6.tgz", + "integrity": "sha512-DMiUzlY9DSjVsOylJssxLHSgj6tWM9PRFJOGW/RaOglVOK9nzTxoOMfTfRQXGUCUQ/HmlG2efwC+XqUEJ5ay4w==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.10", + "to-fast-properties": "^2.0.0" + } + }, + "JSON": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/JSON/-/JSON-1.0.0.tgz", + "integrity": "sha1-hoFTHCj4Q4oHVYn/BySCRuqWDYw=", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "acorn-node": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.2.tgz", + "integrity": "sha512-rIhNEZuNI8ibQcL7ANm/mGyPukIaZsRNX9psFNQURyJW0nu6k8wjSDld20z6v2mDBWqX13pIEnk9gGZJHIlEXg==", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.0", + "xtend": "^4.0.1" + } + }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "0.2.1", + "resolved": "http://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "http://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "0.1.22", + "resolved": "http://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sdk": { + "version": "2.1.50", + "resolved": "http://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.50.tgz", + "integrity": "sha1-Zd3CuEDmkCEIftNfKC1jKgUpShs=", + "dev": true, + "requires": { + "sax": "0.5.3", + "xml2js": "0.2.8", + "xmlbuilder": "0.4.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "binary-extensions": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", + "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", + "dev": true + }, + "bl": { + "version": "1.2.2", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-cookie-lite": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/browser-cookie-lite/-/browser-cookie-lite-0.3.1.tgz", + "integrity": "sha1-Y4hN0sDDnSf/1lmPK21Jjmj7IjM=" + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browserify": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.2.3.tgz", + "integrity": "sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^2.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cache-api": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/browserify-cache-api/-/browserify-cache-api-3.0.1.tgz", + "integrity": "sha1-liR+hT8Gj9bg1FzHPwuyzZd47wI=", + "dev": true, + "requires": { + "async": "^1.5.2", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + } + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-incremental": { + "version": "3.1.1", + "resolved": "http://registry.npmjs.org/browserify-incremental/-/browserify-incremental-3.1.1.tgz", + "integrity": "sha1-BxPLdYckemMqnwjPG9FpuHi2Koo=", + "dev": true, + "requires": { + "JSONStream": "^0.10.0", + "browserify-cache-api": "^3.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "JSONStream": { + "version": "0.10.0", + "resolved": "http://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", + "integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=", + "dev": true, + "requires": { + "jsonparse": "0.0.5", + "through": ">=2.2.7 <3" + } + }, + "jsonparse": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", + "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=", + "dev": true + } + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.4.tgz", + "integrity": "sha512-u5iz+ijIMUlmV8blX82VGFrB9ecnUg5qEt55CMZ/YJEhha+d8qpBfOFuutJ6F/VKRXjZoD33b6uvarpPxcl3RA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000899", + "electron-to-chromium": "^1.3.82", + "node-releases": "^1.0.1" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-to-vinyl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-to-vinyl/-/buffer-to-vinyl-1.1.0.tgz", + "integrity": "sha1-APFfruOreh3aLN5tkSG//dB7ImI=", + "dev": true, + "requires": { + "file-type": "^3.1.0", + "readable-stream": "^2.0.2", + "uuid": "^2.0.1", + "vinyl": "^1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30000912", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000912.tgz", + "integrity": "sha512-M3zAtV36U+xw5mMROlTXpAHClmPAor6GPKAMD5Yi7glCB5sbMPFtnQ3rGpk4XqPdUrrTIaVYSJZxREZWNy8QJg==", + "dev": true + }, + "chai": { + "version": "3.5.0", + "resolved": "http://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "^1.0.1", + "deep-eql": "^0.1.3", + "type-detect": "^1.0.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "charm": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/charm/-/charm-0.2.0.tgz", + "integrity": "sha1-us0G2HF3WTYvemYqHpZ691N/2os=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colors": { + "version": "0.6.2", + "resolved": "http://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-3.0.0.tgz", + "integrity": "sha1-rx3VDQbjv8QyRh033hGzjA2ZG+0=", + "dev": true, + "requires": { + "buffer-to-vinyl": "^1.0.0", + "concat-stream": "^1.4.6", + "decompress-tar": "^3.0.0", + "decompress-tarbz2": "^3.0.0", + "decompress-targz": "^3.0.0", + "decompress-unzip": "^3.0.0", + "stream-combiner2": "^1.1.1", + "vinyl-assign": "^1.0.1", + "vinyl-fs": "^2.2.0" + } + }, + "decompress-tar": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-3.1.0.tgz", + "integrity": "sha1-IXx4n5uURQ76rcXF5TeXj8MzxGY=", + "dev": true, + "requires": { + "is-tar": "^1.0.0", + "object-assign": "^2.0.0", + "strip-dirs": "^1.0.0", + "tar-stream": "^1.1.1", + "through2": "^0.6.1", + "vinyl": "^0.4.3" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "decompress-tarbz2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-3.1.0.tgz", + "integrity": "sha1-iyOTVoE1X58YnYclag+L3ZbZZm0=", + "dev": true, + "requires": { + "is-bzip2": "^1.0.0", + "object-assign": "^2.0.0", + "seek-bzip": "^1.0.3", + "strip-dirs": "^1.0.0", + "tar-stream": "^1.1.1", + "through2": "^0.6.1", + "vinyl": "^0.4.3" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "decompress-targz": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-3.1.0.tgz", + "integrity": "sha1-ssE9+YFmJomRtxXWRH9kLpaW9aA=", + "dev": true, + "requires": { + "is-gzip": "^1.0.0", + "object-assign": "^2.0.0", + "strip-dirs": "^1.0.0", + "tar-stream": "^1.1.1", + "through2": "^0.6.1", + "vinyl": "^0.4.3" + }, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true, + "requires": { + "clone": "^0.2.0", + "clone-stats": "^0.0.1" + } + } + } + }, + "decompress-unzip": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-3.4.0.tgz", + "integrity": "sha1-YUdbQVIGa74/7hL51inRX+ZHjus=", + "dev": true, + "requires": { + "is-zip": "^1.0.0", + "read-all-stream": "^3.0.0", + "stat-mode": "^0.2.0", + "strip-dirs": "^1.0.0", + "through2": "^2.0.0", + "vinyl": "^1.0.0", + "yauzl": "^2.2.1" + } + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "http://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + }, + "dependencies": { + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "shasum": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detective": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz", + "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.1.0.tgz", + "integrity": "sha1-eYpJOBqkZBUem08Ob/Kwmooa0j8=", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "digdug": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/digdug/-/digdug-1.5.2.tgz", + "integrity": "sha1-LnB4UV/CuEhTAY6bwtPsuZCKibM=", + "dev": true, + "requires": { + "decompress": "3.0.0", + "dojo": "2.0.0-alpha.7" + } + }, + "dojo": { + "version": "2.0.0-alpha.7", + "resolved": "http://registry.npmjs.org/dojo/-/dojo-2.0.0-alpha.7.tgz", + "integrity": "sha1-wrJdQ9j3LMycj+iaNJBqLSceXJE=", + "dev": true + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "duplexify": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", + "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "electron-to-chromium": { + "version": "1.3.85", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.85.tgz", + "integrity": "sha512-kWSDVVF9t3mft2OHVZy4K85X2beP6c6mFm3teFS/mLSDJpQwuFIWHrULCX+w6H1E55ZYmFRlT+ATAFRwhrYzsw==", + "dev": true + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.7.1.tgz", + "integrity": "sha1-MOz89mypjcZ80v0WKr626vqM5vw=", + "dev": true, + "requires": { + "esprima": "^1.2.2", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.5.0", + "source-map": "~0.2.0" + }, + "dependencies": { + "esprima": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz", + "integrity": "sha1-CZNQL+r2aBODJXVvMPmlH+7sEek=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "http://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "http://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "fast-levenshtein": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz", + "integrity": "sha1-AXjc3uAjuSkFGTrwlZ6KdjnP3Lk=", + "dev": true + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-type": { + "version": "3.9.0", + "resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fileset": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.2.1.tgz", + "integrity": "sha1-WI74lzxmI7KnbfRlEFaWuWqsgGc=", + "dev": true, + "requires": { + "glob": "5.x", + "minimatch": "2.x" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", + "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + } + } + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + }, + "dependencies": { + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + } + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "glob-stream": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "glob": "^5.0.3", + "glob-parent": "^3.0.0", + "micromatch": "^2.3.7", + "ordered-read-streams": "^0.3.0", + "through2": "^0.6.0", + "to-absolute-glob": "^0.1.1", + "unique-stream": "^2.0.2" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true, + "requires": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + } + } + }, + "globals": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", + "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "dev": true + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "http://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "lodash": { + "version": "0.9.2", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + } + } + }, + "grunt-aws": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/grunt-aws/-/grunt-aws-0.6.2.tgz", + "integrity": "sha1-JncoLBLwxdE1TNXr4g7gWpBcN+w=", + "dev": true, + "requires": { + "async": "~0.2.9", + "aws-sdk": "~2.1.29", + "lodash": "~1.3.1", + "mime": "~1.2.11" + }, + "dependencies": { + "async": { + "version": "0.2.10", + "resolved": "http://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + }, + "lodash": { + "version": "1.3.1", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", + "integrity": "sha1-pGY7U2hriV/wdOK6UE37dqjit3A=", + "dev": true + } + } + }, + "grunt-babel": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/grunt-babel/-/grunt-babel-8.0.0.tgz", + "integrity": "sha512-WuiZFvGzcyzlEoPIcY1snI234ydDWeWWV5bpnB7PZsOLHcDsxWKnrR1rMWEUsbdVPPjvIirwFNsuo4CbJmsdFQ==", + "dev": true + }, + "grunt-browserify": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/grunt-browserify/-/grunt-browserify-5.3.0.tgz", + "integrity": "sha1-R/2M+LrFj+LeaDr9xX9/OoDKeS0=", + "dev": true, + "requires": { + "async": "^2.5.0", + "browserify": "^16.0.0", + "browserify-incremental": "^3.1.1", + "glob": "^7.1.2", + "lodash": "^4.17.4", + "resolve": "^1.1.6", + "watchify": "^3.6.1" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "grunt-contrib-concat": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-0.5.1.tgz", + "integrity": "sha1-lTxu/f39LBB6uchQd/LUsk0xzUk=", + "dev": true, + "requires": { + "chalk": "^0.5.1", + "source-map": "^0.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", + "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=", + "dev": true + }, + "chalk": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", + "dev": true, + "requires": { + "ansi-styles": "^1.1.0", + "escape-string-regexp": "^1.0.0", + "has-ansi": "^0.1.0", + "strip-ansi": "^0.3.0", + "supports-color": "^0.2.0" + } + }, + "source-map": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.3.0.tgz", + "integrity": "sha1-hYb7mloAXltQHiHNGLbyG0V60fk=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", + "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=", + "dev": true + } + } + }, + "grunt-contrib-uglify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-4.0.0.tgz", + "integrity": "sha512-vy3Vop2KDqdiwcGOGAjyKvjHFrRD/YK4KPQWR3Yt6OdYlgFw1z7HCuk66+IJ9s7oJmp9uRQXuuSHyawKRAgiMw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "maxmin": "^2.1.0", + "uglify-js": "~3.4.8", + "uri-path": "^1.0.0" + } + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "http://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "http://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + }, + "dependencies": { + "lodash": { + "version": "0.9.2", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + } + } + }, + "gulp-sourcemaps": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.1", + "graceful-fs": "^4.1.2", + "strip-bom": "^2.0.0", + "through2": "^2.0.0", + "vinyl": "^1.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + } + } + }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "dev": true, + "requires": { + "duplexer": "^0.1.1" + } + }, + "handlebars": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", + "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", + "dev": true, + "requires": { + "async": "^2.5.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", + "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", + "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "http://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "~0.5.3" + } + }, + "insert-module-globals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", + "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + } + }, + "intern": { + "version": "3.3.2", + "resolved": "http://registry.npmjs.org/intern/-/intern-3.3.2.tgz", + "integrity": "sha1-0Rp5bZwFzScVEVbl1Mf7KftSIsA=", + "dev": true, + "requires": { + "chai": "3.5.0", + "charm": "0.2.0", + "diff": "1.1.0", + "digdug": "~1.5.2", + "dojo": "2.0.0-alpha.7", + "glob": "7.0.3", + "istanbul": "0.4.1", + "leadfoot": "~1.6.12", + "mimetype": "0.0.8", + "source-map": "0.1.33" + }, + "dependencies": { + "glob": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "integrity": "sha1-CqI1kxpKlqwT1g/6wvuHe9btT1g=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "source-map": { + "version": "0.1.33", + "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.1.33.tgz", + "integrity": "sha1-xlkpenOvGMBzsKoufMkeMWtcVww=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-absolute": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz", + "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=", + "dev": true, + "requires": { + "is-relative": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-bzip2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-bzip2/-/is-bzip2-1.0.0.tgz", + "integrity": "sha1-XuWOqlounIDiFAe+3yOuWsCRs/w=", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=", + "dev": true + }, + "is-natural-number": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-2.1.1.tgz", + "integrity": "sha1-fUxXKDd+84bD4ZSpkRv1fG3DNec=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-relative": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz", + "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-tar": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz", + "integrity": "sha1-L2suF5LB9bs2UZrKqdZcDSb+hT0=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-valid-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-zip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-zip/-/is-zip-1.0.0.tgz", + "integrity": "sha1-R7Co/004p2QxzP2ZqOFaTIa6IyU=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.1.tgz", + "integrity": "sha1-zXMI6zSdBbnyGBYyukxKO1NNJyQ=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.7.x", + "esprima": "2.7.x", + "fileset": "0.2.x", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "async": { + "version": "1.5.2", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "js-base64": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz", + "integrity": "sha1-8OgK4DmkvWVLXygfyT8EqRSn/M4=", + "dev": true + }, + "js-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz", + "integrity": "sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jstimezonedetect": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/jstimezonedetect/-/jstimezonedetect-1.0.5.tgz", + "integrity": "sha1-k9A1zSDox9ZOsTdc9ap6EKAkRmo=" + }, + "jszip": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz", + "integrity": "sha1-dET9hVHd8+XacZj+oMkbyDCMwnQ=", + "dev": true, + "requires": { + "pako": "~0.2.5" + }, + "dependencies": { + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "labeled-stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz", + "integrity": "sha512-MC94mHZRvJ3LfykJlTUipBqenZz1pacOZEMhhQ8dMGcDHs0SBE5GbsavUXV7YtP3icBW17W0Zy1I0lfASmo9Pg==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "isarray": "^2.0.4", + "stream-splicer": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz", + "integrity": "sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==", + "dev": true + } + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } + }, + "leadfoot": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/leadfoot/-/leadfoot-1.6.12.tgz", + "integrity": "sha1-QkUL/QmKkmrY5d2WhHB50NGqDqs=", + "dev": true, + "requires": { + "dojo": "2.0.0-alpha.7", + "jszip": "2.5.0" + } + }, + "levn": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz", + "integrity": "sha1-uo0znQykphDjo/FFucr0iAcVUFQ=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.0", + "type-check": "~0.3.1" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "maxmin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", + "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "figures": "^1.0.1", + "gzip-size": "^3.0.0", + "pretty-bytes": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.2.11", + "resolved": "http://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=", + "dev": true + }, + "mimetype": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mimetype/-/mimetype-0.0.8.tgz", + "integrity": "sha1-+zACJ5S793Jct7Rt+CDofdkf0IY=", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "module-deps": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.0.tgz", + "integrity": "sha512-hKPmO06so6bL/ZvqVNVqdTVO8UAYsi3tQWlCa+z9KuWhoN4KDQtb5hcqQQv58qYiDE21wIvnttZEPiDgEbpwbA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.0.2", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "murmurhash": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/murmurhash/-/murmurhash-0.0.2.tgz", + "integrity": "sha1-bwe9ihEF5wnCb8iUIMtZMMJFhf4=" + }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "node-releases": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.0.5.tgz", + "integrity": "sha512-Ky7q0BO1BBkG/rQz6PkEZ59rwo+aSfhczHP1wwq8IowoVdN/FpiP7qp0XW0P2+BVCWe5fQUBozdbVd54q1RbCQ==", + "dev": true, + "requires": { + "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.5.0.tgz", + "integrity": "sha1-t1qJlaLUF98ltuTjhi9QqohlE2g=", + "dev": true, + "requires": { + "deep-is": "~0.1.2", + "fast-levenshtein": "~1.0.0", + "levn": "~0.2.5", + "prelude-ls": "~1.1.1", + "type-check": "~0.3.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "ordered-read-streams": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", + "dev": true, + "requires": { + "is-stream": "^1.0.1", + "readable-stream": "^2.0.1" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "outpipe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", + "dev": true, + "requires": { + "shell-quote": "^1.4.2" + } + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "pretty-bytes": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", + "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0", + "readable-stream": "^2.0.0" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz", + "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-transform": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", + "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpu-core": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.2.0.tgz", + "integrity": "sha512-Z835VSnJJ46CNBttalHD/dB+Sj2ezmY6Xp38npwU87peK6mqOzOpV8eYktdkLTEkzzD+JsTcxd84ozd8I14+rw==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^7.0.0", + "regjsgen": "^0.4.0", + "regjsparser": "^0.3.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.0.2" + } + }, + "regjsgen": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", + "integrity": "sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA==", + "dev": true + }, + "regjsparser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.3.0.tgz", + "integrity": "sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.2.8", + "resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "sax": { + "version": "0.5.3", + "resolved": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz", + "integrity": "sha1-N3NxSg2RV8qqcwKXHvpcbc2lUtY=", + "dev": true + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "requires": { + "commander": "~2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + } + } + }, + "semver": { + "version": "4.3.2", + "resolved": "http://registry.npmjs.org/semver/-/semver-4.3.2.tgz", + "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "sha1": { + "version": "git://github.com/pvorb/node-sha1.git#910081c83f3661507d9d89e66e3f38d8b59d5559", + "from": "git://github.com/pvorb/node-sha1.git#910081c83f3661507d9d89e66e3f38d8b59d5559", + "requires": { + "charenc": ">= 0.0.0", + "crypt": ">= 0.0.0" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "~0.0.0", + "sha.js": "~2.4.4" + } + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "snowplow-tracker-core": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/snowplow-tracker-core/-/snowplow-tracker-core-0.6.1.tgz", + "integrity": "sha1-9c8NQEJ98g54HEGrH5jGxmjKMIA=", + "requires": { + "uuid": "2.0.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", + "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=", + "dev": true, + "requires": { + "ansi-regex": "^0.2.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true, + "requires": { + "first-chunk-stream": "^1.0.0", + "strip-bom": "^2.0.0" + } + }, + "strip-dirs": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz", + "integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=", + "dev": true, + "requires": { + "chalk": "^1.0.0", + "get-stdin": "^4.0.1", + "is-absolute": "^0.1.5", + "is-natural-number": "^2.0.0", + "minimist": "^1.1.0", + "sum-up": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "^1.1.0" + } + }, + "sum-up": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", + "integrity": "sha1-HGYfZnBX9jvLeHWqFDi8FiUlFW4=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "^1.2.0" + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "~0.11.0" + } + }, + "to-absolute-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "dev": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true + }, + "undeclared-identifiers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.2.tgz", + "integrity": "sha512-13EaeocO4edF/3JKime9rD7oB6QI8llAGhgn5fKOPyfkJbRb6NFv9pYV6dFEmpa4uRjKeBqLZP8GpuzqHlKDMQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "http://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz", + "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz", + "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==", + "dev": true + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true, + "requires": { + "json-stable-stringify": "^1.0.0", + "through2-filter": "^2.0.0" + }, + "dependencies": { + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "uri-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", + "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=", + "dev": true + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "2.0.3", + "resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=" + }, + "vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "dev": true + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + }, + "vinyl-assign": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vinyl-assign/-/vinyl-assign-1.2.1.tgz", + "integrity": "sha1-TRmIkbVRWRHXcajNnFSApGoHSkU=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "readable-stream": "^2.0.0" + } + }, + "vinyl-fs": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", + "dev": true, + "requires": { + "duplexify": "^3.2.0", + "glob-stream": "^5.3.2", + "graceful-fs": "^4.0.0", + "gulp-sourcemaps": "1.6.0", + "is-valid-glob": "^0.3.0", + "lazystream": "^1.0.0", + "lodash.isequal": "^4.0.0", + "merge-stream": "^1.0.0", + "mkdirp": "^0.5.0", + "object-assign": "^4.0.0", + "readable-stream": "^2.0.4", + "strip-bom": "^2.0.0", + "strip-bom-stream": "^1.0.0", + "through2": "^2.0.0", + "through2-filter": "^2.0.0", + "vali-date": "^1.0.0", + "vinyl": "^1.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + } + } + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "watchify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.0.tgz", + "integrity": "sha512-7jWG0c3cKKm2hKScnSAMUEUjRJKXUShwMPk0ASVhICycQhwND3IMAdhJYmc1mxxKzBUJTSF5HZizfrKrS6BzkA==", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "browserify": "^16.1.0", + "chokidar": "^1.0.0", + "defined": "^1.0.0", + "outpipe": "^1.1.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "dev": true, + "requires": { + "sax": "0.5.x" + } + }, + "xmlbuilder": { + "version": "0.4.2", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz", + "integrity": "sha1-F3bWXz/brUcKCNhgTN6xxOVA/4M=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/snowplow-javascript-tracker/package.json b/snowplow-javascript-tracker/package.json new file mode 100644 index 0000000000..fd5e556e1a --- /dev/null +++ b/snowplow-javascript-tracker/package.json @@ -0,0 +1,49 @@ +{ + "name": "snowplow-tracker", + "version": "2.10.0", + "dependencies": { + "snowplow-tracker-core": "0.7.0", + "browser-cookie-lite": "0.3.1", + "jstimezonedetect": "1.0.5", + "murmurhash": "0.0.2", + "sha1": "git://github.com/pvorb/node-sha1.git#910081c83f3661507d9d89e66e3f38d8b59d5559", + "uuid": "2.0.3" + }, + "devDependencies": { + "@babel/core": "^7.1.2", + "@babel/preset-env": "^7.1.0", + "JSON": "1.0.0", + "grunt": "^1.0.3", + "grunt-aws": "0.6.2", + "grunt-babel": "^8.0.0", + "lodash": "^4.17.11", + "grunt-browserify": "^5.3.0", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-uglify": "^4.0.0", + "intern": "3.3.2", + "js-base64": "^2.4.9", + "semver": "4.3.2" + }, + "contributors": [ + "Alex Dean", + "Simon Andersson", + "Anthon Pang", + "Fred Blundun", + "Joshua Beemster", + "Michael Hadam" + ], + "description": "JavaScript tracker for Snowplow", + "repository": { + "type": "git", + "url": "https://github.com/snowplow/snowplow-javascript-tracker.git" + }, + "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues", + "keywords": [ + "tracking", + "web analytics", + "events", + "open source" + ], + "license": "Simplified BSD", + "private": true +} diff --git a/snowplow-javascript-tracker/src/js/errors.js b/snowplow-javascript-tracker/src/js/errors.js new file mode 100644 index 0000000000..5d0d6f069f --- /dev/null +++ b/snowplow-javascript-tracker/src/js/errors.js @@ -0,0 +1,127 @@ +/* + * JavaScript tracker for Snowplow: tracker.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2016 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var isFunction = require('lodash/isFunction'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this, + windowAlias = window; + + +object.errorManager = function (core) { + + /** + * Send error as self-describing event + * + * @param message string Message appeared in console + * @param filename string Source file (not used) + * @param lineno number Line number + * @param colno number Column number (not used) + * @param error Error error object (not present in all browsers) + * @param contexts Array of custom contexts + */ + function track(message, filename, lineno, colno, error, contexts) { + var stack = (error && error.stack) ? error.stack : null; + + core.trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/application_error/jsonschema/1-0-1', + data: { + programmingLanguage: "JAVASCRIPT", + message: message || "JS Exception. Browser doesn't support ErrorEvent API", + stackTrace: stack, + lineNumber: lineno, + lineColumn: colno, + fileName: filename + } + }, contexts) + } + + /** + * Attach custom contexts using `contextAdder` + * + * + * @param contextsAdder function to get details from internal browser state + * @returns {Array} custom contexts + */ + function sendError(errorEvent, commonContexts, contextsAdder) { + var contexts; + if (isFunction(contextsAdder)) { + contexts = commonContexts.concat(contextsAdder(errorEvent)); + } else { + contexts = commonContexts; + } + + track(errorEvent.message, errorEvent.filename, errorEvent.lineno, errorEvent.colno, errorEvent.error, contexts) + } + + return { + + /** + * Track unhandled exception. + * This method supposed to be used inside try/catch block or with window.onerror + * (contexts won't be attached), but NOT with `addEventListener` - use + * `enableErrorTracker` for this + * + * @param message string Message appeared in console + * @param filename string Source file (not used) + * @param lineno number Line number + * @param colno number Column number (not used) + * @param error Error error object (not present in all browsers) + * @param contexts Array of custom contexts + */ + trackError: track, + + /** + * Curried function to enable tracking of unhandled exceptions. + * Listen for `error` event and + * + * @param filter Function ErrorEvent => Bool to check whether error should be tracker + * @param contextsAdder Function ErrorEvent => Array to add custom contexts with + * internal state based on particular error + */ + enableErrorTracking: function (filter, contextsAdder, contexts) { + /** + * Closure callback to filter, contextualize and track unhandled exceptions + * + * @param errorEvent ErrorEvent passed to event listener + */ + function captureError (errorEvent) { + if (isFunction(filter) && filter(errorEvent) || filter == null) { + sendError(errorEvent, contexts, contextsAdder) + } + } + + helpers.addEventListener(windowAlias, 'error', captureError, true); + } + } +}; diff --git a/snowplow-javascript-tracker/src/js/forms.js b/snowplow-javascript-tracker/src/js/forms.js new file mode 100755 index 0000000000..1efc033c65 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/forms.js @@ -0,0 +1,189 @@ +/* + * JavaScript tracker for Snowplow: forms.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var forEach = require('lodash/forEach'), + filter = require('lodash/filter'), + find = require('lodash/find'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; + +/** + * Object for handling automatic form tracking + * + * @param object core The tracker core + * @param string trackerId Unique identifier for the tracker instance, used to mark tracked elements + * @param function contextAdder Function to add common contexts like PerformanceTiming to all events + * @return object formTrackingManager instance + */ +object.getFormTrackingManager = function (core, trackerId, contextAdder) { + + // Tag names of mutable elements inside a form + var innerElementTags = ['textarea', 'input', 'select']; + + // Used to mark elements with event listeners + var trackingMarker = trackerId + 'form'; + + // Filter to determine which forms should be tracked + var formFilter = function () { return true }; + + // Filter to determine which form fields should be tracked + var fieldFilter = function () { return true }; + + // Default function applied to all elements, optionally overridden by transform field + var fieldTransform = function (x) { return x }; + + /* + * Get an identifier for a form, input, textarea, or select element + */ + function getFormElementName(elt) { + return elt[find(['name', 'id', 'type', 'nodeName'], function (propName) { + + // If elt has a child whose name is "id", that element will be returned + // instead of the actual id of elt unless we ensure that a string is returned + return elt[propName] && typeof elt[propName] === 'string'; + })]; + } + + /* + * Identifies the parent form in which an element is contained + */ + function getParentFormName(elt) { + while (elt && elt.nodeName && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') { + elt = elt.parentNode; + } + if (elt && elt.nodeName && elt.nodeName.toUpperCase() === 'FORM') { + return getFormElementName(elt); + } + } + + /* + * Returns a list of the input, textarea, and select elements inside a form along with their values + */ + function getInnerFormElements(elt) { + var innerElements = []; + forEach(innerElementTags, function (tagname) { + + var trackedChildren = filter(elt.getElementsByTagName(tagname), function (child) { + return child.hasOwnProperty(trackingMarker); + }); + + forEach(trackedChildren, function (child) { + if (child.type === 'submit') { + return; + } + var elementJson = { + name: getFormElementName(child), + value: child.value, + nodeName: child.nodeName + }; + if (child.type && child.nodeName.toUpperCase() === 'INPUT') { + elementJson.type = child.type; + } + + if ((child.type === 'checkbox' || child.type === 'radio') && !child.checked) { + elementJson.value = null; + } + innerElements.push(elementJson); + }); + }); + + return innerElements; + } + + /* + * Return function to handle form field change event + */ + function getFormChangeListener(event_type, context) { + return function (e) { + var elt = e.target; + var type = (elt.nodeName && elt.nodeName.toUpperCase() === 'INPUT') ? elt.type : null; + var value = (elt.type === 'checkbox' && !elt.checked) ? null : fieldTransform(elt.value); + if (event_type === 'change_form' || (type !== 'checkbox' && type !== 'radio')) { + core.trackFormFocusOrChange(event_type, getParentFormName(elt), getFormElementName(elt), elt.nodeName, type, helpers.getCssClasses(elt), value, contextAdder(helpers.resolveDynamicContexts(context, elt, type, value))); + } + }; + } + + /* + * Return function to handle form submission event + */ + function getFormSubmissionListener(context) { + return function (e) { + var elt = e.target; + var innerElements = getInnerFormElements(elt); + forEach(innerElements, function (innerElement) { + innerElement.value = fieldTransform(innerElement.value); + }); + core.trackFormSubmission(getFormElementName(elt), helpers.getCssClasses(elt), innerElements, contextAdder(helpers.resolveDynamicContexts(context, elt, innerElements))); + }; + } + + return { + + /* + * Configures form tracking: which forms and fields will be tracked, and the context to attach + */ + configureFormTracking: function (config) { + if (config) { + formFilter = helpers.getFilter(config.forms, true); + fieldFilter = helpers.getFilter(config.fields, false); + fieldTransform = helpers.getTransform(config.fields); + } + }, + + /* + * Add submission event listeners to all form elements + * Add value change event listeners to all mutable inner form elements + */ + addFormListeners: function (context) { + forEach(document.getElementsByTagName('form'), function (form) { + if (formFilter(form) && !form[trackingMarker]) { + + forEach(innerElementTags, function (tagname) { + forEach(form.getElementsByTagName(tagname), function (innerElement) { + if (fieldFilter(innerElement) && !innerElement[trackingMarker] && innerElement.type.toLowerCase() !== 'password') { + helpers.addEventListener(innerElement, 'focus', getFormChangeListener('focus_form', context), false); + helpers.addEventListener(innerElement, 'change', getFormChangeListener('change_form', context), false); + innerElement[trackingMarker] = true; + } + }); + }); + + helpers.addEventListener(form, 'submit', getFormSubmissionListener(context)); + form[trackingMarker] = true; + } + }); + } + }; +}; diff --git a/snowplow-javascript-tracker/src/js/guard.js b/snowplow-javascript-tracker/src/js/guard.js new file mode 100644 index 0000000000..7c53dbd0fa --- /dev/null +++ b/snowplow-javascript-tracker/src/js/guard.js @@ -0,0 +1,60 @@ +/* + * JavaScript tracker for Snowplow: tracker.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2018 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +var object = typeof exports !== 'undefined' ? exports : this; + +var makeSafe = function (fn) { + return function () { + try { + return fn.apply(this, arguments); + } catch (ex) { + // TODO: Debug mode + } + }; +}; + +exports.productionize = function (methods) { + let safeMethods = {}; + if (typeof methods === 'object' && methods !== null) { + Object.getOwnPropertyNames(methods).forEach( + function (val, idx, array) { + if (typeof methods[val] === 'function') { + safeMethods[val] = makeSafe(methods[val]); + } + } + ); + } + return safeMethods; +}; \ No newline at end of file diff --git a/snowplow-javascript-tracker/src/js/in_queue.js b/snowplow-javascript-tracker/src/js/in_queue.js new file mode 100644 index 0000000000..f6ff72326f --- /dev/null +++ b/snowplow-javascript-tracker/src/js/in_queue.js @@ -0,0 +1,193 @@ +/* + * JavaScript tracker for Snowplow: queue.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function() { + + var + map = require('lodash/map'), + isUndefined = require('lodash/isUndefined'), + isFunction = require('lodash/isFunction'), + helpers = require('./lib/helpers'), + + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + /************************************************************ + * Proxy object + * - this allows the caller to continue push()'ing to _snaq + * after the Tracker has been initialized and loaded + ************************************************************/ + + object.InQueueManager = function(TrackerConstructor, version, mutSnowplowState, asyncQueue, functionName) { + + // Page view ID should be shared between all tracker instances + var trackerDictionary = {}; + + /** + * Get an array of trackers to which a function should be applied. + * + * @param array names List of namespaces to use. If empty, use all namespaces. + */ + function getNamedTrackers(names) { + var namedTrackers = []; + + if (!names || names.length === 0) { + namedTrackers = map(trackerDictionary); + } else { + for (var i = 0; i < names.length; i++) { + if (trackerDictionary.hasOwnProperty(names[i])) { + namedTrackers.push(trackerDictionary[names[i]]); + } else { + helpers.warn('Warning: Tracker namespace "' + names[i] + '" not configured'); + } + } + } + + if (namedTrackers.length === 0) { + helpers.warn('Warning: No tracker configured'); + } + + return namedTrackers; + } + + /** + * Legacy support for input of the form _snaq.push(['setCollectorCf', 'd34uzc5hjrimh8']) + * + * @param string f Either 'setCollectorCf' or 'setCollectorUrl' + * @param string endpoint + * @param string namespace Optional tracker name + * + * TODO: remove this in 2.1.0 + */ + function legacyCreateNewNamespace(f, endpoint, namespace) { + helpers.warn(f + ' is deprecated. Set the collector when a new tracker instance using newTracker.'); + + var name; + + if (isUndefined(namespace)) { + name = 'sp'; + } else { + name = namespace; + } + + createNewNamespace(name); + trackerDictionary[name][f](endpoint); + } + + /** + * Initiate a new tracker namespace + * + * @param string namespace + * @param string endpoint Of the form d3rkrsqld9gmqf.cloudfront.net + */ + function createNewNamespace(namespace, endpoint, argmap) { + argmap = argmap || {}; + + if (!trackerDictionary.hasOwnProperty(namespace)) { + trackerDictionary[namespace] = new TrackerConstructor(functionName, namespace, version, mutSnowplowState, argmap); + trackerDictionary[namespace].setCollectorUrl(endpoint); + } else { + helpers.warn('Tracker namespace ' + namespace + ' already exists.'); + } + } + + /** + * Output an array of the form ['functionName', [trackerName1, trackerName2, ...]] + * + * @param string inputString + */ + function parseInputString(inputString) { + var separatedString = inputString.split(':'), + extractedFunction = separatedString[0], + extractedNames = (separatedString.length > 1) ? separatedString[1].split(';') : []; + + return [extractedFunction, extractedNames]; + } + + /** + * apply wrapper + * + * @param array parameterArray An array comprising either: + * [ 'methodName', optional_parameters ] + * or: + * [ functionObject, optional_parameters ] + */ + function applyAsyncFunction() { + var i, j, f, parameterArray, input, parsedString, names, namedTrackers; + + // Outer loop in case someone push'es in zarg of arrays + for (i = 0; i < arguments.length; i += 1) { + parameterArray = arguments[i]; + + // Arguments is not an array, so we turn it into one + input = Array.prototype.shift.call(parameterArray); + + // Custom callback rather than tracker method, called with trackerDictionary as the context + if (isFunction(input)) { + input.apply(trackerDictionary, parameterArray); + continue; + } + + parsedString = parseInputString(input); + f = parsedString[0]; + names = parsedString[1]; + + if (f === 'newTracker') { + createNewNamespace(parameterArray[0], parameterArray[1], parameterArray[2]); + continue; + } + + if ((f === 'setCollectorCf' || f === 'setCollectorUrl') && (!names || names.length === 0)) { + legacyCreateNewNamespace(f, parameterArray[0], parameterArray[1]); + continue; + } + + namedTrackers = getNamedTrackers(names); + + for (j = 0; j < namedTrackers.length; j++) { + namedTrackers[j][f].apply(namedTrackers[j], parameterArray); + } + } + } + + // We need to manually apply any events collected before this initialization + for (var i = 0; i < asyncQueue.length; i++) { + applyAsyncFunction(asyncQueue[i]); + } + + return { + push: applyAsyncFunction + }; + }; + +}()); diff --git a/snowplow-javascript-tracker/src/js/init.js b/snowplow-javascript-tracker/src/js/init.js new file mode 100644 index 0000000000..9e243b1929 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/init.js @@ -0,0 +1,53 @@ +/* + * JavaScript tracker for Snowplow: init.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// Snowplow Asynchronous Queue + +/* + * Get the name of the global input function + */ + +var snowplow = require('./snowplow'), + queueName, + queue, + windowAlias = window; + +if (windowAlias.GlobalSnowplowNamespace && windowAlias.GlobalSnowplowNamespace.length > 0) { + queueName = windowAlias.GlobalSnowplowNamespace.shift(); + queue = windowAlias[queueName]; + queue.q = new snowplow.Snowplow(queue.q, queueName); +} else { + windowAlias._snaq = windowAlias._snaq || []; + windowAlias._snaq = new snowplow.Snowplow(windowAlias._snaq, '_snaq'); +} diff --git a/snowplow-javascript-tracker/src/js/lib/detectors.js b/snowplow-javascript-tracker/src/js/lib/detectors.js new file mode 100644 index 0000000000..e9c11b2075 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/lib/detectors.js @@ -0,0 +1,254 @@ +/* + * JavaScript tracker for Snowplow: detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function() { + + var + isFunction = require('lodash/isFunction'), + isUndefined = require('lodash/isUndefined'), + murmurhash3_32_gc = require('murmurhash').v3, + tz = require('jstimezonedetect').jstz.determine(), + cookie = require('browser-cookie-lite'), + + object = typeof exports !== 'undefined' ? exports : this, // For eventual node.js environment support + + windowAlias = window, + navigatorAlias = navigator, + screenAlias = screen, + documentAlias = document; + + /* + * Checks whether sessionStorage is available, in a way that + * does not throw a SecurityError in Firefox if "always ask" + * is enabled for cookies (https://github.com/snowplow/snowplow/issues/163). + */ + object.hasSessionStorage = function () { + try { + return !!windowAlias.sessionStorage; + } catch (e) { + return true; // SecurityError when referencing it means it exists + } + }; + + /* + * Checks whether localStorage is available, in a way that + * does not throw a SecurityError in Firefox if "always ask" + * is enabled for cookies (https://github.com/snowplow/snowplow/issues/163). + */ + object.hasLocalStorage = function () { + try { + return !!windowAlias.localStorage; + } catch (e) { + return true; // SecurityError when referencing it means it exists + } + }; + + /* + * Checks whether localStorage is accessible + * sets and removes an item to handle private IOS5 browsing + * (http://git.io/jFB2Xw) + */ + object.localStorageAccessible = function() { + var mod = 'modernizr'; + if (!object.hasLocalStorage()) { + return false; + } + try { + windowAlias.localStorage.setItem(mod, mod); + windowAlias.localStorage.removeItem(mod); + return true; + } catch(e) { + return false; + } + }; + + /* + * Does browser have cookies enabled (for this site)? + */ + object.hasCookies = function(testCookieName) { + var cookieName = testCookieName || 'testcookie'; + + if (isUndefined(navigatorAlias.cookieEnabled)) { + cookie.cookie(cookieName, '1'); + return cookie.cookie(cookieName) === '1' ? '1' : '0'; + } + + return navigatorAlias.cookieEnabled ? '1' : '0'; + }; + + /** + * JS Implementation for browser fingerprint. + * Does not require any external resources. + * Based on https://github.com/carlo/jquery-browser-fingerprint + * @return {number} 32-bit positive integer hash + */ + object.detectSignature = function(hashSeed) { + + var fingerprint = [ + navigatorAlias.userAgent, + [ screenAlias.height, screenAlias.width, screenAlias.colorDepth ].join("x"), + ( new Date() ).getTimezoneOffset(), + object.hasSessionStorage(), + object.hasLocalStorage() + ]; + + var plugins = []; + if (navigatorAlias.plugins) + { + for(var i = 0; i < navigatorAlias.plugins.length; i++) + { + if (navigatorAlias.plugins[i]) { + var mt = []; + for(var j = 0; j < navigatorAlias.plugins[i].length; j++) { + mt.push([navigatorAlias.plugins[i][j].type, navigatorAlias.plugins[i][j].suffixes]); + } + plugins.push([navigatorAlias.plugins[i].name + "::" + navigatorAlias.plugins[i].description, mt.join("~")]); + } + } + } + return murmurhash3_32_gc(fingerprint.join("###") + "###" + plugins.sort().join(";"), hashSeed); + }; + + /* + * Returns visitor timezone + */ + object.detectTimezone = function() { + return (typeof (tz) === 'undefined') ? '' : tz.name(); + }; + + /** + * Gets the current viewport. + * + * Code based on: + * - http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/ + * - http://responsejs.com/labs/dimensions/ + */ + object.detectViewport = function() { + var e = windowAlias, a = 'inner'; + if (!('innerWidth' in windowAlias)) { + a = 'client'; + e = documentAlias.documentElement || documentAlias.body; + } + var width = e[a+'Width']; + var height = e[a+'Height']; + if (width >= 0 && height >= 0) { + return width + 'x' + height; + } else { + return null; + } + }; + + /** + * Gets the dimensions of the current + * document. + * + * Code based on: + * - http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/ + */ + object.detectDocumentSize = function() { + var de = documentAlias.documentElement, // Alias + be = documentAlias.body, + + // document.body may not have rendered, so check whether be.offsetHeight is null + bodyHeight = be ? Math.max(be.offsetHeight, be.scrollHeight) : 0; + var w = Math.max(de.clientWidth, de.offsetWidth, de.scrollWidth); + var h = Math.max(de.clientHeight, de.offsetHeight, de.scrollHeight, bodyHeight); + return isNaN(w) || isNaN(h) ? '' : w + 'x' + h; + }; + + /** + * Returns browser features (plugins, resolution, cookies) + * + * @param boolean useCookies Whether to test for cookies + * @param string testCookieName Name to use for the test cookie + * @return Object containing browser features + */ + object.detectBrowserFeatures = function(useCookies, testCookieName) { + var i, + mimeType, + pluginMap = { + // document types + pdf: 'application/pdf', + + // media players + qt: 'video/quicktime', + realp: 'audio/x-pn-realaudio-plugin', + wma: 'application/x-mplayer2', + + // interactive multimedia + dir: 'application/x-director', + fla: 'application/x-shockwave-flash', + + // RIA + java: 'application/x-java-vm', + gears: 'application/x-googlegears', + ag: 'application/x-silverlight' + }, + features = {}; + + // General plugin detection + if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { + for (i in pluginMap) { + if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { + mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; + features[i] = (mimeType && mimeType.enabledPlugin) ? '1' : '0'; + } + } + } + + // Safari and Opera + // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly + if (navigatorAlias.constructor === window.Navigator && + typeof navigatorAlias.javaEnabled !== 'unknown' && + !isUndefined(navigatorAlias.javaEnabled) && + navigatorAlias.javaEnabled()) { + features.java = '1'; + } + + // Firefox + if (isFunction(windowAlias.GearsFactory)) { + features.gears = '1'; + } + + // Other browser features + features.res = screenAlias.width + 'x' + screenAlias.height; + features.cd = screenAlias.colorDepth; + if (useCookies) { + features.cookie = object.hasCookies(testCookieName); + } + + return features; + }; + +}()); diff --git a/snowplow-javascript-tracker/src/js/lib/helpers.js b/snowplow-javascript-tracker/src/js/lib/helpers.js new file mode 100755 index 0000000000..3a6a0b8b51 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/lib/helpers.js @@ -0,0 +1,436 @@ +/* + * JavaScript tracker for Snowplow: Snowplow.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +;(function () { + + var + isString = require('lodash/isString'), + isUndefined = require('lodash/isUndefined'), + isObject = require('lodash/isObject'), + map = require('lodash/map'), + cookie = require('browser-cookie-lite'), + + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + /** + * Cleans up the page title + */ + object.fixupTitle = function (title) { + if (!isString(title)) { + title = title.text || ''; + + var tmp = document.getElementsByTagName('title'); + if (tmp && !isUndefined(tmp[0])) { + title = tmp[0].text; + } + } + return title; + }; + + /** + * Extract hostname from URL + */ + object.getHostName = function (url) { + // scheme : // [username [: password] @] hostname [: port] [/ [path] [? query] [# fragment]] + var e = new RegExp('^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)'), + matches = e.exec(url); + + return matches ? matches[1] : url; + }; + + /** + * Fix-up domain + */ + object.fixupDomain = function (domain) { + var dl = domain.length; + + // remove trailing '.' + if (domain.charAt(--dl) === '.') { + domain = domain.slice(0, dl); + } + // remove leading '*' + if (domain.slice(0, 2) === '*.') { + domain = domain.slice(1); + } + return domain; + }; + + /** + * Get page referrer. In the case of a single-page app, + * if the URL changes without the page reloading, pass + * in the old URL. It will be returned unless overriden + * by a "refer(r)er" parameter in the querystring. + * + * @param string oldLocation Optional. + * @return string The referrer + */ + object.getReferrer = function (oldLocation) { + + var referrer = ''; + + var fromQs = object.fromQuerystring('referrer', window.location.href) || + object.fromQuerystring('referer', window.location.href); + + // Short-circuit + if (fromQs) { + return fromQs; + } + + // In the case of a single-page app, return the old URL + if (oldLocation) { + return oldLocation; + } + + try { + referrer = window.top.document.referrer; + } catch (e) { + if (window.parent) { + try { + referrer = window.parent.document.referrer; + } catch (e2) { + referrer = ''; + } + } + } + if (referrer === '') { + referrer = document.referrer; + } + return referrer; + }; + + /** + * Cross-browser helper function to add event handler + */ + object.addEventListener = function (element, eventType, eventHandler, useCapture) { + if (element.addEventListener) { + element.addEventListener(eventType, eventHandler, useCapture); + return true; + } + if (element.attachEvent) { + return element.attachEvent('on' + eventType, eventHandler); + } + element['on' + eventType] = eventHandler; + }; + + /** + * Return value from name-value pair in querystring + */ + object.fromQuerystring = function (field, url) { + var match = new RegExp('^[^#]*[?&]' + field + '=([^&#]*)').exec(url); + if (!match) { + return null; + } + return decodeURIComponent(match[1].replace(/\+/g, ' ')); + }; + + /* + * Find dynamic context generating functions and merge their results into the static contexts + * Combine an array of unchanging contexts with the result of a context-creating function + * + * @param {(object|function(...*): ?object)[]} dynamicOrStaticContexts Array of custom context Objects or custom context generating functions + * @param {...*} Parameters to pass to dynamic callbacks + */ + object.resolveDynamicContexts = function (dynamicOrStaticContexts) { + let params = Array.prototype.slice.call(arguments, 1); + return map(dynamicOrStaticContexts, function(context) { + if (typeof context === 'function') { + try { + return context.apply(null, params); + } catch (e) { + //TODO: provide warning + } + } else { + return context; + } + }); + }; + + /** + * Only log deprecation warnings if they won't cause an error + */ + object.warn = function(message) { + if (typeof console !== 'undefined') { + console.warn('Snowplow: ' + message); + } + }; + + /** + * List the classes of a DOM element without using elt.classList (for compatibility with IE 9) + */ + object.getCssClasses = function (elt) { + return elt.className.match(/\S+/g) || []; + }; + + /** + * Check whether an element has at least one class from a given list + */ + function checkClass(elt, classList) { + var classes = object.getCssClasses(elt), + i; + + for (i = 0; i < classes.length; i++) { + if (classList[classes[i]]) { + return true; + } + } + return false; + } + + /** + * Convert a criterion object to a filter function + * + * @param object criterion Either {whitelist: [array of allowable strings]} + * or {blacklist: [array of allowable strings]} + * or {filter: function (elt) {return whether to track the element} + * @param boolean byClass Whether to whitelist/blacklist based on an element's classes (for forms) + * or name attribute (for fields) + */ + object.getFilter = function (criterion, byClass) { + + // If the criterion argument is not an object, add listeners to all elements + if (Array.isArray(criterion) || !isObject(criterion)) { + return function () { + return true; + }; + } + + if (criterion.hasOwnProperty('filter')) { + return criterion.filter; + } else { + var inclusive = criterion.hasOwnProperty('whitelist'); + var specifiedClasses = criterion.whitelist || criterion.blacklist; + if (!Array.isArray(specifiedClasses)) { + specifiedClasses = [specifiedClasses]; + } + + // Convert the array of classes to an object of the form {class1: true, class2: true, ...} + var specifiedClassesSet = {}; + for (var i=0; i= 0) { + var currentDomain = split.slice(position, split.length).join('.'); + cookie.cookie(cookieName, cookieValue, 0, '/', currentDomain); + if (cookie.cookie(cookieName) === cookieValue) { + + // Clean up created cookie(s) + object.deleteCookie(cookieName, currentDomain); + var cookieNames = object.getCookiesWithPrefix(cookiePrefix); + for (var i = 0; i < cookieNames.length; i++) { + object.deleteCookie(cookieNames[i], currentDomain); + } + + return currentDomain; + } + position -= 1; + } + + // Cookies cannot be read + return window.location.hostname; + }; + + /** + * Checks whether a value is present within an array + * + * @param val The value to check for + * @param array The array to check within + * @return boolean Whether it exists + */ + object.isValueInArray = function (val, array) { + for (var i = 0; i < array.length; i++) { + if (array[i] === val) { + return true; + } + } + return false; + }; + + /** + * Deletes an arbitrary cookie by setting the expiration date to the past + * + * @param cookieName The name of the cookie to delete + * @param domainName The domain the cookie is in + */ + object.deleteCookie = function (cookieName, domainName) { + cookie.cookie(cookieName, '', -1, '/', domainName); + }; + + /** + * Fetches the name of all cookies beginning with a certain prefix + * + * @param cookiePrefix The prefix to check for + * @return array The cookies that begin with the prefix + */ + object.getCookiesWithPrefix = function (cookiePrefix) { + var cookies = document.cookie.split("; "); + var cookieNames = []; + for (var i = 0; i < cookies.length; i++) { + if (cookies[i].substring(0, cookiePrefix.length) === cookiePrefix) { + cookieNames.push(cookies[i]); + } + } + return cookieNames; + }; + + /** + * Parses an object and returns either the + * integer or undefined. + * + * @param obj The object to parse + * @return the result of the parse operation + */ + object.parseInt = function (obj) { + var result = parseInt(obj); + return isNaN(result) ? undefined : result; + }; + + /** + * Parses an object and returns either the + * number or undefined. + * + * @param obj The object to parse + * @return the result of the parse operation + */ + object.parseFloat = function (obj) { + var result = parseFloat(obj); + return isNaN(result) ? undefined : result; + } + +}()); diff --git a/snowplow-javascript-tracker/src/js/lib/proxies.js b/snowplow-javascript-tracker/src/js/lib/proxies.js new file mode 100644 index 0000000000..ee7cefd294 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/lib/proxies.js @@ -0,0 +1,101 @@ +/* + * JavaScript tracker for Snowplow: proxies.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function(){ + + var + helpers = require('./helpers'), + object = typeof exports !== 'undefined' ? exports : this; + + /* + * Test whether a string is an IP address + */ + function isIpAddress(string) { + var IPRegExp = new RegExp('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'); + return IPRegExp.test(string); + } + + /* + * If the hostname is an IP address, look for text indicating + * that the page is cached by Yahoo + */ + function isYahooCachedPage(hostName) { + var + initialDivText, + cachedIndicator; + if (isIpAddress(hostName)) { + try { + initialDivText = document.body.children[0].children[0].children[0].children[0].children[0].children[0].innerHTML; + cachedIndicator = 'You have reached the cached page for'; + return initialDivText.slice(0, cachedIndicator.length) === cachedIndicator; + } catch (e) { + return false; + } + } + } + + /* + * Extract parameter from URL + */ + function getParameter(url, name) { + // scheme : // [username [: password] @] hostname [: port] [/ [path] [? query] [# fragment]] + var e = new RegExp('^(?:https?|ftp)(?::/*(?:[^?]+))([?][^#]+)'), + matches = e.exec(url), + result = helpers.fromQuerystring(name, matches[1]); + + return result; + } + + /* + * Fix-up URL when page rendered from search engine cache or translated page. + * TODO: it would be nice to generalise this and/or move into the ETL phase. + */ + object.fixupUrl = function (hostName, href, referrer) { + + if (hostName === 'translate.googleusercontent.com') { // Google + if (referrer === '') { + referrer = href; + } + href = getParameter(href, 'u'); + hostName = helpers.getHostName(href); + } else if (hostName === 'cc.bingj.com' || // Bing + hostName === 'webcache.googleusercontent.com' || // Google + isYahooCachedPage(hostName)) { // Yahoo (via Inktomi 74.6.0.0/16) + href = document.links[0].href; + hostName = helpers.getHostName(href); + } + return [hostName, href, referrer]; + }; + +}()); diff --git a/snowplow-javascript-tracker/src/js/links.js b/snowplow-javascript-tracker/src/js/links.js new file mode 100755 index 0000000000..c7b08feb14 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/links.js @@ -0,0 +1,181 @@ +/* + * JavaScript tracker for Snowplow: links.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var isUndefined = require('lodash/isUndefined'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; + +/** + * Object for handling automatic link tracking + * + * @param object core The tracker core + * @param string trackerId Unique identifier for the tracker instance, used to mark tracked links + * @param function contextAdder Function to add common contexts like PerformanceTiming to all events + * @return object linkTrackingManager instance + */ +object.getLinkTrackingManager = function (core, trackerId, contextAdder) { + + // Filter function used to determine whether clicks on a link should be tracked + var linkTrackingFilter, + + // Whether pseudo clicks are tracked + linkTrackingPseudoClicks, + + // Whether to track the innerHTML of clicked links + linkTrackingContent, + + // The context attached to link click events + linkTrackingContext, + + // Internal state of the pseudo click handler + lastButton, + lastTarget; + + /* + * Process clicks + */ + function processClick(sourceElement, context) { + + var parentElement, + tag, + elementId, + elementClasses, + elementTarget, + elementContent; + + while ((parentElement = sourceElement.parentNode) !== null && + !isUndefined(parentElement) && // buggy IE5.5 + ((tag = sourceElement.tagName.toUpperCase()) !== 'A' && tag !== 'AREA')) { + sourceElement = parentElement; + } + + if (!isUndefined(sourceElement.href)) { + // browsers, such as Safari, don't downcase hostname and href + var originalSourceHostName = sourceElement.hostname || helpers.getHostName(sourceElement.href), + sourceHostName = originalSourceHostName.toLowerCase(), + sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), + scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); + + // Ignore script pseudo-protocol links + if (!scriptProtocol.test(sourceHref)) { + + elementId = sourceElement.id; + elementClasses = helpers.getCssClasses(sourceElement); + elementTarget = sourceElement.target; + elementContent = linkTrackingContent ? sourceElement.innerHTML : null; + + // decodeUrl %xx + sourceHref = unescape(sourceHref); + core.trackLinkClick(sourceHref, elementId, elementClasses, elementTarget, elementContent, contextAdder(helpers.resolveDynamicContexts(context, sourceElement))); + } + } + } + + /* + * Return function to handle click event + */ + function getClickHandler(context) { + return function (evt) { + var button, + target; + + evt = evt || window.event; + button = evt.which || evt.button; + target = evt.target || evt.srcElement; + + // Using evt.type (added in IE4), we avoid defining separate handlers for mouseup and mousedown. + if (evt.type === 'click') { + if (target) { + processClick(target, context); + } + } else if (evt.type === 'mousedown') { + if ((button === 1 || button === 2) && target) { + lastButton = button; + lastTarget = target; + } else { + lastButton = lastTarget = null; + } + } else if (evt.type === 'mouseup') { + if (button === lastButton && target === lastTarget) { + processClick(target, context); + } + lastButton = lastTarget = null; + } + }; + } + + /* + * Add click listener to a DOM element + */ + function addClickListener(element) { + if (linkTrackingPseudoClicks) { + // for simplicity and performance, we ignore drag events + helpers.addEventListener(element, 'mouseup', getClickHandler(linkTrackingContext), false); + helpers.addEventListener(element, 'mousedown', getClickHandler(linkTrackingContext), false); + } else { + helpers.addEventListener(element, 'click', getClickHandler(linkTrackingContext), false); + } + } + + return { + + /* + * Configures link click tracking: how to filter which links will be tracked, + * whether to use pseudo click tracking, and what context to attach to link_click events + */ + configureLinkClickTracking: function (criterion, pseudoClicks, trackContent, context) { + linkTrackingContent = trackContent; + linkTrackingContext = context; + linkTrackingPseudoClicks = pseudoClicks; + linkTrackingFilter = helpers.getFilter(criterion, true); + }, + + /* + * Add click handlers to anchor and AREA elements, except those to be ignored + */ + addClickListeners: function () { + + var linkElements = document.links, + i; + + for (i = 0; i < linkElements.length; i++) { + // Add a listener to link elements which pass the filter and aren't already tracked + if (linkTrackingFilter(linkElements[i]) && !linkElements[i][trackerId]) { + addClickListener(linkElements[i]); + linkElements[i][trackerId] = true; + } + } + } + }; +}; diff --git a/snowplow-javascript-tracker/src/js/out_queue.js b/snowplow-javascript-tracker/src/js/out_queue.js new file mode 100644 index 0000000000..d780f8d1d1 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/out_queue.js @@ -0,0 +1,369 @@ +/* + * JavaScript tracker for Snowplow: out_queue.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function() { + + var + mapValues = require('lodash/mapValues'), + isString = require('lodash/isString'), + map = require('lodash/map'), + localStorageAccessible = require('./lib/detectors').localStorageAccessible, + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + /** + * Object handling sending events to a collector. + * Instantiated once per tracker instance. + * + * @param string functionName The Snowplow function name (used to generate the localStorage key) + * @param string namespace The tracker instance's namespace (used to generate the localStorage key) + * @param object mutSnowplowState Gives the pageUnloadGuard a reference to the outbound queue + * so it can unload the page when all queues are empty + * @param boolean useLocalStorage Whether to use localStorage at all + * @param string eventMethod if null will use 'beacon' otherwise can be set to 'post', 'get', or 'beacon' to force. + * @param int bufferSize How many events to batch in localStorage before sending them all. + * Only applies when sending POST requests and when localStorage is available. + * @param int maxPostBytes Maximum combined size in bytes of the event JSONs in a POST request + * @param string postPath The path where events are to be posted + * @return object OutQueueManager instance + */ + object.OutQueueManager = function (functionName, namespace, mutSnowplowState, useLocalStorage, eventMethod, postPath, bufferSize, maxPostBytes) { + var queueName, + executingQueue = false, + configCollectorUrl, + outQueue; + + //Force to lower case if its a string + eventMethod = eventMethod.toLowerCase ? eventMethod.toLowerCase() : eventMethod; + + // Use the Beacon API if eventMethod is set null, true, or 'beacon'. + var isBeaconRequested = (eventMethod === null) || (eventMethod === true) || (eventMethod === "beacon") || (eventMethod === "true"); + // Fall back to POST or GET for browsers which don't support Beacon API + var isBeaconAvailable = Boolean(isBeaconRequested && navigator && navigator.sendBeacon); + var useBeacon = (isBeaconAvailable && isBeaconRequested); + + // Use GET if specified + var isGetRequested = (eventMethod === "get"); + + // Use POST if specified + var isPostRequested = (eventMethod === "post"); + // usePost acts like a catch all for POST methods - Beacon or XHR + var usePost = (isPostRequested || useBeacon) && !isGetRequested; + // Don't use POST for browsers which don't support CORS XMLHttpRequests (e.g. IE <= 9) + usePost = usePost && Boolean(window.XMLHttpRequest && ('withCredentials' in new XMLHttpRequest())); + + // Resolve all options and capabilities and decide path + var path = usePost ? postPath : '/i'; + + bufferSize = (localStorageAccessible() && useLocalStorage && usePost && bufferSize) || 1; + + // Different queue names for GET and POST since they are stored differently + queueName = ['snowplowOutQueue', functionName, namespace, usePost ? 'post2' : 'get'].join('_'); + + if (useLocalStorage) { + // Catch any JSON parse errors or localStorage that might be thrown + try { + // TODO: backward compatibility with the old version of the queue for POST requests + outQueue = JSON.parse(localStorage.getItem(queueName)); + } + catch(e) {} + } + + // Initialize to and empty array if we didn't get anything out of localStorage + if (!Array.isArray(outQueue)) { + outQueue = []; + } + + // Used by pageUnloadGuard + mutSnowplowState.outQueues.push(outQueue); + + if (usePost && bufferSize > 1) { + mutSnowplowState.bufferFlushers.push(function () { + if (!executingQueue) { + executeQueue(); + } + }); + } + + /* + * Convert a dictionary to a querystring + * The context field is the last in the querystring + */ + function getQuerystring(request) { + var querystring = '?', + lowPriorityKeys = {'co': true, 'cx': true}, + firstPair = true; + + for (var key in request) { + if (request.hasOwnProperty(key) && !(lowPriorityKeys.hasOwnProperty(key))) { + if (!firstPair) { + querystring += '&'; + } else { + firstPair = false; + } + querystring += encodeURIComponent(key) + '=' + encodeURIComponent(request[key]); + } + } + + for (var contextKey in lowPriorityKeys) { + if (request.hasOwnProperty(contextKey) && lowPriorityKeys.hasOwnProperty(contextKey)) { + querystring += '&' + contextKey + '=' + encodeURIComponent(request[contextKey]); + } + } + + return querystring; + } + + /* + * Convert numeric fields to strings to match payload_data schema + */ + function getBody(request) { + var cleanedRequest = mapValues(request, function (v) { + return v.toString(); + }); + return { + evt: cleanedRequest, + bytes: getUTF8Length(JSON.stringify(cleanedRequest)) + }; + } + + /** + * Count the number of bytes a string will occupy when UTF-8 encoded + * Taken from http://stackoverflow.com/questions/2848462/count-bytes-in-textarea-using-javascript/ + * + * @param string s + * @return number Length of s in bytes when UTF-8 encoded + */ + function getUTF8Length(s) { + var len = 0; + for (var i = 0; i < s.length; i++) { + var code = s.charCodeAt(i); + if (code <= 0x7f) { + len += 1; + } else if (code <= 0x7ff) { + len += 2; + } else if (code >= 0xd800 && code <= 0xdfff) { + // Surrogate pair: These take 4 bytes in UTF-8 and 2 chars in UCS-2 + // (Assume next char is the other [valid] half and just skip it) + len += 4; i++; + } else if (code < 0xffff) { + len += 3; + } else { + len += 4; + } + } + return len; + } + + /* + * Queue an image beacon for submission to the collector. + * If we're not processing the queue, we'll start. + */ + function enqueueRequest (request, url) { + + configCollectorUrl = url + path; + if (usePost) { + var body = getBody(request); + if (body.bytes >= maxPostBytes) { + helpers.warn("Event of size " + body.bytes + " is too long - the maximum size is " + maxPostBytes); + var xhr = initializeXMLHttpRequest(configCollectorUrl); + xhr.send(encloseInPayloadDataEnvelope(attachStmToEvent([body.evt]))); + return; + } else { + outQueue.push(body); + } + } else { + outQueue.push(getQuerystring(request)); + } + var savedToLocalStorage = false; + if (useLocalStorage) { + savedToLocalStorage = helpers.attemptWriteLocalStorage(queueName, JSON.stringify(outQueue)); + } + + if (!executingQueue && (!savedToLocalStorage || outQueue.length >= bufferSize)) { + executeQueue(); + } + } + + /* + * Run through the queue of image beacons, sending them one at a time. + * Stops processing when we run out of queued requests, or we get an error. + */ + function executeQueue () { + + // Failsafe in case there is some way for a bad value like "null" to end up in the outQueue + while (outQueue.length && typeof outQueue[0] !== 'string' && typeof outQueue[0] !== 'object') { + outQueue.shift(); + } + + if (outQueue.length < 1) { + executingQueue = false; + return; + } + + // Let's check that we have a Url to ping + if (!isString(configCollectorUrl)) { + throw "No Snowplow collector configured, cannot track"; + } + + executingQueue = true; + + var nextRequest = outQueue[0]; + + if (usePost) { + + var xhr = initializeXMLHttpRequest(configCollectorUrl); + + // Time out POST requests after 5 seconds + var xhrTimeout = setTimeout(function () { + xhr.abort(); + executingQueue = false; + }, 5000); + + function chooseHowManyToExecute(q) { + var numberToSend = 0; + var byteCount = 0; + while (numberToSend < q.length) { + byteCount += q[numberToSend].bytes; + if (byteCount >= maxPostBytes) { + break; + } else { + numberToSend += 1; + } + } + return numberToSend; + } + + // Keep track of number of events to delete from queue + var numberToSend = chooseHowManyToExecute(outQueue); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 400) { + for (var deleteCount = 0; deleteCount < numberToSend; deleteCount++) { + outQueue.shift(); + } + if (useLocalStorage) { + helpers.attemptWriteLocalStorage(queueName, JSON.stringify(outQueue)); + } + clearTimeout(xhrTimeout); + executeQueue(); + } else if (xhr.readyState === 4 && xhr.status >= 400) { + clearTimeout(xhrTimeout); + executingQueue = false; + } + }; + + var batch = map(outQueue.slice(0, numberToSend), function (x) { + return x.evt; + }); + + if (batch.length > 0) { + var beaconStatus; + + if (useBeacon) { + const headers = { type: 'application/json' }; + const blob = new Blob([encloseInPayloadDataEnvelope(attachStmToEvent(batch))], headers); + beaconStatus = navigator.sendBeacon(configCollectorUrl, blob); + } + if (!useBeacon || !beaconStatus) { + xhr.send(encloseInPayloadDataEnvelope(attachStmToEvent(batch))); + } + } + + } else { + var image = new Image(1, 1); + + image.onload = function () { + outQueue.shift(); + if (useLocalStorage) { + helpers.attemptWriteLocalStorage(queueName, JSON.stringify(outQueue)); + } + executeQueue(); + }; + + image.onerror = function () { + executingQueue = false; + }; + + image.src = configCollectorUrl + nextRequest.replace('?', '?stm=' + new Date().getTime() + '&'); + } + } + + /** + * Open an XMLHttpRequest for a given endpoint with the correct credentials and header + * + * @param string url The destination URL + * @return object The XMLHttpRequest + */ + function initializeXMLHttpRequest(url) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.withCredentials = true; + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + return xhr; + } + + /** + * Enclose an array of events in a self-describing payload_data JSON string + * + * @param array events Batch of events + * @return string payload_data self-describing JSON + */ + function encloseInPayloadDataEnvelope(events) { + return JSON.stringify({ + schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4', + data: events + }); + } + + /** + * Attaches the STM field to outbound POST events. + * + * @param events the events to attach the STM to + */ + function attachStmToEvent(events) { + var stm = new Date().getTime().toString(); + for (var i = 0; i < events.length; i++) { + events[i]['stm'] = stm; + } + return events + } + + return { + enqueueRequest: enqueueRequest, + executeQueue: executeQueue + }; + }; + +}()); diff --git a/snowplow-javascript-tracker/src/js/snowplow.js b/snowplow-javascript-tracker/src/js/snowplow.js new file mode 100644 index 0000000000..008976a8f5 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/snowplow.js @@ -0,0 +1,268 @@ +/* + * JavaScript tracker for Snowplow: snowplow.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/*jslint browser:true, plusplus:true, vars:true, nomen:true, evil:true */ +/*global window */ +/*global unescape */ +/*global ActiveXObject */ +/*global _snaq:true */ +/*members encodeURIComponent, decodeURIComponent, getElementsByTagName, + shift, unshift, + addEventListener, attachEvent, removeEventListener, detachEvent, + cookie, domain, readyState, documentElement, doScroll, title, text, + location, top, document, referrer, parent, links, href, protocol, GearsFactory, + event, which, button, srcElement, type, target, + parentNode, tagName, hostname, className, + userAgent, cookieEnabled, platform, mimeTypes, enabledPlugin, javaEnabled, + XDomainRequest, XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, setRequestHeader, send, readyState, status, + getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds, + toLowerCase, charAt, indexOf, lastIndexOf, split, slice, toUpperCase, + onload, src, + round, random, + exec, + res, width, height, + pdf, qt, realp, wma, dir, fla, java, gears, ag, + hook, getHook, + setCollectorCf, setCollectorUrl, setAppId, + setDownloadExtensions, addDownloadExtensions, + setDomains, setIgnoreClasses, setRequestMethod, + setReferrerUrl, setCustomUrl, setDocumentTitle, + setDownloadClasses, setLinkClasses, + discardHashTag, + setCookieNamePrefix, setCookieDomain, setCookiePath, setVisitorIdCookie, + setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout, + doNotTrack, respectDoNotTrack, msDoNotTrack, getTimestamp, getCookieValue, + detectTimezone, detectViewport, + addListener, enableLinkTracking, enableActivityTracking, setLinkTrackingTimer, + enableDarkSocialTracking, + killFrame, redirectFile, setCountPreRendered, + trackLink, trackPageView, trackImpression, + addPlugin, getAsyncTracker +*/ + +;(function() { + + // Load all our modules (at least until we fully modularize & remove grunt-concat) + var + uuid = require('uuid'), + forEach = require('lodash/forEach'), + filter = require('lodash/filter'), + helpers = require('./lib/helpers'), + queue = require('./in_queue'), + tracker = require('./tracker'), + + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + object.Snowplow = function(asynchronousQueue, functionName) { + + var + documentAlias = document, + windowAlias = window, + + /* Tracker identifier with version */ + version = 'js-' + '<%= pkg.version %>', // Update banner.js too + + /* Contains four variables that are shared with tracker.js and must be passed by reference */ + mutSnowplowState = { + + /* List of request queues - one per Tracker instance */ + outQueues: [], + bufferFlushers: [], + + /* Time at which to stop blocking excecution */ + expireDateTime: null, + + /* DOM Ready */ + hasLoaded: false, + registeredOnLoadHandlers: [], + + /* pageViewId, which can changed by other trackers on page; + * initialized by tracker sent first event */ + pageViewId: null + }; + + /************************************************************ + * Private methods + ************************************************************/ + + + /* + * Handle beforeunload event + * + * Subject to Safari's "Runaway JavaScript Timer" and + * Chrome V8 extension that terminates JS that exhibits + * "slow unload", i.e., calling getTime() > 1000 times + */ + function beforeUnloadHandler() { + var now; + + // Flush all POST queues + forEach(mutSnowplowState.bufferFlushers, function (flusher) { + flusher(); + }); + + /* + * Delay/pause (blocks UI) + */ + if (mutSnowplowState.expireDateTime) { + // the things we do for backwards compatibility... + // in ECMA-262 5th ed., we could simply use: + // while (Date.now() < mutSnowplowState.expireDateTime) { } + do { + now = new Date(); + if (filter(mutSnowplowState.outQueues, function (queue) { + return queue.length > 0; + }).length === 0) { + break; + } + } while (now.getTime() < mutSnowplowState.expireDateTime); + } + } + + /* + * Handler for onload event + */ + function loadHandler() { + var i; + + if (!mutSnowplowState.hasLoaded) { + mutSnowplowState.hasLoaded = true; + for (i = 0; i < mutSnowplowState.registeredOnLoadHandlers.length; i++) { + mutSnowplowState.registeredOnLoadHandlers[i](); + } + } + return true; + } + + /* + * Add onload or DOM ready handler + */ + function addReadyListener() { + var _timer; + + if (documentAlias.addEventListener) { + helpers.addEventListener(documentAlias, 'DOMContentLoaded', function ready() { + documentAlias.removeEventListener('DOMContentLoaded', ready, false); + loadHandler(); + }); + } else if (documentAlias.attachEvent) { + documentAlias.attachEvent('onreadystatechange', function ready() { + if (documentAlias.readyState === 'complete') { + documentAlias.detachEvent('onreadystatechange', ready); + loadHandler(); + } + }); + + if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) { + (function ready() { + if (!mutSnowplowState.hasLoaded) { + try { + documentAlias.documentElement.doScroll('left'); + } catch (error) { + setTimeout(ready, 0); + return; + } + loadHandler(); + } + }()); + } + } + + // sniff for older WebKit versions + if ((new RegExp('WebKit')).test(navigator.userAgent)) { + _timer = setInterval(function () { + if (mutSnowplowState.hasLoaded || /loaded|complete/.test(documentAlias.readyState)) { + clearInterval(_timer); + loadHandler(); + } + }, 10); + } + + // fallback + helpers.addEventListener(windowAlias, 'load', loadHandler, false); + } + + /************************************************************ + * Public data and methods + ************************************************************/ + + windowAlias.Snowplow = { + + /** + * Returns a Tracker object, configured with a + * CloudFront collector. + * + * @param string distSubdomain The subdomain on your CloudFront collector's distribution + */ + getTrackerCf: function (distSubdomain) { + var t = new tracker.Tracker(functionName, '', version, mutSnowplowState, {}); + t.setCollectorCf(distSubdomain); + return t; + }, + + /** + * Returns a Tracker object, configured with the + * URL to the collector to use. + * + * @param string rawUrl The collector URL minus protocol and /i + */ + getTrackerUrl: function (rawUrl) { + var t = new tracker.Tracker(functionName, '', version, mutSnowplowState, {}); + t.setCollectorUrl(rawUrl); + return t; + }, + + /** + * Get internal asynchronous tracker object + * + * @return Tracker + */ + getAsyncTracker: function () { + return new tracker.Tracker(functionName, '', version, mutSnowplowState, {}); + } + }; + + /************************************************************ + * Constructor + ************************************************************/ + + // initialize the Snowplow singleton + helpers.addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); + addReadyListener(); + + // Now replace initialization array with queue manager object + return new queue.InQueueManager(tracker.Tracker, version, mutSnowplowState, asynchronousQueue, functionName); + }; + +}()); diff --git a/snowplow-javascript-tracker/src/js/tracker.js b/snowplow-javascript-tracker/src/js/tracker.js new file mode 100755 index 0000000000..e8b96feb84 --- /dev/null +++ b/snowplow-javascript-tracker/src/js/tracker.js @@ -0,0 +1,2739 @@ +/* + * JavaScript tracker for Snowplow: tracker.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2016 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function() { + + var + forEach = require('lodash/forEach'), + map = require('lodash/map'), + helpers = require('./lib/helpers'), + proxies = require('./lib/proxies'), + cookie = require('browser-cookie-lite'), + detectors = require('./lib/detectors'), + sha1 = require('sha1'), + links = require('./links'), + forms = require('./forms'), + errors = require('./errors'), + requestQueue = require('./out_queue'), + coreConstructor = require('snowplow-tracker-core').trackerCore, + productionize = require('./guard').productionize, + uuid = require('uuid'), + + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + /** + * Snowplow Tracker class + * + * @param functionName global function name + * @param namespace The namespace of the tracker object + * @param version The current version of the JavaScript Tracker + * @param mutSnowplowState An object containing hasLoaded, registeredOnLoadHandlers, and expireDateTime + * Passed in by reference in case they are altered by snowplow.js + * @param argmap Optional dictionary of configuration options. Supported fields and their default values: + * + * 1. encodeBase64, true + * 2. cookieDomain, null + * 3. cookieName, '_sp_' + * 4. appId, '' + * 5. platform, 'web' + * 6. respectDoNotTrack, false + * 7. userFingerprintSeed, 123412414 + * 8. pageUnloadTimer, 500 + * 9. forceSecureTracker, false + * 10. forceUnsecureTracker, false + * 11. useLocalStorage, true + * 12. useCookies, true + * 13. sessionCookieTimeout, 1800 + * 14. contexts, {} + * 15. eventMethod, 'beacon' + * 16. post, false *DEPRECATED use eventMethod instead* + * 17. postPath, null + * 18. bufferSize, 1 + * 19. crossDomainLinker, false + * 20. maxPostBytes, 40000 + * 21. discoverRootDomain, false + * 22. cookieLifetime, 63072000 + * 23. stateStorageStrategy, 'cookieAndLocalStorage' + */ + object.Tracker = function Tracker(functionName, namespace, version, mutSnowplowState, argmap) { + + /************************************************************ + * Private members + ************************************************************/ + + var argmap = argmap || {}; + + //use POST if that property is present on the argmap + if(argmap.hasOwnProperty('post')) { + argmap.eventMethod = argmap.post === true ? 'post' : 'get'; + } else { + argmap.eventMethod = argmap.eventMethod || 'beacon' + } + + var + // Tracker core + core = coreConstructor(true, function(payload) { + addBrowserData(payload); + sendRequest(payload, configTrackerPause); + }), + + // Debug - whether to raise errors to console and log to console + // or silence all errors from public methods + debug = false, + + // API functions of the tracker + apiMethods = {}, + + // Safe methods (i.e. ones that won't raise errors) + // These values should be guarded publicMethods + safeMethods = {}, + + // The client-facing methods returned from tracker IIFE + returnMethods = {}, + + // Aliases + documentAlias = document, + windowAlias = window, + navigatorAlias = navigator, + + // Current URL and Referrer URL + locationArray = proxies.fixupUrl(documentAlias.domain, windowAlias.location.href, helpers.getReferrer()), + domainAlias = helpers.fixupDomain(locationArray[0]), + locationHrefAlias = locationArray[1], + configReferrerUrl = locationArray[2], + + // Holder of the logPagePing interval + pagePingInterval, + + customReferrer, + + // Request method is always GET for Snowplow + configRequestMethod = 'GET', + + // Platform defaults to web for this tracker + configPlatform = argmap.hasOwnProperty('platform') ? argmap.platform : 'web', + + // Snowplow collector URL + configCollectorUrl, + + // Custom path for post requests (to get around adblockers) + configPostPath = argmap.hasOwnProperty('postPath') ? argmap.postPath : '/com.snowplowanalytics.snowplow/tp2', + + // Site ID + configTrackerSiteId = argmap.hasOwnProperty('appId') ? argmap.appId : '', // Updated for Snowplow + + // Document URL + configCustomUrl, + + // Document title + lastDocumentTitle = documentAlias.title, + + // Custom title + lastConfigTitle, + + // Maximum delay to wait for web bug image to be fetched (in milliseconds) + configTrackerPause = argmap.hasOwnProperty('pageUnloadTimer') ? argmap.pageUnloadTimer : 500, + + // Whether appropriate values have been supplied to enableActivityTracking + activityTrackingEnabled = false, + + // Minimum visit time after initial page view (in milliseconds) + configMinimumVisitTime, + + // Recurring heart beat after initial ping (in milliseconds) + configHeartBeatTimer, + + // Disallow hash tags in URL. TODO: Should this be set to true by default? + configDiscardHashTag, + + // First-party cookie name prefix + configCookieNamePrefix = argmap.hasOwnProperty('cookieName') ? argmap.cookieName : '_sp_', + + // First-party cookie domain + // User agent defaults to origin hostname + configCookieDomain = argmap.hasOwnProperty('cookieDomain') ? argmap.cookieDomain : null, + + // First-party cookie path + // Default is user agent defined. + configCookiePath = '/', + + // Do Not Track browser feature + dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack || windowAlias.doNotTrack, + + // Do Not Track + configDoNotTrack = argmap.hasOwnProperty('respectDoNotTrack') ? argmap.respectDoNotTrack && (dnt === 'yes' || dnt === '1') : false, + + // Opt out of cookie tracking + configOptOutCookie, + + // Count sites which are pre-rendered + configCountPreRendered, + + // Life of the visitor cookie (in seconds) + configVisitorCookieTimeout = argmap.hasOwnProperty('cookieLifetime') ? argmap.cookieLifetime : 63072000, // 2 years + + // Life of the session cookie (in seconds) + configSessionCookieTimeout = argmap.hasOwnProperty('sessionCookieTimeout') ? argmap.sessionCookieTimeout : 1800, // 30 minutes + + // Default hash seed for MurmurHash3 in detectors.detectSignature + configUserFingerprintHashSeed = argmap.hasOwnProperty('userFingerprintSeed') ? argmap.userFingerprintSeed : 123412414, + + // Document character set + documentCharset = documentAlias.characterSet || documentAlias.charset, + + // This forces the tracker to be HTTPS even if the page is not secure + forceSecureTracker = argmap.hasOwnProperty('forceSecureTracker') ? (argmap.forceSecureTracker === true) : false, + + // This forces the tracker to be HTTP even if the page is secure + forceUnsecureTracker = !forceSecureTracker && argmap.hasOwnProperty('forceUnsecureTracker') ? (argmap.forceUnsecureTracker === true) : false, + + // Whether to use localStorage to store events between sessions while offline + useLocalStorage = argmap.hasOwnProperty('useLocalStorage') ? ( + helpers.warn('argmap.useLocalStorage is deprecated. ' + + 'Use argmap.stateStorageStrategy instead.'), + argmap.useLocalStorage + ) : true, + + // Whether to use cookies + configUseCookies = argmap.hasOwnProperty('useCookies') ? ( + helpers.warn( + 'argmap.useCookies is deprecated. Use argmap.stateStorageStrategy instead.'), + argmap.useCookies + ) : true, + + // Strategy defining how to store the state: cookie, localStorage or none + configStateStorageStrategy = argmap.hasOwnProperty('stateStorageStrategy') ? + argmap.stateStorageStrategy : (!configUseCookies && !useLocalStorage ? + 'none' : (configUseCookies && useLocalStorage ? + 'cookieAndLocalStorage' : (configUseCookies ? 'cookie' : 'localStorage'))), + + // Browser language (or Windows language for IE). Imperfect but CloudFront doesn't log the Accept-Language header + browserLanguage = navigatorAlias.userLanguage || navigatorAlias.language, + + // Browser features via client-side data collection + browserFeatures = detectors.detectBrowserFeatures( + configStateStorageStrategy == 'cookie' || + configStateStorageStrategy == 'cookieAndLocalStorage', + getSnowplowCookieName('testcookie')), + + // Visitor fingerprint + userFingerprint = (argmap.userFingerprint === false) ? '' : detectors.detectSignature(configUserFingerprintHashSeed), + + // Unique ID for the tracker instance used to mark links which are being tracked + trackerId = functionName + '_' + namespace, + + // Guard against installing the activity tracker more than once per Tracker instance + activityTrackingInstalled = false, + + // Last activity timestamp + lastActivityTime, + + // The last time an event was fired on the page - used to invalidate session if cookies are disabled + lastEventTime = new Date().getTime(), + + // How are we scrolling? + minXOffset, + maxXOffset, + minYOffset, + maxYOffset, + + // Hash function + hash = sha1, + + // Domain hash value + domainHash, + + // Domain unique user ID + domainUserId, + + // ID for the current session + memorizedSessionId, + + // Index for the current session - kept in memory in case cookies are disabled + memorizedVisitCount = 1, + + // Business-defined unique user ID + businessUserId, + + // Ecommerce transaction data + // Will be committed, sent and emptied by a call to trackTrans. + ecommerceTransaction = ecommerceTransactionTemplate(), + + // Manager for automatic link click tracking + linkTrackingManager = links.getLinkTrackingManager(core, trackerId, addCommonContexts), + + // Manager for automatic form tracking + formTrackingManager = forms.getFormTrackingManager(core, trackerId, addCommonContexts), + + // Manager for tracking unhandled exceptions + errorManager = errors.errorManager(core), + + // Manager for local storage queue + outQueueManager = new requestQueue.OutQueueManager( + functionName, + namespace, + mutSnowplowState, + configStateStorageStrategy == 'localStorage' || + configStateStorageStrategy == 'cookieAndLocalStorage', + argmap.eventMethod, + configPostPath, + argmap.bufferSize, + argmap.maxPostBytes || 40000), + + // Flag to prevent the geolocation context being added multiple times + geolocationContextAdded = false, + + // Set of contexts to be added to every event + autoContexts = argmap.contexts || {}, + + // Context to be added to every event + commonContexts = [], + + // Enhanced Ecommerce Contexts to be added on every `trackEnhancedEcommerceAction` call + enhancedEcommerceContexts = [], + + // Whether pageViewId should be regenerated after each trackPageView. Affect web_page context + preservePageViewId = false, + + // Whether first trackPageView was fired and pageViewId should not be changed anymore until reload + pageViewSent = false; + + if (argmap.hasOwnProperty('discoverRootDomain') && argmap.discoverRootDomain) { + configCookieDomain = helpers.findRootDomain(); + } + + if (autoContexts.gaCookies) { + commonContexts.push(getGaCookiesContext()); + } + + if (autoContexts.geolocation) { + enableGeolocationContext(); + } + + // Enable base 64 encoding for self-describing events and custom contexts + core.setBase64Encoding(argmap.hasOwnProperty('encodeBase64') ? argmap.encodeBase64 : true); + + // Set up unchanging name-value pairs + core.setTrackerVersion(version); + core.setTrackerNamespace(namespace); + core.setAppId(configTrackerSiteId); + core.setPlatform(configPlatform); + core.setTimezone(detectors.detectTimezone()); + core.addPayloadPair('lang', browserLanguage); + core.addPayloadPair('cs', documentCharset); + + // Browser features. Cookies, color depth and resolution don't get prepended with f_ (because they're not optional features) + for (var i in browserFeatures) { + if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { + if (i === 'res' || i === 'cd' || i === 'cookie') { + core.addPayloadPair(i, browserFeatures[i]); + } else { + core.addPayloadPair('f_' + i, browserFeatures[i]); + } + } + } + + /** + * Recalculate the domain, URL, and referrer + */ + function refreshUrl() { + locationArray = proxies.fixupUrl(documentAlias.domain, windowAlias.location.href, helpers.getReferrer()); + + // If this is a single-page app and the page URL has changed, then: + // - if the new URL's querystring contains a "refer(r)er" parameter, use it as the referrer + // - otherwise use the old URL as the referer + if (locationArray[1] !== locationHrefAlias) { + configReferrerUrl = helpers.getReferrer(locationHrefAlias); + } + + domainAlias = helpers.fixupDomain(locationArray[0]); + locationHrefAlias = locationArray[1]; + } + + /** + * Decorate the querystring of a single link + * + * @param event e The event targeting the link + */ + function linkDecorationHandler() { + var tstamp = new Date().getTime(); + if (this.href) { + this.href = helpers.decorateQuerystring(this.href, '_sp', domainUserId + '.' + tstamp); + } + } + + /** + * Enable querystring decoration for links pasing a filter + * Whenever such a link is clicked on or navigated to via the keyboard, + * add "_sp={{duid}}.{{timestamp}}" to its querystring + * + * @param crossDomainLinker Function used to determine which links to decorate + */ + function decorateLinks(crossDomainLinker) { + for (var i=0; i= 0) { + baseUrl = baseUrl.slice(0, i); + } + if ((i = baseUrl.lastIndexOf('/')) !== baseUrl.length - 1) { + baseUrl = baseUrl.slice(0, i + 1); + } + + return baseUrl + url; + } + + /* + * Send request + */ + function sendRequest(request, delay) { + var now = new Date(); + + // Set to true if Opt-out cookie is defined + var toOptoutByCookie; + if (configOptOutCookie) { + toOptoutByCookie = !!cookie.cookie(configOptOutCookie); + } else { + toOptoutByCookie = false; + } + + if (!(configDoNotTrack || toOptoutByCookie)) { + outQueueManager.enqueueRequest(request.build(), configCollectorUrl); + mutSnowplowState.expireDateTime = now.getTime() + delay; + } + } + + /* + * Get cookie name with prefix and domain hash + */ + function getSnowplowCookieName(baseName) { + return configCookieNamePrefix + baseName + '.' + domainHash; + } + + /* + * Cookie getter. + */ + function getSnowplowCookieValue(cookieName) { + var fullName = getSnowplowCookieName(cookieName); + if (configStateStorageStrategy == 'localStorage') { + return helpers.attemptGetLocalStorage(fullName); + } else if (configStateStorageStrategy == 'cookie' || + configStateStorageStrategy == 'cookieAndLocalStorage') { + return cookie.cookie(fullName); + } + } + + /* + * Update domain hash + */ + function updateDomainHash() { + refreshUrl(); + domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits + } + + /* + * Process all "activity" events. + * For performance, this function must have low overhead. + */ + function activityHandler() { + var now = new Date(); + lastActivityTime = now.getTime(); + } + + /* + * Process all "scroll" events. + */ + function scrollHandler() { + updateMaxScrolls(); + activityHandler(); + } + + /* + * Returns [pageXOffset, pageYOffset]. + * Adapts code taken from: http://www.javascriptkit.com/javatutors/static2.shtml + */ + function getPageOffsets() { + var iebody = (documentAlias.compatMode && documentAlias.compatMode !== "BackCompat") ? + documentAlias.documentElement : + documentAlias.body; + return [iebody.scrollLeft || windowAlias.pageXOffset, iebody.scrollTop || windowAlias.pageYOffset]; + } + + /* + * Quick initialization/reset of max scroll levels + */ + function resetMaxScrolls() { + var offsets = getPageOffsets(); + + var x = offsets[0]; + minXOffset = x; + maxXOffset = x; + + var y = offsets[1]; + minYOffset = y; + maxYOffset = y; + } + + /* + * Check the max scroll levels, updating as necessary + */ + function updateMaxScrolls() { + var offsets = getPageOffsets(); + + var x = offsets[0]; + if (x < minXOffset) { + minXOffset = x; + } else if (x > maxXOffset) { + maxXOffset = x; + } + + var y = offsets[1]; + if (y < minYOffset) { + minYOffset = y; + } else if (y > maxYOffset) { + maxYOffset = y; + } + } + + /* + * Prevents offsets from being decimal or NaN + * See https://github.com/snowplow/snowplow-javascript-tracker/issues/324 + * TODO: the NaN check should be moved into the core + */ + function cleanOffset(offset) { + var rounded = Math.round(offset); + if (!isNaN(rounded)) { + return rounded; + } + } + + /* + * Sets or renews the session cookie + */ + function setSessionCookie() { + var cookieName = getSnowplowCookieName('ses'); + var cookieValue = '*'; + setCookie(cookieName, cookieValue, configSessionCookieTimeout); + } + + /* + * Sets the Visitor ID cookie: either the first time loadDomainUserIdCookie is called + * or when there is a new visit or a new page view + */ + function setDomainUserIdCookie(_domainUserId, createTs, visitCount, nowTs, lastVisitTs, sessionId) { + var cookieName = getSnowplowCookieName('id'); + var cookieValue = _domainUserId + '.' + createTs + '.' + visitCount + '.' + nowTs + + '.' + lastVisitTs + '.' + sessionId; + setCookie(cookieName, cookieValue, configVisitorCookieTimeout); + } + + /* + * Sets a cookie based on the storage strategy: + * - if 'localStorage': attemps to write to local storage + * - if 'cookie': writes to cookies + * - otherwise: no-op + */ + function setCookie(name, value, timeout) { + if (configStateStorageStrategy == 'localStorage') { + helpers.attemptWriteLocalStorage(name, value); + } else if (configStateStorageStrategy == 'cookie' || + configStateStorageStrategy == 'cookieAndLocalStorage') { + cookie.cookie(name, value, timeout, configCookiePath, configCookieDomain); + } + } + + /** + * Generate a pseudo-unique ID to fingerprint this user + */ + function createNewDomainUserId() { + return uuid.v4(); + } + + /* + * Load the domain user ID and the session ID + * Set the cookies (if cookies are enabled) + */ + function initializeIdsAndCookies() { + var sesCookieSet = + configStateStorageStrategy != 'none' && !!getSnowplowCookieValue('ses'); + var idCookieComponents = loadDomainUserIdCookie(); + + if (idCookieComponents[1]) { + domainUserId = idCookieComponents[1]; + } else { + domainUserId = createNewDomainUserId(); + idCookieComponents[1] = domainUserId; + } + + memorizedSessionId = idCookieComponents[6]; + + if (!sesCookieSet) { + // Increment the session ID + idCookieComponents[3] ++; + // Create a new sessionId + memorizedSessionId = uuid.v4(); + idCookieComponents[6] = memorizedSessionId; + // Set lastVisitTs to currentVisitTs + idCookieComponents[5] = idCookieComponents[4]; + } + + if (configStateStorageStrategy != 'none') { + setSessionCookie(); + // Update currentVisitTs + idCookieComponents[4] = Math.round(new Date().getTime() / 1000); + idCookieComponents.shift(); + setDomainUserIdCookie.apply(null, idCookieComponents); + } + } + + /* + * Load visitor ID cookie + */ + function loadDomainUserIdCookie() { + if (configStateStorageStrategy == 'none') { + return []; + } + var now = new Date(), + nowTs = Math.round(now.getTime() / 1000), + id = getSnowplowCookieValue('id'), + tmpContainer; + + if (id) { + tmpContainer = id.split('.'); + // cookies enabled + tmpContainer.unshift('0'); + } else { + + tmpContainer = [ + // cookies disabled + '1', + // Domain user ID + domainUserId, + // Creation timestamp - seconds since Unix epoch + nowTs, + // visitCount - 0 = no previous visit + 0, + // Current visit timestamp + nowTs, + // Last visit timestamp - blank meaning no previous visit + '' + ]; + } + + if (!tmpContainer[6]) { + // session id + tmpContainer[6] = uuid.v4(); + } + + return tmpContainer; + } + + /* + * Attaches common web fields to every request + * (resolution, url, referrer, etc.) + * Also sets the required cookies. + */ + function addBrowserData(sb) { + var nowTs = Math.round(new Date().getTime() / 1000), + idname = getSnowplowCookieName('id'), + sesname = getSnowplowCookieName('ses'), + ses = getSnowplowCookieValue('ses'), + id = loadDomainUserIdCookie(), + cookiesDisabled = id[0], + _domainUserId = id[1], // We could use the global (domainUserId) but this is better etiquette + createTs = id[2], + visitCount = id[3], + currentVisitTs = id[4], + lastVisitTs = id[5], + sessionIdFromCookie = id[6]; + + var toOptoutByCookie; + if (configOptOutCookie) { + toOptoutByCookie = !!cookie.cookie(configOptOutCookie); + } else { + toOptoutByCookie = false; + } + + + if ((configDoNotTrack || toOptoutByCookie) && + configStateStorageStrategy != 'none') { + if (configStateStorageStrategy == 'localStorage') { + helpers.attemptWriteLocalStorage(idname, ''); + helpers.attemptWriteLocalStorage(sesname, ''); + } else if (configStateStorageStrategy == 'cookie' || + configStateStorageStrategy == 'cookieAndLocalStorage') { + cookie.cookie(idname, '', -1, configCookiePath, configCookieDomain); + cookie.cookie(sesname, '', -1, configCookiePath, configCookieDomain); + } + return; + } + + // If cookies are enabled, base visit count and session ID on the cookies + if (cookiesDisabled === '0') { + memorizedSessionId = sessionIdFromCookie; + + // New session? + if (!ses && configStateStorageStrategy != 'none') { + // New session (aka new visit) + visitCount++; + // Update the last visit timestamp + lastVisitTs = currentVisitTs; + // Regenerate the session ID + memorizedSessionId = uuid.v4(); + } + + memorizedVisitCount = visitCount; + + // Otherwise, a new session starts if configSessionCookieTimeout seconds have passed since the last event + } else { + if ((new Date().getTime() - lastEventTime) > configSessionCookieTimeout * 1000) { + memorizedSessionId = uuid.v4(); + memorizedVisitCount++; + } + } + + // Build out the rest of the request + sb.add('vp', detectors.detectViewport()); + sb.add('ds', detectors.detectDocumentSize()); + sb.add('vid', memorizedVisitCount); + sb.add('sid', memorizedSessionId); + sb.add('duid', _domainUserId); // Set to our local variable + sb.add('fp', userFingerprint); + sb.add('uid', businessUserId); + + refreshUrl(); + + sb.add('refr', purify(customReferrer || configReferrerUrl)); + + // Add the page URL last as it may take us over the IE limit (and we don't always need it) + sb.add('url', purify(configCustomUrl || locationHrefAlias)); + + // Update cookies + if (configStateStorageStrategy != 'none') { + setDomainUserIdCookie(_domainUserId, createTs, memorizedVisitCount, nowTs, + lastVisitTs, memorizedSessionId); + setSessionCookie(); + } + + lastEventTime = new Date().getTime(); + } + + /** + * Builds a collector URL from a CloudFront distribution. + * We don't bother to support custom CNAMEs because Amazon CloudFront doesn't support that for SSL. + * + * @param string account The account ID to build the tracker URL from + * + * @return string The URL on which the collector is hosted + */ + function collectorUrlFromCfDist(distSubdomain) { + return asCollectorUrl(distSubdomain + '.cloudfront.net'); + } + + /** + * Adds the protocol in front of our collector URL, and i to the end + * + * @param string rawUrl The collector URL without protocol + * + * @return string collectorUrl The tracker URL with protocol + */ + function asCollectorUrl(rawUrl) { + if (forceSecureTracker) { + return ('https' + '://' + rawUrl); + } + if (forceUnsecureTracker) { + return ('http' + '://' + rawUrl); + } + return ('https:' === documentAlias.location.protocol ? 'https' : 'http') + '://' + rawUrl; + } + + /** + * Add common contexts to every event + * TODO: move this functionality into the core + * + * @param array userContexts List of user-defined contexts + * @return userContexts combined with commonContexts + */ + function addCommonContexts(userContexts) { + var combinedContexts = commonContexts.concat(userContexts || []); + + if (autoContexts.webPage) { + combinedContexts.push(getWebPageContext()); + } + + // Add PerformanceTiming Context + if (autoContexts.performanceTiming) { + var performanceTimingContext = getPerformanceTimingContext(); + if (performanceTimingContext) { + combinedContexts.push(performanceTimingContext); + } + } + + // Add Optimizely Contexts + if (windowAlias.optimizely) { + + if (autoContexts.optimizelySummary) { + var activeExperiments = getOptimizelySummaryContexts(); + forEach(activeExperiments, function (e) { + combinedContexts.push(e) + }) + } + + if (autoContexts.optimizelyXSummary) { + var activeExperiments = getOptimizelyXSummaryContexts(); + forEach(activeExperiments, function (e) { + combinedContexts.push(e); + }) + } + + if (autoContexts.optimizelyExperiments) { + var experimentContexts = getOptimizelyExperimentContexts(); + for (var i = 0; i < experimentContexts.length; i++) { + combinedContexts.push(experimentContexts[i]); + } + } + + if (autoContexts.optimizelyStates) { + var stateContexts = getOptimizelyStateContexts(); + for (var i = 0; i < stateContexts.length; i++) { + combinedContexts.push(stateContexts[i]); + } + } + + if (autoContexts.optimizelyVariations) { + var variationContexts = getOptimizelyVariationContexts(); + for (var i = 0; i < variationContexts.length; i++) { + combinedContexts.push(variationContexts[i]); + } + } + + if (autoContexts.optimizelyVisitor) { + var optimizelyVisitorContext = getOptimizelyVisitorContext(); + if (optimizelyVisitorContext) { + combinedContexts.push(optimizelyVisitorContext); + } + } + + if (autoContexts.optimizelyAudiences) { + var audienceContexts = getOptimizelyAudienceContexts(); + for (var i = 0; i < audienceContexts.length; i++) { + combinedContexts.push(audienceContexts[i]); + } + } + + if (autoContexts.optimizelyDimensions) { + var dimensionContexts = getOptimizelyDimensionContexts(); + for (var i = 0; i < dimensionContexts.length; i++) { + combinedContexts.push(dimensionContexts[i]); + } + } + } + + // Add Augur Context + if (autoContexts.augurIdentityLite) { + var augurIdentityLiteContext = getAugurIdentityLiteContext(); + if (augurIdentityLiteContext) { + combinedContexts.push(augurIdentityLiteContext); + } + } + + //Add Parrable Context + if (autoContexts.parrable) { + var parrableContext = getParrableContext(); + if (parrableContext) { + combinedContexts.push(parrableContext); + } + } + return combinedContexts; + } + + /** + * Initialize new `pageViewId` if it shouldn't be preserved. + * Should be called when `trackPageView` is invoked + */ + function resetPageView() { + if (!preservePageViewId || mutSnowplowState.pageViewId == null) { + mutSnowplowState.pageViewId = uuid.v4(); + } + } + + /** + * Safe function to get `pageViewId`. + * Generates it if it wasn't initialized by other tracker + */ + function getPageViewId() { + if (mutSnowplowState.pageViewId == null) { + mutSnowplowState.pageViewId = uuid.v4(); + } + return mutSnowplowState.pageViewId + } + + /** + * Put together a web page context with a unique UUID for the page view + * + * @return object web_page context + */ + function getWebPageContext() { + return { + schema: 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0', + data: { + id: getPageViewId() + } + }; + } + + /** + * Creates a context from the window.performance.timing object + * + * @return object PerformanceTiming context + */ + function getPerformanceTimingContext() { + var allowedKeys = [ + 'navigationStart', 'redirectStart', 'redirectEnd', 'fetchStart', 'domainLookupStart', 'domainLookupEnd', 'connectStart', + 'secureConnectionStart', 'connectEnd', 'requestStart', 'responseStart', 'responseEnd', 'unloadEventStart', 'unloadEventEnd', + 'domLoading', 'domInteractive', 'domContentLoadedEventStart', 'domContentLoadedEventEnd', 'domComplete', 'loadEventStart', + 'loadEventEnd', 'msFirstPaint', 'chromeFirstPaint', 'requestEnd', 'proxyStart', 'proxyEnd' + ]; + var performance = windowAlias.performance || windowAlias.mozPerformance || windowAlias.msPerformance || windowAlias.webkitPerformance; + if (performance) { + + // On Safari, the fields we are interested in are on the prototype chain of + // performance.timing so we cannot copy them using lodash.clone + var performanceTiming = {}; + for (var field in performance.timing) { + if (helpers.isValueInArray(field, allowedKeys) && (performance.timing[field] !== null)) { + performanceTiming[field] = performance.timing[field]; + } + } + + // Old Chrome versions add an unwanted requestEnd field + delete performanceTiming.requestEnd; + + // Add the Chrome firstPaintTime to the performance if it exists + if (windowAlias.chrome && windowAlias.chrome.loadTimes && typeof windowAlias.chrome.loadTimes().firstPaintTime === 'number') { + performanceTiming.chromeFirstPaint = Math.round(windowAlias.chrome.loadTimes().firstPaintTime * 1000); + } + + return { + schema: 'iglu:org.w3/PerformanceTiming/jsonschema/1-0-0', + data: performanceTiming + }; + } + } + + /** + * Check that *both* optimizely and optimizely.data exist and return + * optimizely.data.property + * + * @param property optimizely data property + * @param snd optional nested property + */ + function getOptimizelyData(property, snd) { + var data; + if (windowAlias.optimizely && windowAlias.optimizely.data) { + data = windowAlias.optimizely.data[property]; + if (typeof snd !== 'undefined' && data !== undefined) { + data = data[snd] + } + } + return data + } + + /** + * Check that *both* optimizely and optimizely.data exist + * + * @param property optimizely data property + * @param snd optional nested property + */ + function getOptimizelyXData(property, snd) { + var data; + if (windowAlias.optimizely) { + data = windowAlias.optimizely.get(property); + if (typeof snd !== 'undefined' && data !== undefined) { + data = data[snd] + } + } + return data + } + + /** + * Get data for Optimizely "lite" contexts - active experiments on current page + * + * @returns Array content of lite optimizely lite context + */ + function getOptimizelySummary() { + var state = getOptimizelyData('state'); + var experiments = getOptimizelyData('experiments'); + + return map(state && experiments && state.activeExperiments, function (activeExperiment) { + var current = experiments[activeExperiment]; + return { + activeExperimentId: activeExperiment.toString(), + // User can be only in one variation (don't know why is this array) + variation: state.variationIdsMap[activeExperiment][0].toString(), + conditional: current && current.conditional, + manual: current && current.manual, + name: current && current.name + } + }); + } + + /** + * Get data for OptimizelyX contexts - active experiments on current page + * + * @returns Array content of lite optimizely lite context + */ + function getOptimizelyXSummary() { + var state = getOptimizelyXData('state'); + var experiment_ids = state.getActiveExperimentIds(); + var experiments = getOptimizelyXData('data', 'experiments'); + var visitor = getOptimizelyXData('visitor'); + + return map(experiment_ids, function(activeExperiment) { + variation = state.getVariationMap()[activeExperiment]; + variationName = variation.name; + variationId = variation.id; + visitorId = visitor.visitorId; + return { + experimentId: parseInt(activeExperiment), + variationName: variationName, + variation: parseInt(variationId), + visitorId: visitorId + } + }) + } + + /** + * Creates a context from the window['optimizely'].data.experiments object + * + * @return Array Experiment contexts + */ + function getOptimizelyExperimentContexts() { + var experiments = getOptimizelyData('experiments'); + if (experiments) { + var contexts = []; + + for (var key in experiments) { + if (experiments.hasOwnProperty(key)) { + var context = {}; + context.id = key; + var experiment = experiments[key]; + context.code = experiment.code; + context.manual = experiment.manual; + context.conditional = experiment.conditional; + context.name = experiment.name; + context.variationIds = experiment.variation_ids; + + contexts.push({ + schema: 'iglu:com.optimizely/experiment/jsonschema/1-0-0', + data: context + }); + } + } + return contexts; + } + return []; + } + + /** + * Creates a context from the window['optimizely'].data.state object + * + * @return Array State contexts + */ + function getOptimizelyStateContexts() { + var experimentIds = []; + var experiments = getOptimizelyData('experiments'); + if (experiments) { + for (var key in experiments) { + if (experiments.hasOwnProperty(key)) { + experimentIds.push(key); + } + } + } + + var state = getOptimizelyData('state'); + if (state) { + var contexts = []; + var activeExperiments = state.activeExperiments || []; + + for (var i = 0; i < experimentIds.length; i++) { + var experimentId = experimentIds[i]; + var context = {}; + context.experimentId = experimentId; + context.isActive = helpers.isValueInArray(experimentIds[i], activeExperiments); + var variationMap = state.variationMap || {}; + context.variationIndex = variationMap[experimentId]; + var variationNamesMap = state.variationNamesMap || {}; + context.variationName = variationNamesMap[experimentId]; + var variationIdsMap = state.variationIdsMap || {}; + if (variationIdsMap[experimentId] && variationIdsMap[experimentId].length === 1) { + context.variationId = variationIdsMap[experimentId][0]; + } + + contexts.push({ + schema: 'iglu:com.optimizely/state/jsonschema/1-0-0', + data: context + }); + } + return contexts; + } + return []; + } + + /** + * Creates a context from the window['optimizely'].data.variations object + * + * @return Array Variation contexts + */ + function getOptimizelyVariationContexts() { + var variations = getOptimizelyData('variations'); + if (variations) { + var contexts = []; + + for (var key in variations) { + if (variations.hasOwnProperty(key)) { + var context = {}; + context.id = key; + var variation = variations[key]; + context.name = variation.name; + context.code = variation.code; + + contexts.push({ + schema: 'iglu:com.optimizely/variation/jsonschema/1-0-0', + data: context + }); + } + } + return contexts; + } + return []; + } + + /** + * Creates a context from the window['optimizely'].data.visitor object + * + * @return object Visitor context + */ + function getOptimizelyVisitorContext() { + var visitor = getOptimizelyData('visitor'); + if (visitor) { + var context = {}; + context.browser = visitor.browser; + context.browserVersion = visitor.browserVersion; + context.device = visitor.device; + context.deviceType = visitor.deviceType; + context.ip = visitor.ip; + var platform = visitor.platform || {}; + context.platformId = platform.id; + context.platformVersion = platform.version; + var location = visitor.location || {}; + context.locationCity = location.city; + context.locationRegion = location.region; + context.locationCountry = location.country; + context.mobile = visitor.mobile; + context.mobileId = visitor.mobileId; + context.referrer = visitor.referrer; + context.os = visitor.os; + + return { + schema: 'iglu:com.optimizely/visitor/jsonschema/1-0-0', + data: context + }; + } + } + + /** + * Creates a context from the window['optimizely'].data.visitor.audiences object + * + * @return Array VisitorAudience contexts + */ + function getOptimizelyAudienceContexts() { + var audienceIds = getOptimizelyData('visitor', 'audiences'); + if (audienceIds) { + var contexts = []; + + for (var key in audienceIds) { + if (audienceIds.hasOwnProperty(key)) { + var context = { id: key, isMember: audienceIds[key] }; + + contexts.push({ + schema: 'iglu:com.optimizely/visitor_audience/jsonschema/1-0-0', + data: context + }); + } + } + return contexts; + } + return []; + } + + /** + * Creates a context from the window['optimizely'].data.visitor.dimensions object + * + * @return Array VisitorDimension contexts + */ + function getOptimizelyDimensionContexts() { + var dimensionIds = getOptimizelyData('visitor', 'dimensions'); + if (dimensionIds) { + var contexts = []; + + for (var key in dimensionIds) { + if (dimensionIds.hasOwnProperty(key)) { + var context = { id: key, value: dimensionIds[key] }; + + contexts.push({ + schema: 'iglu:com.optimizely/visitor_dimension/jsonschema/1-0-0', + data: context + }); + } + } + return contexts; + } + return []; + } + + + /** + * Creates an Optimizely lite context containing only data required to join + * event to experiment data + * + * @returns Array of custom contexts + */ + function getOptimizelySummaryContexts() { + return map(getOptimizelySummary(), function (experiment) { + return { + schema: 'iglu:com.optimizely.snowplow/optimizely_summary/jsonschema/1-0-0', + data: experiment + }; + }); + } + + /** + * Creates an OptimizelyX context containing only data required to join + * event to experiment data + * + * @returns Array of custom contexts + */ + function getOptimizelyXSummaryContexts() { + return map(getOptimizelyXSummary(), function (experiment) { + return { + schema: 'iglu:com.optimizely.optimizelyx/summary/jsonschema/1-0-0', + data: experiment + }; + }); + } + + /** + * Creates a context from the window['augur'] object + * + * @return object The IdentityLite context + */ + function getAugurIdentityLiteContext() { + var augur = windowAlias.augur; + if (augur) { + var context = { consumer: {}, device: {} }; + var consumer = augur.consumer || {}; + context.consumer.UUID = consumer.UID; + var device = augur.device || {}; + context.device.ID = device.ID; + context.device.isBot = device.isBot; + context.device.isProxied = device.isProxied; + context.device.isTor = device.isTor; + var fingerprint = device.fingerprint || {}; + context.device.isIncognito = fingerprint.browserHasIncognitoEnabled; + + return { + schema: 'iglu:io.augur.snowplow/identity_lite/jsonschema/1-0-0', + data: context + }; + } + } + + /** + * Creates a context from the window['_hawk'] object + * + * @return object The Parrable context + */ + function getParrableContext() { + var parrable = window['_hawk']; + if (parrable) { + var context = { encryptedId: null, optout: null }; + context['encryptedId'] = parrable.browserid; + var regex = new RegExp('(?:^|;)\\s?' + "_parrable_hawk_optout".replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1') + '=(.*?)(?:;|$)', 'i'), match = document.cookie.match(regex); + context['optout'] = (match && decodeURIComponent(match[1])) ? match && decodeURIComponent(match[1]) : "false"; + return { + schema: 'iglu:com.parrable/encrypted_payload/jsonschema/1-0-0', + data: context + }; + } + } + + /** + * Expires current session and starts a new session. + */ + function newSession() { + // If cookies are enabled, base visit count and session ID on the cookies + var nowTs = Math.round(new Date().getTime() / 1000), + ses = getSnowplowCookieValue('ses'), + id = loadDomainUserIdCookie(), + cookiesDisabled = id[0], + _domainUserId = id[1], // We could use the global (domainUserId) but this is better etiquette + createTs = id[2], + visitCount = id[3], + currentVisitTs = id[4], + lastVisitTs = id[5], + sessionIdFromCookie = id[6]; + + // When cookies are enabled + if (cookiesDisabled === '0') { + memorizedSessionId = sessionIdFromCookie; + + // When cookie/local storage is enabled - make a new session + if (configStateStorageStrategy != 'none') { + // New session (aka new visit) + visitCount++; + // Update the last visit timestamp + lastVisitTs = currentVisitTs; + // Regenerate the session ID + memorizedSessionId = uuid.v4(); + } + + memorizedVisitCount = visitCount; + + // Create a new session cookie + setSessionCookie() + + } else { + memorizedSessionId = uuid.v4(); + memorizedVisitCount++; + } + + // Update cookies + if (configStateStorageStrategy != 'none') { + setDomainUserIdCookie(_domainUserId, createTs, memorizedVisitCount, nowTs, + lastVisitTs, memorizedSessionId); + setSessionCookie(); + } + + lastEventTime = new Date().getTime(); + } + + /** + * Attempts to create a context using the geolocation API and add it to commonContexts + */ + function enableGeolocationContext() { + if (!geolocationContextAdded && navigatorAlias.geolocation && navigatorAlias.geolocation.getCurrentPosition) { + geolocationContextAdded = true; + navigatorAlias.geolocation.getCurrentPosition(function (position) { + var coords = position.coords; + var geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + latitude: coords.latitude, + longitude: coords.longitude, + latitudeLongitudeAccuracy: coords.accuracy, + altitude: coords.altitude, + altitudeAccuracy: coords.altitudeAccuracy, + bearing: coords.heading, + speed: coords.speed, + timestamp: Math.round(position.timestamp) + } + }; + commonContexts.push(geolocationContext); + }); + } + } + + /** + * Creates a context containing the values of the cookies set by GA + * + * @return object GA cookies context + */ + function getGaCookiesContext() { + var gaCookieData = {}; + forEach(['__utma', '__utmb', '__utmc', '__utmv', '__utmz', '_ga'], function (cookieType) { + var value = cookie.cookie(cookieType); + if (value) { + gaCookieData[cookieType] = value; + } + }); + return { + schema: 'iglu:com.google.analytics/cookies/jsonschema/1-0-0', + data: gaCookieData + }; + } + + /** + * Combine an array of unchanging contexts with the result of a context-creating function + * + * @param staticContexts Array of custom contexts + * @param contextCallback Function returning an array of contexts + */ + function finalizeContexts(staticContexts, contextCallback) { + return (staticContexts || []).concat(contextCallback ? contextCallback() : []); + } + + /** + * Log the page view / visit + * + * @param customTitle string The user-defined page title to attach to this page view + * @param context object Custom context relating to the event + * @param contextCallback Function returning an array of contexts + * @param tstamp number + */ + function logPageView(customTitle, context, contextCallback, tstamp) { + + refreshUrl(); + if (pageViewSent) { // Do not reset pageViewId if previous events were not page_view + resetPageView(); + } + pageViewSent = true; + + // So we know what document.title was at the time of trackPageView + lastDocumentTitle = documentAlias.title; + lastConfigTitle = customTitle; + + // Fixup page title + var pageTitle = helpers.fixupTitle(lastConfigTitle || lastDocumentTitle); + + // Log page view + core.trackPageView( + purify(configCustomUrl || locationHrefAlias), + pageTitle, + purify(customReferrer || configReferrerUrl), + addCommonContexts(finalizeContexts(context, contextCallback)), + tstamp); + + // Send ping (to log that user has stayed on page) + var now = new Date(); + + if (activityTrackingEnabled && !activityTrackingInstalled) { + activityTrackingInstalled = true; + + // Add mousewheel event handler, detect passive event listeners for performance + var detectPassiveEvents = { + update: function update() { + if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { + var passive = false; + var options = Object.defineProperty({}, 'passive', { + get: function get() { + passive = true; + } + }); + // note: have to set and remove a no-op listener instead of null + // (which was used previously), becasue Edge v15 throws an error + // when providing a null callback. + // https://github.com/rafrex/detect-passive-events/pull/3 + var noop = function noop() { + }; + window.addEventListener('testPassiveEventSupport', noop, options); + window.removeEventListener('testPassiveEventSupport', noop, options); + detectPassiveEvents.hasSupport = passive; + } + } + }; + detectPassiveEvents.update(); + + // Detect available wheel event + var wheelEvent = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel" + document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel" + "DOMMouseScroll"; // let's assume that remaining browsers are older Firefox + + if (Object.prototype.hasOwnProperty.call(detectPassiveEvents, 'hasSupport')) { + helpers.addEventListener(documentAlias, wheelEvent, activityHandler, {passive: true}); + } else { + helpers.addEventListener(documentAlias, wheelEvent, activityHandler); + } + + // Capture our initial scroll points + resetMaxScrolls(); + + // Add event handlers; cross-browser compatibility here varies significantly + // @see http://quirksmode.org/dom/events + helpers.addEventListener(documentAlias, 'click', activityHandler); + helpers.addEventListener(documentAlias, 'mouseup', activityHandler); + helpers.addEventListener(documentAlias, 'mousedown', activityHandler); + helpers.addEventListener(documentAlias, 'mousemove', activityHandler); + helpers.addEventListener(windowAlias, 'scroll', scrollHandler); // Will updateMaxScrolls() for us + helpers.addEventListener(documentAlias, 'keypress', activityHandler); + helpers.addEventListener(documentAlias, 'keydown', activityHandler); + helpers.addEventListener(documentAlias, 'keyup', activityHandler); + helpers.addEventListener(windowAlias, 'resize', activityHandler); + helpers.addEventListener(windowAlias, 'focus', activityHandler); + helpers.addEventListener(windowAlias, 'blur', activityHandler); + + // Periodic check for activity. + lastActivityTime = now.getTime(); + clearInterval(pagePingInterval); + pagePingInterval = setInterval(function heartBeat() { + var now = new Date(); + + // There was activity during the heart beat period; + // on average, this is going to overstate the visitDuration by configHeartBeatTimer/2 + if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) { + // Send ping if minimum visit time has elapsed + if (configMinimumVisitTime < now.getTime()) { + logPagePing(finalizeContexts(context, contextCallback)); // Grab the min/max globals + } + } + }, configHeartBeatTimer); + } + } + + /** + * Log that a user is still viewing a given page + * by sending a page ping. + * Not part of the public API - only called from + * logPageView() above. + * + * @param context object Custom context relating to the event + */ + function logPagePing(context) { + refreshUrl(); + var newDocumentTitle = documentAlias.title; + if (newDocumentTitle !== lastDocumentTitle) { + lastDocumentTitle = newDocumentTitle; + lastConfigTitle = null; + } + core.trackPagePing( + purify(configCustomUrl || locationHrefAlias), + helpers.fixupTitle(lastConfigTitle || lastDocumentTitle), + purify(customReferrer || configReferrerUrl), + cleanOffset(minXOffset), + cleanOffset(maxXOffset), + cleanOffset(minYOffset), + cleanOffset(maxYOffset), + addCommonContexts(context)); + resetMaxScrolls(); + } + + /** + * Log ecommerce transaction metadata + * + * @param string orderId + * @param string affiliation + * @param string total + * @param string tax + * @param string shipping + * @param string city + * @param string state + * @param string country + * @param string currency The currency the total/tax/shipping are expressed in + * @param object context Custom context relating to the event + * @param tstamp number or Timestamp object + */ + function logTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, context, tstamp) { + core.trackEcommerceTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, addCommonContexts(context), tstamp); + } + + /** + * Log ecommerce transaction item + * + * @param string orderId + * @param string sku + * @param string name + * @param string category + * @param string price + * @param string quantity + * @param string currency The currency the price is expressed in + * @param object context Custom context relating to the event + */ + function logTransactionItem(orderId, sku, name, category, price, quantity, currency, context, tstamp) { + core.trackEcommerceTransactionItem(orderId, sku, name, category, price, quantity, currency, addCommonContexts(context), tstamp); + } + + /** + * Construct a browser prefix + * + * E.g: (moz, hidden) -> mozHidden + */ + function prefixPropertyName(prefix, propertyName) { + + if (prefix !== '') { + return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1); + } + + return propertyName; + } + + /** + * Check for pre-rendered web pages, and log the page view/link + * according to the configuration and/or visibility + * + * @see http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/PageVisibility/Overview.html + */ + function trackCallback(callback) { + var isPreRendered, + i, + // Chrome 13, IE10, FF10 + prefixes = ['', 'webkit', 'ms', 'moz'], + prefix; + + // If configPrerendered == true - we'll never set `isPreRendered` to true and fire immediately, + // otherwise we need to check if this is just prerendered + if (!configCountPreRendered) { // true by default + + for (i = 0; i < prefixes.length; i++) { + prefix = prefixes[i]; + + // does this browser support the page visibility API? (drop this check along with IE9 and iOS6) + if (documentAlias[prefixPropertyName(prefix, 'hidden')]) { + // if pre-rendered, then defer callback until page visibility changes + if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') { + isPreRendered = true; + } + break; + } else if (documentAlias[prefixPropertyName(prefix, 'hidden')] === false) { break } + } + } + + // Implies configCountPreRendered = false + if (isPreRendered) { + // note: the event name doesn't follow the same naming convention as vendor properties + helpers.addEventListener(documentAlias, prefix + 'visibilitychange', function ready() { + documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false); + callback(); + }); + return; + } + + // configCountPreRendered === true || isPreRendered === false + callback(); + } + + /** + * Update the returned methods (public facing methods) + */ + function updateReturnMethods() { + if (debug) { + returnMethods = apiMethods; + } else { + returnMethods = safeMethods; + } + } + + /************************************************************ + * Constructor + ************************************************************/ + + /* + * Initialize tracker + */ + updateDomainHash(); + + initializeIdsAndCookies(); + + if (argmap.crossDomainLinker) { + decorateLinks(argmap.crossDomainLinker); + } + + /************************************************************ + * Public data and methods + ************************************************************/ + + /** + * Get the domain session index also known as current memorized visit count. + * + * @return int Domain session index + */ + apiMethods.getDomainSessionIndex = function() { + return memorizedVisitCount; + }; + + /** + * Get the page view ID as generated or provided by mutSnowplowState.pageViewId. + * + * @return string Page view ID + */ + apiMethods.getPageViewId = function () { + return getPageViewId(); + }; + + /** + * Expires current session and starts a new session. + */ + apiMethods.newSession = newSession; + + /** + * Get the cookie name as cookieNamePrefix + basename + . + domain. + * + * @return string Cookie name + */ + apiMethods.getCookieName = function(basename) { + return getSnowplowCookieName(basename); + }; + + /** + * Get the current user ID (as set previously + * with setUserId()). + * + * @return string Business-defined user ID + */ + apiMethods.getUserId = function () { + return businessUserId; + }; + + /** + * Get visitor ID (from first party cookie) + * + * @return string Visitor ID in hexits (or null, if not yet known) + */ + apiMethods.getDomainUserId = function () { + return (loadDomainUserIdCookie())[1]; + }; + + /** + * Get the visitor information (from first party cookie) + * + * @return array + */ + apiMethods.getDomainUserInfo = function () { + return loadDomainUserIdCookie(); + }; + + /** + * Get the user fingerprint + * + * @return string The user fingerprint + */ + apiMethods.getUserFingerprint = function () { + return userFingerprint; + }; + + /** + * Specify the app ID + * + * @param int|string appId + */ + apiMethods.setAppId = function (appId) { + helpers.warn('setAppId is deprecated. Instead add an "appId" field to the argmap argument of newTracker.'); + core.setAppId(appId); + }; + + /** + * Override referrer + * + * @param string url + */ + apiMethods.setReferrerUrl = function (url) { + customReferrer = url; + }; + + /** + * Override url + * + * @param string url + */ + apiMethods.setCustomUrl = function (url) { + refreshUrl(); + configCustomUrl = resolveRelativeReference(locationHrefAlias, url); + }; + + /** + * Override document.title + * + * @param string title + */ + apiMethods.setDocumentTitle = function (title) { + // So we know what document.title was at the time of trackPageView + lastDocumentTitle = documentAlias.title; + lastConfigTitle = title; + }; + + /** + * Strip hash tag (or anchor) from URL + * + * @param bool enableFilter + */ + apiMethods.discardHashTag = function (enableFilter) { + configDiscardHashTag = enableFilter; + }; + + /** + * Set first-party cookie name prefix + * + * @param string cookieNamePrefix + */ + apiMethods.setCookieNamePrefix = function (cookieNamePrefix) { + helpers.warn('setCookieNamePrefix is deprecated. Instead add a "cookieName" field to the argmap argument of newTracker.'); + configCookieNamePrefix = cookieNamePrefix; + }; + + /** + * Set first-party cookie domain + * + * @param string domain + */ + apiMethods.setCookieDomain = function (domain) { + helpers.warn('setCookieDomain is deprecated. Instead add a "cookieDomain" field to the argmap argument of newTracker.'); + configCookieDomain = helpers.fixupDomain(domain); + updateDomainHash(); + }; + + /** + * Set first-party cookie path + * + * @param string domain + */ + apiMethods.setCookiePath = function (path) { + configCookiePath = path; + updateDomainHash(); + }; + + /** + * Set visitor cookie timeout (in seconds) + * + * @param int timeout + */ + apiMethods.setVisitorCookieTimeout = function (timeout) { + configVisitorCookieTimeout = timeout; + }; + + /** + * Set session cookie timeout (in seconds) + * + * @param int timeout + */ + apiMethods.setSessionCookieTimeout = function (timeout) { + helpers.warn('setSessionCookieTimeout is deprecated. Instead add a "sessionCookieTimeout" field to the argmap argument of newTracker.') + configSessionCookieTimeout = timeout; + }; + + /** + * @param number seed The seed used for MurmurHash3 + */ + apiMethods.setUserFingerprintSeed = function(seed) { + helpers.warn('setUserFingerprintSeed is deprecated. Instead add a "userFingerprintSeed" field to the argmap argument of newTracker.'); + configUserFingerprintHashSeed = seed; + userFingerprint = detectors.detectSignature(configUserFingerprintHashSeed); + }; + + /** + * Enable/disable user fingerprinting. User fingerprinting is enabled by default. + * @param bool enable If false, turn off user fingerprinting + */ + apiMethods.enableUserFingerprint = function(enable) { + helpers.warn('enableUserFingerprintSeed is deprecated. Instead add a "userFingerprint" field to the argmap argument of newTracker.'); + if (!enable) { + userFingerprint = ''; + } + }; + + /** + * Prevent tracking if user's browser has Do Not Track feature enabled, + * where tracking is: + * 1) Sending events to a collector + * 2) Setting first-party cookies + * @param bool enable If true and Do Not Track feature enabled, don't track. + */ + apiMethods.respectDoNotTrack = function (enable) { + helpers.warn('This usage of respectDoNotTrack is deprecated. Instead add a "respectDoNotTrack" field to the argmap argument of newTracker.'); + var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack; + + configDoNotTrack = enable && (dnt === 'yes' || dnt === '1'); + }; + + /** + * Enable querystring decoration for links pasing a filter + * + * @param function crossDomainLinker Function used to determine which links to decorate + */ + apiMethods.crossDomainLinker = function (crossDomainLinkerCriterion) { + decorateLinks(crossDomainLinkerCriterion); + }; + + /** + * Install link tracker + * + * The default behaviour is to use actual click events. However, some browsers + * (e.g., Firefox, Opera, and Konqueror) don't generate click events for the middle mouse button. + * + * To capture more "clicks", the pseudo click-handler uses mousedown + mouseup events. + * This is not industry standard and is vulnerable to false positives (e.g., drag events). + * + * There is a Safari/Chrome/Webkit bug that prevents tracking requests from being sent + * by either click handler. The workaround is to set a target attribute (which can't + * be "_self", "_top", or "_parent"). + * + * @see https://bugs.webkit.org/show_bug.cgi?id=54783 + * + * @param object criterion Criterion by which it will be decided whether a link will be tracked + * @param bool pseudoClicks If true, use pseudo click-handler (mousedown+mouseup) + * @param bool trackContent Whether to track the innerHTML of the link element + * @param array context Context for all link click events + */ + apiMethods.enableLinkClickTracking = function (criterion, pseudoClicks, trackContent, context) { + if (mutSnowplowState.hasLoaded) { + // the load event has already fired, add the click listeners now + linkTrackingManager.configureLinkClickTracking(criterion, pseudoClicks, trackContent, context); + linkTrackingManager.addClickListeners(); + } else { + // defer until page has loaded + mutSnowplowState.registeredOnLoadHandlers.push(function () { + linkTrackingManager.configureLinkClickTracking(criterion, pseudoClicks, trackContent, context); + linkTrackingManager.addClickListeners(); + }); + } + }; + + /** + * Add click event listeners to links which have been added to the page since the + * last time enableLinkClickTracking or refreshLinkClickTracking was used + */ + apiMethods.refreshLinkClickTracking = function () { + if (mutSnowplowState.hasLoaded) { + linkTrackingManager.addClickListeners(); + } else { + mutSnowplowState.registeredOnLoadHandlers.push(function () { + linkTrackingManager.addClickListeners(); + }); + } + }; + + /** + * Enables page activity tracking (sends page + * pings to the Collector regularly). + * + * @param int minimumVisitLength Seconds to wait before sending first page ping + * @param int heartBeatDelay Seconds to wait between pings + */ + apiMethods.enableActivityTracking = function (minimumVisitLength, heartBeatDelay) { + if (minimumVisitLength === parseInt(minimumVisitLength, 10) && + heartBeatDelay === parseInt(heartBeatDelay, 10)) { + activityTrackingEnabled = true; + configMinimumVisitTime = new Date().getTime() + minimumVisitLength * 1000; + configHeartBeatTimer = heartBeatDelay * 1000; + } else { + helpers.warn("Activity tracking not enabled, please provide integer values " + + "for minimumVisitLength and heartBeatDelay.") + } + }; + + /** + * Triggers the activityHandler manually to allow external user defined + * activity. i.e. While watching a video + */ + apiMethods.updatePageActivity = function () { + activityHandler(); + }; + + /** + * Enables automatic form tracking. + * An event will be fired when a form field is changed or a form submitted. + * This can be called multiple times: only forms not already tracked will be tracked. + * + * @param object config Configuration object determining which forms and fields to track. + * Has two properties: "forms" and "fields" + * @param array context Context for all form tracking events + */ + apiMethods.enableFormTracking = function (config, context) { + if (mutSnowplowState.hasLoaded) { + formTrackingManager.configureFormTracking(config); + formTrackingManager.addFormListeners(context); + } else { + mutSnowplowState.registeredOnLoadHandlers.push(function () { + formTrackingManager.configureFormTracking(config); + formTrackingManager.addFormListeners(context); + }); + } + }; + + /** + * Frame buster + */ + apiMethods.killFrame = function () { + if (windowAlias.location !== windowAlias.top.location) { + windowAlias.top.location = windowAlias.location; + } + }; + + /** + * Redirect if browsing offline (aka file: buster) + * + * @param string url Redirect to this URL + */ + apiMethods.redirectFile = function (url) { + if (windowAlias.location.protocol === 'file:') { + windowAlias.location = url; + } + }; + + /** + * Sets the opt out cookie. + * + * @param string name of the opt out cookie + */ + apiMethods.setOptOutCookie = function (name) { + configOptOutCookie = name; + }; + + /** + * Count sites in pre-rendered state + * + * @param bool enable If true, track when in pre-rendered state + */ + apiMethods.setCountPreRendered = function (enable) { + configCountPreRendered = enable; + }; + + /** + * Set the business-defined user ID for this user. + * + * @param string userId The business-defined user ID + */ + apiMethods.setUserId = function(userId) { + businessUserId = userId; + }; + + /** + * Alias for setUserId. + * + * @param string userId The business-defined user ID + */ + apiMethods.identifyUser = function(userId) { + setUserId(userId); + }; + + /** + * Set the business-defined user ID for this user using the location querystring. + * + * @param string queryName Name of a querystring name-value pair + */ + apiMethods.setUserIdFromLocation = function(querystringField) { + refreshUrl(); + businessUserId = helpers.fromQuerystring(querystringField, locationHrefAlias); + }; + + /** + * Set the business-defined user ID for this user using the referrer querystring. + * + * @param string queryName Name of a querystring name-value pair + */ + apiMethods.setUserIdFromReferrer = function(querystringField) { + refreshUrl(); + businessUserId = helpers.fromQuerystring(querystringField, configReferrerUrl); + }; + + /** + * Set the business-defined user ID for this user to the value of a cookie. + * + * @param string cookieName Name of the cookie whose value will be assigned to businessUserId + */ + apiMethods.setUserIdFromCookie = function(cookieName) { + businessUserId = cookie.cookie(cookieName); + }; + + /** + * Configure this tracker to log to a CloudFront collector. + * + * @param string distSubdomain The subdomain on your CloudFront collector's distribution + */ + apiMethods.setCollectorCf = function (distSubdomain) { + configCollectorUrl = collectorUrlFromCfDist(distSubdomain); + }; + + /** + * + * Specify the Snowplow collector URL. No need to include HTTP + * or HTTPS - we will add this. + * + * @param string rawUrl The collector URL minus protocol and /i + */ + apiMethods.setCollectorUrl = function (rawUrl) { + configCollectorUrl = asCollectorUrl(rawUrl); + }; + + /** + * Specify the platform + * + * @param string platform Overrides the default tracking platform + */ + apiMethods.setPlatform = function(platform) { + helpers.warn('setPlatform is deprecated. Instead add a "platform" field to the argmap argument of newTracker.'); + core.setPlatform(platform); + }; + + /** + * + * Enable Base64 encoding for self-describing event payload + * + * @param bool enabled A boolean value indicating if the Base64 encoding for self-describing events should be enabled or not + */ + apiMethods.encodeBase64 = function (enabled) { + helpers.warn('This usage of encodeBase64 is deprecated. Instead add an "encodeBase64" field to the argmap argument of newTracker.'); + core.setBase64Encoding(enabled); + }; + + /** + * Send all events in the outQueue + * Use only when sending POSTs with a bufferSize of at least 2 + */ + apiMethods.flushBuffer = function () { + outQueueManager.executeQueue(); + }; + + /** + * Add the geolocation context to all events + */ + apiMethods.enableGeolocationContext = enableGeolocationContext, + + /** + * Log visit to this page + * + * @param string customTitle + * @param object Custom context relating to the event + * @param object contextCallback Function returning an array of contexts + * @param tstamp number or Timestamp object + */ + apiMethods.trackPageView = function (customTitle, context, contextCallback, tstamp) { + trackCallback(function () { + logPageView(customTitle, context, contextCallback, tstamp); + }); + }; + + /** + * Track a structured event happening on this page. + * + * Replaces trackEvent, making clear that the type + * of event being tracked is a structured one. + * + * @param string category The name you supply for the group of objects you want to track + * @param string action A string that is uniquely paired with each category, and commonly used to define the type of user interaction for the web object + * @param string label (optional) An optional string to provide additional dimensions to the event data + * @param string property (optional) Describes the object or the action performed on it, e.g. quantity of item added to basket + * @param int|float|string value (optional) An integer that you can use to provide numerical data about the user event + * @param object Custom context relating to the event + * @param tstamp number or Timestamp object + */ + apiMethods.trackStructEvent = function (category, action, label, property, value, context, tstamp) { + trackCallback(function () { + core.trackStructEvent(category, action, label, property, value, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track a self-describing event (previously unstructured event) happening on this page. + * + * @param object eventJson Contains the properties and schema location for the event + * @param object context Custom context relating to the event + * @param tstamp number or Timestamp object + */ + apiMethods.trackSelfDescribingEvent = function (eventJson, context, tstamp) { + trackCallback(function () { + core.trackSelfDescribingEvent(eventJson, addCommonContexts(context), tstamp); + }); + }; + + /** + * Alias for `trackSelfDescribingEvent`, left for compatibility + */ + apiMethods.trackUnstructEvent = function (eventJson, context, tstamp) { + trackCallback(function () { + core.trackSelfDescribingEvent(eventJson, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track an ecommerce transaction + * + * @param string orderId Required. Internal unique order id number for this transaction. + * @param string affiliation Optional. Partner or store affiliation. + * @param string total Required. Total amount of the transaction. + * @param string tax Optional. Tax amount of the transaction. + * @param string shipping Optional. Shipping charge for the transaction. + * @param string city Optional. City to associate with transaction. + * @param string state Optional. State to associate with transaction. + * @param string country Optional. Country to associate with transaction. + * @param string currency Optional. Currency to associate with this transaction. + * @param object context Optional. Context relating to the event. + * @param tstamp number or Timestamp object + */ + apiMethods.addTrans = function(orderId, affiliation, total, tax, shipping, city, state, country, currency, context, tstamp) { + ecommerceTransaction.transaction = { + orderId: orderId, + affiliation: affiliation, + total: total, + tax: tax, + shipping: shipping, + city: city, + state: state, + country: country, + currency: currency, + context: context, + tstamp: tstamp + }; + }; + + /** + * Track an ecommerce transaction item + * + * @param string orderId Required Order ID of the transaction to associate with item. + * @param string sku Required. Item's SKU code. + * @param string name Optional. Product name. + * @param string category Optional. Product category. + * @param string price Required. Product price. + * @param string quantity Required. Purchase quantity. + * @param string currency Optional. Product price currency. + * @param object context Optional. Context relating to the event. + * @param tstamp number or Timestamp object + */ + apiMethods.addItem = function(orderId, sku, name, category, price, quantity, currency, context, tstamp) { + ecommerceTransaction.items.push({ + orderId: orderId, + sku: sku, + name: name, + category: category, + price: price, + quantity: quantity, + currency: currency, + context: context, + tstamp: tstamp + }); + }; + + /** + * Commit the ecommerce transaction + * + * This call will send the data specified with addTrans, + * addItem methods to the tracking server. + */ + apiMethods.trackTrans = function() { + trackCallback(function () { + + logTransaction( + ecommerceTransaction.transaction.orderId, + ecommerceTransaction.transaction.affiliation, + ecommerceTransaction.transaction.total, + ecommerceTransaction.transaction.tax, + ecommerceTransaction.transaction.shipping, + ecommerceTransaction.transaction.city, + ecommerceTransaction.transaction.state, + ecommerceTransaction.transaction.country, + ecommerceTransaction.transaction.currency, + ecommerceTransaction.transaction.context, + ecommerceTransaction.transaction.tstamp + ); + for (var i = 0; i < ecommerceTransaction.items.length; i++) { + var item = ecommerceTransaction.items[i]; + logTransactionItem( + item.orderId, + item.sku, + item.name, + item.category, + item.price, + item.quantity, + item.currency, + item.context, + item.tstamp + ); + } + + ecommerceTransaction = ecommerceTransactionTemplate(); + }); + }; + + /** + * Manually log a click from your own code + * + * @param string elementId + * @param array elementClasses + * @param string elementTarget + * @param string targetUrl + * @param string elementContent innerHTML of the element + * @param object Custom context relating to the event + * @param tstamp number or Timestamp object + */ + // TODO: break this into trackLink(destUrl) and trackDownload(destUrl) + apiMethods.trackLinkClick = function(targetUrl, elementId, elementClasses, elementTarget, elementContent, context, tstamp) { + trackCallback(function () { + core.trackLinkClick(targetUrl, elementId, elementClasses, elementTarget, elementContent, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track an ad being served + * + * @param string impressionId Identifier for a particular ad impression + * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param number cost Cost + * @param string bannerId Identifier for the ad banner displayed + * @param string zoneId Identifier for the ad zone + * @param string advertiserId Identifier for the advertiser + * @param string campaignId Identifier for the campaign which the banner belongs to + * @param object Custom context relating to the event + * @param tstamp number or Timestamp object + */ + apiMethods.trackAdImpression = function(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, context, tstamp) { + trackCallback(function () { + core.trackAdImpression(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track an ad being clicked + * + * @param string clickId Identifier for the ad click + * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param number cost Cost + * @param string targetUrl (required) The link's target URL + * @param string bannerId Identifier for the ad banner displayed + * @param string zoneId Identifier for the ad zone + * @param string impressionId Identifier for a particular ad impression + * @param string advertiserId Identifier for the advertiser + * @param string campaignId Identifier for the campaign which the banner belongs to + * @param object Custom context relating to the event + * @param tstamp number or Timestamp object + */ + apiMethods.trackAdClick = function(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, context, tstamp) { + trackCallback(function () { + core.trackAdClick(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track an ad conversion event + * + * @param string conversionId Identifier for the ad conversion event + * @param number cost Cost + * @param string category The name you supply for the group of objects you want to track + * @param string action A string that is uniquely paired with each category + * @param string property Describes the object of the conversion or the action performed on it + * @param number initialValue Revenue attributable to the conversion at time of conversion + * @param string advertiserId Identifier for the advertiser + * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param string campaignId Identifier for the campaign which the banner belongs to + * @param object Custom context relating to the event + * @param tstamp number or Timestamp object + */ + apiMethods.trackAdConversion = function(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, context, tstamp) { + trackCallback(function () { + core.trackAdConversion(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track a social interaction event + * + * @param string action (required) Social action performed + * @param string network (required) Social network + * @param string target Object of the social action e.g. the video liked, the tweet retweeted + * @param object Custom context relating to the event + * @param tstamp number or Timestamp object + */ + apiMethods.trackSocialInteraction = function(action, network, target, context, tstamp) { + trackCallback(function () { + core.trackSocialInteraction(action, network, target, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track an add-to-cart event + * + * @param string sku Required. Item's SKU code. + * @param string name Optional. Product name. + * @param string category Optional. Product category. + * @param string unitPrice Optional. Product price. + * @param string quantity Required. Quantity added. + * @param string currency Optional. Product price currency. + * @param array context Optional. Context relating to the event. + * @param tstamp number or Timestamp object + */ + apiMethods.trackAddToCart = function(sku, name, category, unitPrice, quantity, currency, context, tstamp) { + trackCallback(function () { + core.trackAddToCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track a remove-from-cart event + * + * @param string sku Required. Item's SKU code. + * @param string name Optional. Product name. + * @param string category Optional. Product category. + * @param string unitPrice Optional. Product price. + * @param string quantity Required. Quantity removed. + * @param string currency Optional. Product price currency. + * @param array context Optional. Context relating to the event. + * @param tstamp Opinal number or Timestamp object + */ + apiMethods.trackRemoveFromCart = function(sku, name, category, unitPrice, quantity, currency, context, tstamp) { + trackCallback(function () { + core.trackRemoveFromCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track an internal search event + * + * @param array terms Search terms + * @param object filters Search filters + * @param number totalResults Number of results + * @param number pageResults Number of results displayed on page + * @param array context Optional. Context relating to the event. + * @param tstamp Opinal number or Timestamp object + */ + apiMethods.trackSiteSearch = function(terms, filters, totalResults, pageResults, context, tstamp) { + trackCallback(function () { + core.trackSiteSearch(terms, filters, totalResults, pageResults, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track a timing event (such as the time taken for a resource to load) + * + * @param string category Required. + * @param string variable Required. + * @param number timing Required. + * @param string label Optional. + * @param array context Optional. Context relating to the event. + * @param tstamp Opinal number or Timestamp object + */ + apiMethods.trackTiming = function (category, variable, timing, label, context, tstamp) { + trackCallback(function () { + core.trackSelfDescribingEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0', + data: { + category: category, + variable: variable, + timing: timing, + label: label + } + }, addCommonContexts(context), tstamp) + }); + }; + + /** + * Track a consent withdrawn action + * + * @param {boolean} all - Indicates user withdraws all consent regardless of context documents. + * @param {string} [id] - Number associated with document. + * @param {string} [version] - Document version number. + * @param {string} [name] - Document name. + * @param {string} [description] - Document description. + * @param {array} [context] - Context relating to the event. + * @param {number|Timestamp} [tstamp] - Number or Timestamp object. + */ + apiMethods.trackConsentWithdrawn = function (all, id, version, name, description, context, tstamp) { + trackCallback(function () { + core.trackConsentWithdrawn(all, id, version, name, description, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track a consent granted action + * + * @param {string} id - ID number associated with document. + * @param {string} version - Document version number. + * @param {string} [name] - Document name. + * @param {string} [description] - Document description. + * @param {string} [expiry] - Date-time when consent document(s) expire. + * @param {array} [context] - Context containing consent documents. + * @param {Timestamp|number} [tstamp] - number or Timestamp object. + */ + apiMethods.trackConsentGranted = function (id, version, name, description, expiry, context, tstamp) { + trackCallback(function () { + core.trackConsentGranted(id, version, name, description, expiry, addCommonContexts(context), tstamp); + }); + }; + + /** + * Track a GA Enhanced Ecommerce Action with all stored + * Enhanced Ecommerce contexts + * + * @param string action + * @param array context Optional. Context relating to the event. + * @param tstamp Opinal number or Timestamp object + */ + apiMethods.trackEnhancedEcommerceAction = function (action, context, tstamp) { + var combinedEnhancedEcommerceContexts = enhancedEcommerceContexts.concat(context || []); + enhancedEcommerceContexts.length = 0; + + trackCallback(function () { + core.trackSelfDescribingEvent({ + schema: 'iglu:com.google.analytics.enhanced-ecommerce/action/jsonschema/1-0-0', + data: { + action: action + } + }, addCommonContexts(combinedEnhancedEcommerceContexts), tstamp); + }); + }; + + /** + * Adds a GA Enhanced Ecommerce Action Context + * + * @param string id + * @param string affiliation + * @param number revenue + * @param number tax + * @param number shipping + * @param string coupon + * @param string list + * @param integer step + * @param string option + * @param string currency + */ + apiMethods.addEnhancedEcommerceActionContext = function (id, affiliation, revenue, tax, shipping, coupon, list, step, option, currency) { + enhancedEcommerceContexts.push({ + schema: 'iglu:com.google.analytics.enhanced-ecommerce/actionFieldObject/jsonschema/1-0-0', + data: { + id: id, + affiliation: affiliation, + revenue: helpers.parseFloat(revenue), + tax: helpers.parseFloat(tax), + shipping: helpers.parseFloat(shipping), + coupon: coupon, + list: list, + step: helpers.parseInt(step), + option: option, + currency: currency + } + }); + }; + + /** + * Adds a GA Enhanced Ecommerce Impression Context + * + * @param string id + * @param string name + * @param string list + * @param string brand + * @param string category + * @param string variant + * @param integer position + * @param number price + * @param string currency + */ + apiMethods.addEnhancedEcommerceImpressionContext = function (id, name, list, brand, category, variant, position, price, currency) { + enhancedEcommerceContexts.push({ + schema: 'iglu:com.google.analytics.enhanced-ecommerce/impressionFieldObject/jsonschema/1-0-0', + data: { + id: id, + name: name, + list: list, + brand: brand, + category: category, + variant: variant, + position: helpers.parseInt(position), + price: helpers.parseFloat(price), + currency: currency + } + }); + }; + + /** + * Adds a GA Enhanced Ecommerce Product Context + * + * @param string id + * @param string name + * @param string list + * @param string brand + * @param string category + * @param string variant + * @param number price + * @param integer quantity + * @param string coupon + * @param integer position + * @param string currency + */ + apiMethods.addEnhancedEcommerceProductContext = function (id, name, list, brand, category, variant, price, quantity, coupon, position, currency) { + enhancedEcommerceContexts.push({ + schema: 'iglu:com.google.analytics.enhanced-ecommerce/productFieldObject/jsonschema/1-0-0', + data: { + id: id, + name: name, + list: list, + brand: brand, + category: category, + variant: variant, + price: helpers.parseFloat(price), + quantity: helpers.parseInt(quantity), + coupon: coupon, + position: helpers.parseInt(position), + currency: currency + } + }); + }; + + /** + * Adds a GA Enhanced Ecommerce Promo Context + * + * @param string id + * @param string name + * @param string creative + * @param string position + * @param string currency + */ + apiMethods.addEnhancedEcommercePromoContext = function (id, name, creative, position, currency) { + enhancedEcommerceContexts.push({ + schema: 'iglu:com.google.analytics.enhanced-ecommerce/promoFieldObject/jsonschema/1-0-0', + data: { + id: id, + name: name, + creative: creative, + position: position, + currency: currency + } + }); + }; + + /** + * All provided contexts will be sent with every event + * + * @param contexts Array + */ + apiMethods.addGlobalContexts = function (contexts) { core.addGlobalContexts(contexts); }; + + /** + * All provided contexts will no longer be sent with every event + * + * @param contexts Array + */ + apiMethods.removeGlobalContexts = function (contexts) { core.removeGlobalContexts(contexts); }; + + /** + * Clear all global contexts that are sent with events + */ + apiMethods.clearGlobalContexts = function () { core.clearGlobalContexts(); }; + + /** + * Enable tracking of unhandled exceptions with custom contexts + * + * @param filter Function ErrorEvent => Bool to check whether error should be tracker + * @param contextsAdder Function ErrorEvent => Array to add custom contexts with + * internal state based on particular error + */ + apiMethods.enableErrorTracking = function (filter, contextsAdder) { + errorManager.enableErrorTracking(filter, contextsAdder, addCommonContexts()) + }; + + /** + * Track unhandled exception. + * This method supposed to be used inside try/catch block + * + * @param message string Message appeared in console + * @param filename string Source file (not used) + * @param lineno number Line number + * @param colno number Column number (not used) + * @param error Error error object (not present in all browsers) + * @param contexts Array of custom contexts + */ + apiMethods.trackError = function (message, filename, lineno, colno, error, contexts) { + var enrichedContexts = addCommonContexts(contexts); + errorManager.trackError(message, filename, lineno, colno, error, enrichedContexts); + }; + + /** + * Stop regenerating `pageViewId` (available from `web_page` context) + */ + apiMethods.preservePageViewId = function () { + preservePageViewId = true + }; + + apiMethods.setDebug = function (isDebug) { + debug = Boolean(isDebug).valueOf(); + updateReturnMethods(); + }; + + // Create guarded methods from apiMethods, + // and set returnMethods to apiMethods or safeMethods depending on value of debug + safeMethods = productionize(apiMethods); + updateReturnMethods(); + + return returnMethods; + }; +}()); diff --git a/snowplow-javascript-tracker/tags/tag.js b/snowplow-javascript-tracker/tags/tag.js new file mode 100644 index 0000000000..bfb5409654 --- /dev/null +++ b/snowplow-javascript-tracker/tags/tag.js @@ -0,0 +1,81 @@ +/* + * JavaScript tracker for Snowplow: tag.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Use this function to load Snowplow + * + * @param object p The window + * @param object l The document + * @param string o "script", the tag name of script elements + * @param string w The source of the Snowplow script. Make sure you get the latest version. + * @param string i The Snowplow namespace. The Snowplow user should set this. + * @param undefined n The new script (to be created inside the function) + * @param undefined g The first script on the page (to be found inside the function) + */ +;(function(p,l,o,w,i,n,g) { + "p:nomunge, l:nomunge, o:nomunge, w:nomunge, i:nomunge, n:nomunge, g:nomunge"; + + // Stop if the Snowplow namespace i already exists + if (!p[i]) { + + // Initialise the 'GlobalSnowplowNamespace' array + p['GlobalSnowplowNamespace'] = p['GlobalSnowplowNamespace'] || []; + + // Add the new Snowplow namespace to the global array so sp.js can find it + p['GlobalSnowplowNamespace'].push(i); + + // Create the Snowplow function + p[i] = function() { + (p[i].q = p[i].q || []).push(arguments); + }; + + // Initialise the asynchronous queue + p[i].q = p[i].q || []; + + // Create a new script element + n = l.createElement(o); + + // Get the first script on the page + g = l.getElementsByTagName(o)[0]; + + // The new script should load asynchronously + n.async = 1; + + // Load Snowplow + n.src = w; + + // Insert the Snowplow script before every other script so it executes as soon as possible + g.parentNode.insertBefore(n,g); + } +} (window, document, 'script', '//d1fc8wv8zag5ca.cloudfront.net/2/sp.js', 'new_name_here')); diff --git a/snowplow-javascript-tracker/tests/functional/detectors.js b/snowplow-javascript-tracker/tests/functional/detectors.js new file mode 100644 index 0000000000..a7fb5ac6a7 --- /dev/null +++ b/snowplow-javascript-tracker/tests/functional/detectors.js @@ -0,0 +1,201 @@ +/* + * JavaScript tracker for Snowplow: tests/functional/detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + 'intern!object', + 'intern/chai!assert' +], function(registerSuite, assert) { + + var + // Expected viewport dimensions vary based on browser + expectedViewportWidths = [ + 996, + 1008, // Windows 8, Windows 8.1 + 1016, // Linux, Firefox 27 + 1014, // Windows 7, Chrome 32 + 1022, + 1024, // Windows 7, IE 9, Firefox 27; Mac OS, Safari + 1020 // Windows 8, Windows 8.1 + ], + expectedViewportHeights = [ + 660, // Firefox 27.0, Linux + 632, // Firefox 27.0, Linux + 658, // Firefox 27.0, Windows 7 + 666, + 670, // Safari 6/7 + 687, // Windows 7, IE 9 + 694, // Chrome 32, Windows 7 - 707 + 695, // Windows 7, IE 9 + 686, // Chrome 32, Linux - 686 + 705, // Windows 8/8.1 + 717 // Windows 8/8.1 + ], + + // User fingerprint varies based on browser features + // TODO: try to hash this off the useragent - + // i.e. formal 1:1 relationship between viewport or signature and an individual browser + expectedSignatures = [ + 1587753772, // IE9, Windows 7 + 1101697779, // IE10, Windows 8 + 645783373, // IE11, Windows 8 + 580945094, // Firefox 27.0, Windows 7 + 1382842778, // Firefox 27.0, Linux + 1727588738, // Chrome 32.0, Windows 7 + 3978691790, // Chrome 32.0, Linux + 3552180943, // Safari 7, OS X 10.9 + 812921052 // Safari 6.2.7 Mac OS X 10.8 + ]; + + registerSuite({ + + name: 'Detectors test', + + // These tests don't work as intended. + // I tend to blame SauceLabs on this, because it can fail on one of two subsequent runs + // on equal environments, while these functions are fully deterministic. + // + // 'Get viewport dimensions': function() { + // + // return this.remote + // .get(require.toUrl('tests/pages/detectors.html')) + // .setFindTimeout(5000) + // .setWindowSize(1024, 768) + // .findByCssSelector('body.loaded') + // .findById('detectViewport') + // .getVisibleText() + // .then(function (text) { + // var dimensions = text.split('x'); + // assert.include(expectedViewportWidths, parseInt(dimensions[0]), 'Viewport width is valid'); + // assert.include(expectedViewportHeights, parseInt(dimensions[1]), 'Viewport height is valid'); + // }); + // }, + // + // 'Detect document size': function () { + // return this.remote + // .get(require.toUrl('tests/pages/detectors.html')) + // .setFindTimeout(5000) + // .setWindowSize(1024, 768) + // .findByCssSelector('body.loaded') + // .findById('detectDocumentDimensions') + // .getVisibleText() + // .then(function (text) { + // var dimensions = text.split('x'); + // assert.include(expectedViewportWidths, parseInt(dimensions[0]), 'Document width is valid'); + // assert.include(expectedViewportHeights, parseInt(dimensions[1]), 'Document height is valid'); + // }); + // }, + // + // 'User fingerprinting': function() { + // + // return this.remote + // .get(require.toUrl('tests/pages/detectors.html')) + // .setFindTimeout(5000) + // .setWindowSize(1024, 768) + // .findByCssSelector('body.loaded') + // .findById('detectSignature') + // .getVisibleText() + // .then(function (text) { + // assert.include(expectedSignatures, parseInt(text), 'Create a user fingerprint based on browser features'); + // }); + // }, + + 'Check localStorage availability': function() { + + return this.remote + .get(require.toUrl('tests/pages/detectors.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('localStorageAccessible') + .getVisibleText() + .then(function (text) { + assert.strictEqual(text, 'true', 'Detect localStorage accessibility'); + }); + }, + + 'Check sessionStorage availability': function() { + + return this.remote + .get(require.toUrl('tests/pages/detectors.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('hasSessionStorage') + .getVisibleText() + .then(function (text) { + assert.strictEqual(text, 'true', 'Detect sessionStorage'); + }); + }, + + 'Check whether cookies are enabled': function() { + + return this.remote + .get(require.toUrl('tests/pages/detectors.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('hasCookies') + .getVisibleText() + .then(function (text) { + assert.equal(text, '1', 'Detect whether cookies can be set'); + }); + }, + + 'Detect timezone': function() { + + return this.remote + .get(require.toUrl('tests/pages/detectors.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('detectTimezone') + .getVisibleText() + .then(function (text) { + assert.include(['UTC', 'America/Los_Angeles'], text, 'Detect the timezone'); + }); + }, + + 'Browser features': function() { + + return this.remote + .get(require.toUrl('tests/pages/detectors.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('detectBrowserFeatures') + .getVisibleText() + .then(function (text) { + var features = JSON.parse(text); + // The only features which are the same for all tested browsers + assert.equal('1', features.java, 'Detect Java'); + assert.equal(24, features.cd, 'Detect color depth'); + }); + } + }); +}); diff --git a/snowplow-javascript-tracker/tests/functional/helpers.js b/snowplow-javascript-tracker/tests/functional/helpers.js new file mode 100644 index 0000000000..543d9e632f --- /dev/null +++ b/snowplow-javascript-tracker/tests/functional/helpers.js @@ -0,0 +1,98 @@ +/* + * JavaScript tracker for Snowplow: tests/functional/helpers.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + 'intern!object', + 'intern/chai!assert', + 'intern/dojo/node!../../src/js/lib/helpers' +], function(registerSuite, assert, helpers) { + + registerSuite({ + + name: 'Helpers test', + + 'Get page title': function() { + + return this.remote + .get(require.toUrl('tests/pages/helpers.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('title') + .getVisibleText() + .then(function (text) { + assert.strictEqual(text, 'Helpers test page', 'Get the page title' ); + }); + }, + + 'Get host name': function() { + + return this.remote + .get(require.toUrl('tests/pages/helpers.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('hostname') + .getVisibleText() + .then(function (text){ + assert.strictEqual(text, 'localhost', 'Get the host name'); + }); + }, + + 'Get referrer from querystring': function() { + + return this.remote + .get(require.toUrl('tests/pages/helpers.html') + '?name=value&referrer=previous#fragment') + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('referrer') + .getVisibleText() + .then(function (text){ + assert.strictEqual(text, 'previous', 'Get the referrer from the querystring'); + }); + }, + + 'Add event listener': function() { + + return this.remote + .get(require.toUrl('tests/pages/helpers.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .findById('click') + .click() + .getVisibleText() + .then(function (text){ + assert.strictEqual(text, 'clicked', 'Add a click event listener'); + }); + } + }); +}); diff --git a/snowplow-javascript-tracker/tests/integration/integration.js b/snowplow-javascript-tracker/tests/integration/integration.js new file mode 100755 index 0000000000..7d644bba3e --- /dev/null +++ b/snowplow-javascript-tracker/tests/integration/integration.js @@ -0,0 +1,320 @@ +/* + * JavaScript tracker for Snowplow: tests/functional/helpers.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2016 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + 'intern!object', + 'intern/chai!assert', + 'intern/dojo/node!lodash', + 'intern/dojo/node!http', + 'intern/dojo/node!url', + "intern/dojo/node!js-base64" +], function(registerSuite, assert, lodash, http, url, jsBase64) { + var decodeBase64 = jsBase64.Base64.fromBase64; + + /** + * Expected amount of request for each browser + * This must be increased when new tracking call added to + * pages/integration-template.html + */ + var log = []; + + function pageViewsHaveDifferentIds () { + var pageViews = lodash.filter(log, function (logLine) { + return logLine.e === 'pv'; + }); + var contexts = lodash.map(pageViews, function (logLine) { + var data = JSON.parse(decodeBase64(logLine.cx)).data; + return lodash.find(data, function (context) { + return context.schema === 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0'; + }); + }); + var ids = lodash.map(contexts, function (wpContext) { + return wpContext.data.id; + }); + + return lodash.uniq(ids).length >= 2; + } + + /** + * Check if expected payload exists in `log` + */ + function checkExistenceOfExpectedQuerystring(expected) { + function compare(e, other) { // e === expected + var result = lodash.map(e, function (v, k) { + if (lodash.isFunction(v)) { return v(other[k]); } + else { return lodash.isEqual(v, other[k]); } + }); + return lodash.every(result); + } + + function strip(logLine) { + var expectedKeys = lodash.keys(expected); + var stripped = lodash.pickBy(logLine, function (v, k) { return lodash.includes(expectedKeys, k); }); + if (lodash.keys(stripped).length !== expectedKeys.length) { return null; } + else { return stripped; } + } + + return lodash.some(log, function (logLine) { + var stripped = strip(logLine); + if (stripped == null) { return false; } + else { return lodash.isEqualWith(expected, stripped, compare); } + }); + } + + function someTestsFailed(suite) { + return lodash.some(suite.tests, function (test) { return test.error !== null; }); + } + + // Ngrok must be running to forward requests to localhost + http.createServer(function (request, response) { + response.writeHead(200, {'Content-Type': 'image/gif'}); + + if (request.method === 'GET') { + var payload = url.parse(request.url, true).query; + log.push(payload); + } + + var img = new Buffer('47494638396101000100800000dbdfef00000021f90401000000002c00000000010001000002024401003b', 'hex'); + response.end(img, 'binary'); + + }).listen(8500, function () { console.log("Collector mock running...\n"); }); + + registerSuite({ + + teardown: function () { + if (someTestsFailed(this)) { + console.log("Tests failed with following log:"); + lodash.forEach(log, function (l) { console.log(l); }); + } + console.log("Cleaning log"); + log = []; + }, + + name: 'Test that request_recorder logs meet expectations', + + 'Check existence of page view in log': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'pv', + p: 'mob', + aid: 'CFe23a', + uid: 'Malcolm', + page: 'My Title', + cx: function (cx) { + var contexts = JSON.parse(decodeBase64(cx)).data; + return lodash.some(contexts, + lodash.matches({ + schema:"iglu:com.example_company/user/jsonschema/2-0-0", + data:{ + userType:'tester' + } + }) + ); + } + }), 'A page view should be detected'); + }, + + 'Check nonexistence of nonexistent event types in log': function () { + assert.isFalse(checkExistenceOfExpectedQuerystring({ + e: 'ad' + }), 'No nonexistent event type should be detected'); + }, + + 'Check a structured event was sent': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'se', + se_ca: 'Mixes', + se_ac: 'Play', + se_la: 'MRC/fabric-0503-mix', + se_va: '0.0' + }), 'A structured event should be detected'); + }, + + 'Check an unstructured event with true timestamp was sent': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'ue', + ue_px: 'eyJzY2hlbWEiOiJpZ2x1OmNvbS5zbm93cGxvd2FuYWx5dGljcy5zbm93cGxvdy91bnN0cnVjdF9ldmVudC9qc29uc2NoZW1hLzEtMC0wIiwiZGF0YSI6eyJzY2hlbWEiOiJpZ2x1OmNvbS5hY21lX2NvbXBhbnkvdmlld2VkX3Byb2R1Y3QvanNvbnNjaGVtYS81LTAtMCIsImRhdGEiOnsicHJvZHVjdElkIjoiQVNPMDEwNDMifX19', + ttm: '1477401868' + }), 'An unstructured event should be detected'); + }, + + 'Check a transaction event was sent': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'tr', + tr_id: 'order-123', + tr_af: 'acme', + tr_tt: '8000', + tr_tx: '100', + tr_ci: 'phoenix', + tr_st: 'arizona', + tr_co: 'USA', + tr_cu: 'JPY' + }), 'A transaction event should be detected'); + }, + + 'Check a transaction item event was sent': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'ti', + ti_id: 'order-123', + ti_sk: '1001', + ti_nm: 'Blue t-shirt', + ti_ca: 'clothing', + ti_pr: '2000', + ti_qu: '2', + ti_cu: 'JPY' + }), 'A transaction item event should be detected'); + }, + + 'Check an unhandled exception was sent': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + ue_px: function (ue) { + var event = JSON.parse(decodeBase64(ue)).data; + // We cannot test more because implementations vary much in old browsers (FF27,IE9) + return (event.schema === 'iglu:com.snowplowanalytics.snowplow/application_error/jsonschema/1-0-1') && + (event.data.programmingLanguage === 'JAVASCRIPT') && + (event.data.message != null); + } + })); + }, + + 'Check pageViewId is regenerated for each trackPageView': function () { + assert.isTrue(pageViewsHaveDifferentIds()); + }, + + 'Check global contexts are for structured events': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'se', + cx: function (cx) { + var contexts = JSON.parse(decodeBase64(cx)).data; + return 2 === lodash.size( + lodash.filter(contexts, + lodash.overSome( + lodash.matches({ + schema: "iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1", + data: { + osType: 'ubuntu' + } + }), + lodash.matches({ + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude': 55.1 + } + }) + ) + ) + ); + } + })); + }, + + 'Check an unstructured event with global context from accept ruleset': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'ue', + ue_px: function (ue_px) { + var event = JSON.parse(decodeBase64(ue_px)).data; + return lodash.isMatch(event, + { + schema:"iglu:com.acme_company/viewed_product/jsonschema/5-0-0", + data:{ + productId: 'ASO01042' + } + } + ); + }, + cx: function (cx) { + var contexts = JSON.parse(decodeBase64(cx)).data; + return 2 === lodash.size( + lodash.filter(contexts, + lodash.overSome( + lodash.matches({ + schema: "iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1", + data: { + osType: 'ubuntu' + } + }), + lodash.matches({ + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude': 55.1 + } + }) + ) + ) + ); + } + }), 'An unstructured event with global contexts should be detected'); + }, + + 'Check an unstructured event missing global context from reject ruleset': function () { + assert.isTrue(checkExistenceOfExpectedQuerystring({ + e: 'ue', + ue_px: function (ue_px) { + var event = JSON.parse(decodeBase64(ue_px)).data; + return lodash.isMatch(event, + { + schema:"iglu:com.acme_company/viewed_product/jsonschema/5-0-0", + data:{ + productId: 'ASO01041' + } + } + ); + }, + cx: function (cx) { + var contexts = JSON.parse(decodeBase64(cx)).data; + return 0 === lodash.size( + lodash.filter(contexts, + lodash.overSome( + lodash.matches({ + schema: "iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-1", + data: { + osType: 'ubuntu' + } + }), + lodash.matches({ + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + 'latitude': 40.0, + 'longitude': 55.1 + } + }) + ) + ) + ); + } + }), 'An unstructured event without global contexts should be detected'); + } + }); +}); diff --git a/snowplow-javascript-tracker/tests/integration/setup.js b/snowplow-javascript-tracker/tests/integration/setup.js new file mode 100644 index 0000000000..0689b99708 --- /dev/null +++ b/snowplow-javascript-tracker/tests/integration/setup.js @@ -0,0 +1,52 @@ +/* + * JavaScript tracker for Snowplow: tests/functional/detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + 'intern!object' +], function(registerSuite) { + + registerSuite({ + + name: 'Integration setup', + + 'Set up the page and send some events to request_recorder': function() { + + return this.remote + .get(require.toUrl('tests/pages/integration.html')) + .setFindTimeout(5000) + .findByCssSelector('body.loaded') + .sleep(15000); // Time for requests to get written + } + }); +}); diff --git a/snowplow-javascript-tracker/tests/intern.js b/snowplow-javascript-tracker/tests/intern.js new file mode 100644 index 0000000000..b143f25b98 --- /dev/null +++ b/snowplow-javascript-tracker/tests/intern.js @@ -0,0 +1,61 @@ +/* + * JavaScript tracker for Snowplow: tests/intern.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define({ + + capabilities: { + 'selenium-version': '2.48.0' + }, + + environments: [ + { browserName: 'internet explorer', version: '11', platform: 'Windows 8.1' }, + { browserName: 'internet explorer', version: '10', platform: 'Windows 8' }, + { browserName: 'internet explorer', version: '9', platform: 'Windows 7' }, + { browserName: 'firefox', version: '27', platform: [ 'Windows 7', 'Linux' ] }, + { browserName: 'chrome', version: '32', platform: [ 'Windows 7', 'Linux' ] }, + { browserName: 'safari', version: '8', platform: 'OS X 10.10' }, + { browserName: 'safari', version: '10', platform: 'OS X 10.11' } + ], + + maxConcurrency: 1, + + tunnel: 'SauceLabsTunnel', + tunnelOptions: { + logFile: process.cwd() + '/SauceLabsTunnel.log' + }, + + // A regular expression matching URLs to files that should not be included in code coverage analysis + excludeInstrumentation: /^(?:tests|node_modules)\// + +}); diff --git a/snowplow-javascript-tracker/tests/local/http-server.py b/snowplow-javascript-tracker/tests/local/http-server.py new file mode 100644 index 0000000000..a1dac7da6f --- /dev/null +++ b/snowplow-javascript-tracker/tests/local/http-server.py @@ -0,0 +1,71 @@ +import os +import sys +from http.server import HTTPServer, BaseHTTPRequestHandler +from http.client import NO_CONTENT +from pprint import pprint + + +gif_string = b'GIF87a\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\xff\xff\xff,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;' +file_path = os.path.dirname(__file__) + +def get_serve(file): + return os.path.join(file_path, 'serve', file) + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + + def do_HEAD(self): + self.send_response(200) + if (self.path == '/integration.html'): + self.send_header("Content-type", "text/html") + self.end_headers() + else: + self.send_header("Content-type", "image/gif") + self.end_headers() + + + def do_GET(self): + """Respond to a GET request.""" + self.send_response(200) + if (self.path == '/integration.html'): + self.send_header("Content-type", "text/html") + self.end_headers() + f = open(get_serve('integration.html'), "rb") + body = f.read() + f.close() + self.wfile.write(body) + elif (self.path == '/snowplow.js'): + self.send_header("Content-type", "text/html") + self.end_headers() + f = open(get_serve('snowplow.js'), "rb") + body = f.read() + f.close() + self.wfile.write(body) + else: + self.send_header("Content-type", "image/gif") + self.end_headers() + self.wfile.write(gif_string) + + def do_POST(self): + self.send_response(200) + self.send_header("Content-type", "image/gif") + self.end_headers() + self.wfile.write(''.encode()) + content_len = int(self.headers.get('content-length', 0)) + post_body = self.rfile.read(content_len) + pprint(post_body) + + def do_OPTIONS(self): + self.send_response(200) + self.send_header('Access-Control-Allow-Origin', 'null') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header("Access-Control-Allow-Headers", "X-Requested-With") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + self.end_headers() + + +httpd = HTTPServer(('127.0.0.1', 8000), SimpleHTTPRequestHandler) +try: + httpd.serve_forever() +except KeyboardInterrupt: + print('User closed server with Ctrl-c') + sys.exit(0) diff --git a/snowplow-javascript-tracker/tests/nonfunctional/helpers.js b/snowplow-javascript-tracker/tests/nonfunctional/helpers.js new file mode 100644 index 0000000000..9a137b7890 --- /dev/null +++ b/snowplow-javascript-tracker/tests/nonfunctional/helpers.js @@ -0,0 +1,156 @@ +/* + * JavaScript tracker for Snowplow: tests/nonfunctional/helpers.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + "intern!object", + "intern/chai!assert", + "intern/dojo/node!../../src/js/lib/helpers" +], function (registerSuite, assert, helpers) { + + var decorateQuerystring = helpers.decorateQuerystring; + var resolveDynamicContexts = helpers.resolveDynamicContexts; + + registerSuite({ + name: "decorateQuerystring test", + "Decorate a URL with no querystring or fragment": function () { + var url = 'http://www.example.com'; + var expected = 'http://www.example.com?_sp=a.b'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Decorate a URL with a fragment but no querystring": function () { + var url = 'http://www.example.com#fragment'; + var expected = 'http://www.example.com?_sp=a.b#fragment'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Decorate a URL with an empty querystring": function () { + var url = 'http://www.example.com?'; + var expected = 'http://www.example.com?_sp=a.b'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Decorate a URL with a nonempty querystring": function () { + var url = 'http://www.example.com?name=value'; + var expected = 'http://www.example.com?_sp=a.b&name=value'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Override an existing field": function () { + var url = 'http://www.example.com?_sp=outdated'; + var expected = 'http://www.example.com?_sp=a.b'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Decorate a URL whose querystring contains multiple question marks": function () { + var url = 'http://www.example.com?test=working?&name=value'; + var expected = 'http://www.example.com?_sp=a.b&test=working?&name=value'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Override a field in a querystring containing a question mark": function () { + var url = 'http://www.example.com?test=working?&_sp=outdated'; + var expected = 'http://www.example.com?test=working?&_sp=a.b'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + + "Decorate a querystring with multiple ?s and #s": function () { + var url = 'http://www.example.com?test=working?&_sp=outdated?&?name=value#fragment?#?#'; + var expected = 'http://www.example.com?test=working?&_sp=a.b&?name=value#fragment?#?#'; + var actual = decorateQuerystring(url, '_sp', 'a.b'); + assert.equal(actual, expected); + }, + }); + + registerSuite({ + name: "getCssClasses test", + "Tokenize a DOM element's className field": function () { + var element = { + className: ' the quick brown_fox-jumps/over\nthe\t\tlazy dog ' + }; + var expected = ['the', 'quick', 'brown_fox-jumps/over', 'the', 'lazy', 'dog']; + var actual = helpers.getCssClasses(element); + assert.deepEqual(actual, expected); + }, + }); + + registerSuite({ + name: "resolveDynamicContexts tests", + "Resolves context generators and static contexts": function () { + var contextGenerator = function () { + return { + 'schema': 'iglu:com.acme.marketing/some_event/jsonschema/1-0-0', + 'data': {'test': 1} + } + }; + var staticContext = { + 'schema': 'iglu:com.acme.marketing/some_event/jsonschema/1-0-0', + 'data': {'test': 1} + }; + var expected = [contextGenerator(), staticContext]; + var actual = resolveDynamicContexts([contextGenerator, staticContext]); + assert.deepEqual(actual, expected); + }, + + "Resolves context generators with arguments": function () { + var contextGenerator = function (argOne, argTwo) { + return { + 'schema': 'iglu:com.acme.marketing/some_event/jsonschema/1-0-0', + 'data': { + 'firstVal': argOne, + 'secondVal': argTwo + } + } + }; + var expected = [ + { + 'schema': 'iglu:com.acme.marketing/some_event/jsonschema/1-0-0', + 'data': { + 'firstVal': 1, + 'secondVal': 2 + } + } + ]; + var actual = resolveDynamicContexts([contextGenerator], 1, 2); + assert.deepEqual(actual, expected); + }, + }); +}); diff --git a/snowplow-javascript-tracker/tests/nonfunctional/in_queue.js b/snowplow-javascript-tracker/tests/nonfunctional/in_queue.js new file mode 100755 index 0000000000..341d3cd969 --- /dev/null +++ b/snowplow-javascript-tracker/tests/nonfunctional/in_queue.js @@ -0,0 +1,104 @@ +/* + * JavaScript tracker for Snowplow: tests/nonfunctional/in_queue.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + "intern!object", + "intern/chai!assert", + "intern/dojo/node!../../src/js/in_queue" +], function(registerSuite, assert, in_queue) { + + var output = 0; + + function mockTrackerConstructor (functionName, namespace, version, mutSnowplowState, argmap) { + var configCollectorUrl, + attribute = 10; + + return { + setCollectorUrl: function(rawUrl) { + configCollectorUrl = "http://" + rawUrl + "/i"; + }, + increaseAttribute: function(n) { + attribute += n; + }, + setAttribute: function(p) { + attribute = p; + }, + setOutputToAttribute: function() { + output = attribute; + }, + addAttributeToOutput: function() { + output += attribute; + } + }; + } + + var asyncQueue = [ + ["newTracker", "firstTracker", "firstEndpoint"], + ["increaseAttribute", 5], + ["setOutputToAttribute"] + ]; + asyncQueue = new in_queue.InQueueManager(mockTrackerConstructor, 0, {}, asyncQueue, 'snowplow'); + + registerSuite({ + name: "InQueueManager test", + "Make a proxy": function() { + assert.equal(output, 15, "Function originally stored in asyncQueue is executed when asyncQueue becomes an AsyncQueueProxy"); + }, + + "Add to asyncQueue after conversion": function() { + asyncQueue.push(["setAttribute", 7]); + asyncQueue.push(["setOutputToAttribute"]); + assert.equal(output, 7, "Function added to asyncQueue after it becomes an AsyncQueueProxy is executed"); + }, + + "Backward compatibility: Create a tracker using the legacy setCollectorUrl method": function() { + asyncQueue.push(["setCollectorUrl", "secondEndpoint"]); + asyncQueue.push(["addAttributeToOutput"]); + assert.equal(output, 24, "A second tracker is created and both trackers' attributes are added to output"); + }, + + "Use 'function:tracker1;tracker2' syntax to control which trackers execute which functions": function() { + asyncQueue.push(["setAttribute:firstTracker", 2]); + asyncQueue.push(["setAttribute:sp", 3]); + asyncQueue.push(["addAttributeToOutput:firstTracker;sp"]); + assert.equal(output, 29, "Set the attributes of the two trackers individually, then add both to output"); + }, + + "Execute a user-defined custom callback": function () { + var callbackExecuted = false; + asyncQueue.push([function () { callbackExecuted = true; }]); + assert.isTrue(callbackExecuted); + } + }); +}); diff --git a/snowplow-javascript-tracker/tests/nonfunctional/proxies.js b/snowplow-javascript-tracker/tests/nonfunctional/proxies.js new file mode 100644 index 0000000000..fd082eac2b --- /dev/null +++ b/snowplow-javascript-tracker/tests/nonfunctional/proxies.js @@ -0,0 +1,141 @@ +/* + * JavaScript tracker for Snowplow: tests/nonfunctional/proxies.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +define([ + "intern!object", + "intern/chai!assert", + "intern/dojo/node!../../src/js/lib/proxies" +], function(registerSuite, assert, proxies) { + + this.document = { + links: [{href: "http://www.example.com/"}], + body: { + children: [{ + children: [{ + children: [{ + children: [{ + children: [{ + children: [{ + innerHTML: 'You have reached the cached page for' + }] + }] + }] + }] + }] + }] + } + }; + + registerSuite({ + + "name": "Proxies test", + + "Host name is not a special case": function() { + + var + initialLocationArray = ["normalhostname", "href", "http://referrer.com"], + fixedupLocationArray = proxies.fixupUrl.apply(null, initialLocationArray), + expectedLocationArray = fixedupLocationArray, + i; + + for (i = 0; i < 3; i++) { + assert.strictEqual(fixedupLocationArray[i], expectedLocationArray[i], 'Except in special cases, fixupUrl changes nothing'); + } + }, + + "Host name = 'translate.googleusercontent.com'": function() { + var + initialLocationArray = [ + "translate.googleusercontent.com", + "http://translate.googleusercontent.com/translate?hl=en&sl=fr&u=http:www.francais.fr/path", + ""], + fixedupLocationArray = proxies.fixupUrl.apply(null, initialLocationArray), + expectedLocationArray = [ + "www.francais.fr", + "http:www.francais.fr/path", + "http://translate.googleusercontent.com/translate?hl=en&sl=fr&u=http:www.francais.fr/path"], + i; + + for (i = 0; i < 3; i++) { + assert.strictEqual(fixedupLocationArray[i], expectedLocationArray[i], 'Get the URL for the untranslated page from the querystring and make the translated page the referrer'); + } + }, + + "Host name = 'ccj.bingj.com'": function() { + var + initialLocationArray = [ + "cc.bingj.com", + "http://cc.bingj.com/cache.aspx?q=example.com&d=4870936571937837&mkt=en-GB&setlang=en-GB&w=QyOPD1fo3C2nC9sXMLmUUs81Jt78MYIp", + "http://referrer.com"], + fixedupLocationArray = proxies.fixupUrl.apply(null, initialLocationArray), + expectedLocationArray = [ "www.example.com", "http://www.example.com/", "http://referrer.com" ], + i; + + for (i = 0; i < 3; i++) { + assert.strictEqual(fixedupLocationArray[i], expectedLocationArray[i], 'On a page cached by Bing, get the original URL from the first link'); + } + }, + + "Host name = 'webcache.googleusercontent.com'": function() { + var + initialLocationArray = [ + "webcache.googleusercontent.com", + "http://webcache.googleusercontent.com/search?q=cache:http://example.com/#fragment", + "http://referrer.com"], + fixedupLocationArray = proxies.fixupUrl.apply(null, initialLocationArray), + expectedLocationArray = [ "www.example.com", "http://www.example.com/", "http://referrer.com" ], + i; + + for (i = 0; i < 3; i++) { + assert.strictEqual(fixedupLocationArray[i], expectedLocationArray[i], 'On a page cached by Google, get the original URL from the first link'); + } + }, + + + "Host name is an IP address": function() { + var + initialLocationArray = [ + "98.139.21.31", + "http://98.139.21.31/search/srpcache", + "http://referrer.com"], + fixedupLocationArray = proxies.fixupUrl.apply(null, initialLocationArray), + expectedLocationArray = [ "www.example.com", "http://www.example.com/", "http://referrer.com" ], + i; + + for (i = 0; i < 3; i++) { + assert.strictEqual(fixedupLocationArray[i], expectedLocationArray[i], 'On a page cached by Yahoo, get the original URL from the first link'); + } + } + }) +}); diff --git a/snowplow-javascript-tracker/tests/pages/detectors.html b/snowplow-javascript-tracker/tests/pages/detectors.html new file mode 100644 index 0000000000..fdccf3911c --- /dev/null +++ b/snowplow-javascript-tracker/tests/pages/detectors.html @@ -0,0 +1,25 @@ + + + + Detectors test page + + +

Viewport dimensions here

+

Document dimensions here

+

localStorage accessibility here

+

sessionStorage here

+

hasCookies here

+

Timezone here

+

User fingerprint here

+

User fingerprint here

+ + + + + + + \ No newline at end of file diff --git a/snowplow-javascript-tracker/tests/pages/helpers.html b/snowplow-javascript-tracker/tests/pages/helpers.html new file mode 100644 index 0000000000..615c8961be --- /dev/null +++ b/snowplow-javascript-tracker/tests/pages/helpers.html @@ -0,0 +1,21 @@ + + + + Helpers test page + + +

Title here

+

Hostname here

+

Referrer here

+

Click here

+ + + + + + + \ No newline at end of file diff --git a/snowplow-javascript-tracker/tests/pages/integration-template.html b/snowplow-javascript-tracker/tests/pages/integration-template.html new file mode 100644 index 0000000000..dc3c5eca19 --- /dev/null +++ b/snowplow-javascript-tracker/tests/pages/integration-template.html @@ -0,0 +1,157 @@ + + + + Integration test page + + +

Page for sending requests to request_recorder

+ + + + + + + diff --git a/snowplow-javascript-tracker/tests/scripts/detectors.js b/snowplow-javascript-tracker/tests/scripts/detectors.js new file mode 100644 index 0000000000..7add52ffdc --- /dev/null +++ b/snowplow-javascript-tracker/tests/scripts/detectors.js @@ -0,0 +1,44 @@ +/* + * JavaScript tracker for Snowplow: tests/scripts/detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var detectors = require('../../src/js/lib/detectors.js'); + +document.getElementById('detectViewport').innerHTML = detectors.detectViewport(); +document.getElementById('detectDocumentDimensions').innerHTML = detectors.detectDocumentSize(); +document.getElementById('localStorageAccessible').innerHTML = detectors.localStorageAccessible(); +document.getElementById('hasSessionStorage').innerHTML = detectors.hasSessionStorage(); +document.getElementById('hasCookies').innerHTML = detectors.hasCookies(); +document.getElementById('detectTimezone').innerHTML = detectors.detectTimezone(); +document.getElementById('detectSignature').innerHTML = detectors.detectSignature(); +document.getElementById('detectBrowserFeatures').innerHTML = JSON.stringify(detectors.detectBrowserFeatures(), undefined, 2); diff --git a/snowplow-javascript-tracker/tests/scripts/helpers.js b/snowplow-javascript-tracker/tests/scripts/helpers.js new file mode 100644 index 0000000000..3782c06af4 --- /dev/null +++ b/snowplow-javascript-tracker/tests/scripts/helpers.js @@ -0,0 +1,42 @@ +/* + * JavaScript tracker for Snowplow: tests/scripts/helpers.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +var helpers = require('../../src/js/lib/helpers.js'); + +document.getElementById('title').innerHTML = helpers.fixupTitle(0); +document.getElementById('hostname').innerHTML = helpers.getHostName(location.href); +document.getElementById('referrer').innerHTML = helpers.getReferrer(); +helpers.addEventListener(document.getElementById('click'), 'click', function(){ + document.getElementById('click').innerHTML = 'clicked'; +}); diff --git a/snowplow-javascript-tracker/vagrant/.gitignore b/snowplow-javascript-tracker/vagrant/.gitignore new file mode 100644 index 0000000000..1b4b29ff1f --- /dev/null +++ b/snowplow-javascript-tracker/vagrant/.gitignore @@ -0,0 +1,3 @@ +.peru +oss-playbooks +ansible diff --git a/snowplow-javascript-tracker/vagrant/ansible.hosts b/snowplow-javascript-tracker/vagrant/ansible.hosts new file mode 100644 index 0000000000..588fa08c76 --- /dev/null +++ b/snowplow-javascript-tracker/vagrant/ansible.hosts @@ -0,0 +1,2 @@ +[vagrant] +127.0.0.1:2222 diff --git a/snowplow-javascript-tracker/vagrant/peru.yaml b/snowplow-javascript-tracker/vagrant/peru.yaml new file mode 100644 index 0000000000..e7fdf41cf7 --- /dev/null +++ b/snowplow-javascript-tracker/vagrant/peru.yaml @@ -0,0 +1,14 @@ +imports: + ansible: ansible + ansible_playbooks: oss-playbooks + +curl module ansible: + # Equivalent of git cloning tags/v1.6.6 but much, much faster + url: https://codeload.github.com/ansible/ansible/zip/69d85c22c7475ccf8169b6ec9dee3ee28c92a314 + unpack: zip + export: ansible-69d85c22c7475ccf8169b6ec9dee3ee28c92a314 + +git module ansible_playbooks: + url: https://github.com/snowplow/ansible-playbooks.git + # Comment out to fetch a specific rev instead of master: + # rev: xxx diff --git a/snowplow-javascript-tracker/vagrant/up.bash b/snowplow-javascript-tracker/vagrant/up.bash new file mode 100755 index 0000000000..7450ae897e --- /dev/null +++ b/snowplow-javascript-tracker/vagrant/up.bash @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +vagrant_dir=/vagrant/vagrant +bashrc=/home/vagrant/.bashrc + +echo "========================================" +echo "INSTALLING PERU AND ANSIBLE DEPENDENCIES" +echo "----------------------------------------" +apt-get update +apt-get install -y language-pack-en git unzip libyaml-dev python3-pip python-yaml python-paramiko python-jinja2 + +echo "===============" +echo "INSTALLING PERU" +echo "---------------" +sudo pip3 install peru + +echo "=======================================" +echo "CLONING ANSIBLE AND PLAYBOOKS WITH PERU" +echo "---------------------------------------" +cd ${vagrant_dir} && peru sync -v +echo "... done" + +env_setup=${vagrant_dir}/ansible/hacking/env-setup +hosts=${vagrant_dir}/ansible.hosts + +echo "===================" +echo "CONFIGURING ANSIBLE" +echo "-------------------" +touch ${bashrc} +echo "source ${env_setup}" >> ${bashrc} +echo "export ANSIBLE_HOSTS=${hosts}" >> ${bashrc} +echo "... done" + +echo "==========================================" +echo "RUNNING PLAYBOOKS WITH ANSIBLE*" +echo "* no output while each playbook is running" +echo "------------------------------------------" +while read pb; do + su - -c "source ${env_setup} && ${vagrant_dir}/ansible/bin/ansible-playbook ${vagrant_dir}/${pb} --connection=local --inventory-file=${hosts}" vagrant +done <${vagrant_dir}/up.playbooks + +guidance=${vagrant_dir}/up.guidance + +if [ -f ${guidance} ]; then + echo "===========" + echo "PLEASE READ" + echo "-----------" + cat $guidance +fi diff --git a/snowplow-javascript-tracker/vagrant/up.guidance b/snowplow-javascript-tracker/vagrant/up.guidance new file mode 100644 index 0000000000..25078aa213 --- /dev/null +++ b/snowplow-javascript-tracker/vagrant/up.guidance @@ -0,0 +1,6 @@ +To get started: +vagrant ssh +cd /vagrant +sudo npm install +cd core +sudo npm install diff --git a/snowplow-javascript-tracker/vagrant/up.playbooks b/snowplow-javascript-tracker/vagrant/up.playbooks new file mode 100644 index 0000000000..9e621ab9f8 --- /dev/null +++ b/snowplow-javascript-tracker/vagrant/up.playbooks @@ -0,0 +1,3 @@ +oss-playbooks/java10.yml +oss-playbooks/nodejs.yml +oss-playbooks/grunt.yml