New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@segment/analytics.js-video-plugins

Package Overview
Dependencies
Maintainers
140
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@segment/analytics.js-video-plugins - npm Package Compare versions

Comparing version

to
0.1.0

.prettierignore

19

CONTRIBUTING.md
# Developing analytics.js-video-plaugins
- [Development Environemnt Setup](#development-setup)
- [Development Environment Setup](#development-setup)
- [Project Structure](#project-structure)

@@ -37,3 +37,3 @@ - [Code Conventions](#code-conventions)

The project also uses Babel to support the use of modern JS syntax. **Please Note:** the project does NOT yet use [babel-polyfill](https://babeljs.io/docs/en/babel-polyfill/). This is to ensure that the bundle size stay as small as possible. If you would like to use new ES5+ built-ins such as `Object.assign`, `Array.from`, etc. please load them in as indiviudal [ponyfills](https://github.com/sindresorhus/ponyfill). **Please ensure you explicitly use ponyfills instead of polyfills!** These plugins are distributed as libraries and cannot pollute the global scope.
The project also uses Babel to support the use of modern JS syntax. **Please Note:** the project does NOT yet use [babel-polyfill](https://babeljs.io/docs/en/babel-polyfill/). This is to ensure that the bundle size stay as small as possible. If you would like to use new ES5+ built-ins such as `Object.assign`, `Array.from`, etc. please load them in as individual [ponyfills](https://github.com/sindresorhus/ponyfill). **Please ensure you explicitly use ponyfills instead of polyfills!** These plugins are distributed as libraries and cannot pollute the global scope.

@@ -44,10 +44,2 @@ The project already ponyfills [`window.fetch`](https://github.com/developit/unfetch) if you need to make async requests. It does not support using `async/await`, please use `Promises`.

## Git Commit Guidelines
The project uses [Commitizen](https://github.com/commitizen/cz-cli) to allow for easy implementation of standardized commit messages.
**To use commitizen, run `yarn commit` in place of `git commit` for ALL commits.**
### Commit Scope
If you're working on a plugin, the [scope](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#scope) of your commits should exactly match the name of the directory of the plugin the change pertains to (`vimeo`, `youtube`, etc). If you're making a change outside of the `/plugins` directory, please use something simple like `ci`, `webpack`, `yarn`, `babel` etc.
## Testing Locally

@@ -57,5 +49,10 @@ If you'd like to test your plugin in a browser, simply run `yarn cdn`. This will build and distribute the plugins as a UMD module ready to be loaded into the browser. You can access the bundle at `http://localhost:8080/index.html`. Feel free to copy/paste this `script` tag into a test website:

```
<script>
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t,e){var n=document.createElement("script");n.type="text/javascript";n.async=!0;n.src="https://cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(n,a);analytics._loadOptions=e};analytics.SNIPPET_VERSION="4.1.0";
analytics.load("Segment write key");
}}();
</script>
<script src='http://localhost:8080/index.html' type='text/javascript'></script>
```
All plugins will be available at runtime at `window.videoPlugins<PLUGIN_NAME>`.
All plugins will be available at runtime at `window.videoPlugins.<PLUGIN_NAME>`.

@@ -250,3 +250,3 @@ module.exports =

var _this = _possibleConstructorReturn(this, (VimeoAnalytics.__proto__ || Object.getPrototypeOf(VimeoAnalytics)).call(this, 'VimeoAnalytics'));
var _this = _possibleConstructorReturn(this, (VimeoAnalytics.__proto__ || Object.getPrototypeOf(VimeoAnalytics)).call(this, "VimeoAnalytics"));

@@ -257,3 +257,3 @@ _this.authToken = authToken;

content: {},
playback: { videoPlayer: 'Vimeo' }
playback: { videoPlayer: "Vimeo" }
};

@@ -266,3 +266,3 @@ _this.mostRecentHeartbeat = 0;

_createClass(VimeoAnalytics, [{
key: 'initialize',
key: "initialize",
value: function initialize() {

@@ -291,3 +291,3 @@ var _this2 = this;

}, {
key: 'registerHandler',
key: "registerHandler",
value: function registerHandler(event, handler) {

@@ -302,17 +302,17 @@ var _this3 = this;

}, {
key: 'trackPlay',
key: "trackPlay",
value: function trackPlay() {
if (this.isPaused) {
this.track('Video Playback Resumed', this.metadata.playback);
this.track("Video Playback Resumed", this.metadata.playback);
this.isPaused = false;
} else {
this.track('Video Playback Started', this.metadata.playback);
this.track('Video Content Started', this.metadata.content);
this.track("Video Playback Started", this.metadata.playback);
this.track("Video Content Started", this.metadata.content);
}
}
}, {
key: 'trackEnded',
key: "trackEnded",
value: function trackEnded() {
this.track('Video Playback Completed', this.metadata.playback);
this.track('Video Content Completed', this.metadata.content);
this.track("Video Playback Completed", this.metadata.playback);
this.track("Video Content Completed", this.metadata.content);
}

@@ -325,3 +325,3 @@

}, {
key: 'trackHeartbeat',
key: "trackHeartbeat",
value: function trackHeartbeat() {

@@ -331,3 +331,3 @@ var mostRecentHeartbeat = this.mostRecentHeartbeat;

if (currentPosition !== mostRecentHeartbeat && currentPosition - mostRecentHeartbeat >= 10) {
this.track('Video Content Playing', this.metadata.content);
this.track("Video Content Playing", this.metadata.content);
this.mostRecentHeartbeat = Math.floor(currentPosition);

@@ -337,6 +337,6 @@ }

}, {
key: 'trackPause',
key: "trackPause",
value: function trackPause() {
this.isPaused = true;
this.track('Video Playback Paused', this.metadata.playback);
this.track("Video Playback Paused", this.metadata.playback);
}

@@ -347,3 +347,3 @@

}, {
key: 'retrieveMetadata',
key: "retrieveMetadata",
value: function retrieveMetadata(data) {

@@ -354,5 +354,5 @@ var _this4 = this;

var videoId = data.id;
(0, _unfetch2.default)('https://api.vimeo.com/videos/' + videoId, {
(0, _unfetch2.default)("https://api.vimeo.com/videos/" + videoId, {
headers: {
Authorization: 'Bearer ' + _this4.authToken
Authorization: "Bearer " + _this4.authToken
}

@@ -372,3 +372,3 @@ }).then(function (res) {

}).catch(function (err) {
console.error('Request to Vimeo API Failed with: ', err);
console.error("Request to Vimeo API Failed with: ", err);
return reject(err);

@@ -382,3 +382,3 @@ });

}, {
key: 'updateMetadata',
key: "updateMetadata",
value: function updateMetadata(data) {

@@ -433,9 +433,15 @@ var _this5 = this;

var YoutubeAnalytics = function (_VideoPlugin) {
_inherits(YoutubeAnalytics, _VideoPlugin);
var YouTubeAnalytics = function (_VideoPlugin) {
_inherits(YouTubeAnalytics, _VideoPlugin);
function YoutubeAnalytics(player, apiKey) {
_classCallCheck(this, YoutubeAnalytics);
/**
* Creates a YouTube video plugin to track events directly from the player.
*
* @param {YT.Player} player YouTube IFrame Player (docs: https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player).
* @param {String} apiKey Youtube Data API key (docs: https://developers.google.com/youtube/registering_an_application).
*/
function YouTubeAnalytics(player, apiKey) {
_classCallCheck(this, YouTubeAnalytics);
var _this = _possibleConstructorReturn(this, (YoutubeAnalytics.__proto__ || Object.getPrototypeOf(YoutubeAnalytics)).call(this, 'YoutubeAnalytics'));
var _this = _possibleConstructorReturn(this, (YouTubeAnalytics.__proto__ || Object.getPrototypeOf(YouTubeAnalytics)).call(this, "YoutubeAnalytics"));

@@ -450,7 +456,8 @@ _this.player = player;

_this.isSeeking = false;
_this.lastRecordedTime = { // updated every event
_this.lastRecordedTime = {
// updated every event
timeReported: Date.now(),
timeElapsed: 0.0
};
_this.metadata = [{ playback: { video_player: 'youtube' }, content: {} }];
_this.metadata = [{ playback: { video_player: "youtube" }, content: {} }];
_this.playlistIndex = 0;

@@ -460,4 +467,4 @@ return _this;

_createClass(YoutubeAnalytics, [{
key: 'initialize',
_createClass(YouTubeAnalytics, [{
key: "initialize",
value: function initialize() {

@@ -468,10 +475,7 @@ // Youtube API requires listeners to exist as top-level props on window object

var player = this.player;
player.addEventListener('onReady', 'segmentYoutubeOnReady');
player.addEventListener('onStateChange', 'segmentYoutubeOnStateChange');
this.player.addEventListener("onReady", "segmentYoutubeOnReady");
this.player.addEventListener("onStateChange", "segmentYoutubeOnStateChange");
}
}, {
key: 'onPlayerReady',
key: "onPlayerReady",
value: function onPlayerReady(event) {

@@ -485,5 +489,4 @@ // this fires when the player html element loads

}, {
key: 'onPlayerStateChange',
key: "onPlayerStateChange",
value: function onPlayerStateChange(event) {
var self = this;
var currentTime = this.player.getCurrentTime();

@@ -493,3 +496,3 @@ if (this.metadata[this.playlistIndex]) {

this.metadata[this.playlistIndex].playback.quality = this.player.getPlaybackQuality();
this.metadata[this.playlistIndex].playback.sound = this.player.getVolume();
this.metadata[this.playlistIndex].playback.sound = this.player.isMuted() ? 0 : this.player.getVolume();
}

@@ -528,6 +531,12 @@

// retrieve metadata from Youtube Data API using user's API Key
/**
* Retrieves the video metadata from Youtube Data API using user's API Key
* docs: https://developers.google.com/youtube/v3/docs/videos/list
*
* @returns {Promise} A promise that resolves in a metadata object from the video
* contained in the player.
*/
}, {
key: 'retrieveMetadata',
key: "retrieveMetadata",
value: function retrieveMetadata() {

@@ -538,3 +547,2 @@ var _this2 = this;

var videoData = _this2.player.getVideoData();
console.log(_this2.player.getPlaylist());
var playlist = _this2.player.getPlaylist() || [videoData.video_id];

@@ -544,5 +552,5 @@

(0, _unfetch2.default)('https://www.googleapis.com/youtube/v3/videos?id=' + videoIds + '&part=snippet,contentDetails&key=' + _this2.apiKey).then(function (res) {
(0, _unfetch2.default)("https://www.googleapis.com/youtube/v3/videos?id=" + videoIds + "&part=snippet,contentDetails&key=" + _this2.apiKey).then(function (res) {
if (!res.ok) {
var err = new Error('Segment request to Youtube API failed (likely due to a bad API Key. Events will still be sent but will not contain video metadata)');
var err = new Error("Segment request to Youtube API failed (likely due to a bad API Key. Events will still be sent but will not contain video metadata)");
err.response = res;

@@ -572,3 +580,3 @@ throw err;

total_length: total_length,
video_player: 'youtube'
video_player: "youtube"
};

@@ -579,3 +587,3 @@ }

_this2.metadata = playlist.map(function (item) {
return { playback: { video_player: 'youtube' }, content: {} };
return { playback: { video_player: "youtube" }, content: {} };
});

@@ -587,3 +595,3 @@ reject(err);

}, {
key: 'handleBuffer',
key: "handleBuffer",
value: function handleBuffer() {

@@ -594,3 +602,3 @@ var seekDetected = this.determineSeek();

this.playbackStarted = true;
this.track('Video Playback Started', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Started", this.metadata[this.playlistIndex].playback);
}

@@ -601,3 +609,3 @@

this.isSeeking = true;
this.track('Video Playback Seek Started', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Seek Started", this.metadata[this.playlistIndex].playback);
}

@@ -607,3 +615,3 @@

if (this.isSeeking) {
this.track('Video Playback Seek Completed', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Seek Completed", this.metadata[this.playlistIndex].playback);
this.isSeeking = false;

@@ -618,12 +626,12 @@ }

// user skipped to end of last video in playlist
this.track('Video Playback Completed', this.metadata[this.player.getPlaylistIndex()].playback);
this.track('Video Playback Started', this.metadata[this.player.getPlaylistIndex()].playback); // playlist automatically starts from beginning
this.track("Video Playback Completed", this.metadata[this.player.getPlaylistIndex()].playback);
this.track("Video Playback Started", this.metadata[this.player.getPlaylistIndex()].playback); // playlist automatically starts from beginning
}
}
this.track('Video Playback Buffer Started', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Buffer Started", this.metadata[this.playlistIndex].playback);
this.isBuffering = true;
}
}, {
key: 'handlePlay',
key: "handlePlay",
value: function handlePlay() {

@@ -633,3 +641,3 @@ if (!this.contentStarted) {

if (this.playlistIndex === -1) this.playlistIndex = 0;
this.track('Video Content Started', this.metadata[this.playlistIndex].content);
this.track("Video Content Started", this.metadata[this.playlistIndex].content);
this.contentStarted = true;

@@ -639,3 +647,3 @@ }

if (this.isBuffering) {
this.track('Video Playback Buffer Completed', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Buffer Completed", this.metadata[this.playlistIndex].playback);
this.isBuffering = false;

@@ -645,3 +653,3 @@ }

if (this.isPaused) {
this.track('Video Playback Resumed', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Resumed", this.metadata[this.playlistIndex].playback);
this.isPaused = false;

@@ -651,3 +659,3 @@ }

}, {
key: 'handlePause',
key: "handlePause",
value: function handlePause() {

@@ -657,3 +665,3 @@ var seekDetected = this.determineSeek();

if (this.isBuffering) {
this.track('Video Playback Buffer Completed', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Buffer Completed", this.metadata[this.playlistIndex].playback);
this.isBuffering = false;

@@ -665,3 +673,3 @@ }

if (seekDetected) {
this.track('Video Playback Seek Started', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Seek Started", this.metadata[this.playlistIndex].playback);
this.isSeeking = true;

@@ -671,3 +679,3 @@ }

else {
this.track('Video Playback Paused', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Paused", this.metadata[this.playlistIndex].playback);
this.isPaused = true;

@@ -678,5 +686,5 @@ }

}, {
key: 'handleEnd',
key: "handleEnd",
value: function handleEnd() {
this.track('Video Content Completed', this.metadata[this.playlistIndex].content);
this.track("Video Content Completed", this.metadata[this.playlistIndex].content);
this.contentStarted = false;

@@ -688,3 +696,3 @@

if (playlist && playlistIndex === playlist.length - 1 || playlistIndex === -1) {
this.track('Video Playback Completed', this.metadata[this.playlistIndex].playback);
this.track("Video Playback Completed", this.metadata[this.playlistIndex].playback);
this.playbackStarted = false;

@@ -697,3 +705,3 @@ }

}, {
key: 'determineSeek',
key: "determineSeek",
value: function determineSeek() {

@@ -707,3 +715,3 @@ var expectedTimeElapsed = this.isPaused || this.isBuffering ? 0 : Date.now() - this.lastRecordedTime.timeReported;

return YoutubeAnalytics;
return YouTubeAnalytics;
}(_plugin2.default);

@@ -714,3 +722,3 @@

exports.default = YoutubeAnalytics;
exports.default = YouTubeAnalytics;
function YTDurationToSeconds(duration) {

@@ -721,3 +729,3 @@ var match = duration.match(/PT(\d+H)?(\d+M)?(\d+S)?/);

if (x != null) {
return x.replace(/\D/, '');
return x.replace(/\D/, "");
}

@@ -724,0 +732,0 @@ });

@@ -1,36 +0,36 @@

const baseConfig = require('./karma.conf.js')
const baseConfig = require("./karma.conf.js");
const customLaunchers = {
sl_chrome_latest: {
base: 'SauceLabs',
browserName: 'chrome',
platform: 'linux',
version: 'latest'
base: "SauceLabs",
browserName: "chrome",
platform: "linux",
version: "latest"
},
sl_chrome_latest_1: {
base: 'SauceLabs',
browserName: 'chrome',
platform: 'linux',
version: 'latest'
base: "SauceLabs",
browserName: "chrome",
platform: "linux",
version: "latest"
},
sl_firefox_latest: {
base: 'SauceLabs',
browserName: 'firefox',
platform: 'linux',
version: 'latest'
base: "SauceLabs",
browserName: "firefox",
platform: "linux",
version: "latest"
},
sl_firefox_latest_1: {
base: 'SauceLabs',
browserName: 'firefox',
platform: 'linux',
version: 'latest-1'
base: "SauceLabs",
browserName: "firefox",
platform: "linux",
version: "latest-1"
},
sl_safari_9: {
base: 'SauceLabs',
browserName: 'safari',
version: '9.0'
base: "SauceLabs",
browserName: "safari",
version: "9.0"
},
sl_edge_latest: {
base: 'SauceLabs',
browserName: 'microsoftedge'
base: "SauceLabs",
browserName: "microsoftedge"
}

@@ -43,6 +43,8 @@ };

if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
throw new Error('SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are required but are missing');
throw new Error(
"SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are required but are missing"
);
}
config.set({
config.set({
browserDisconnectTolerance: 1,

@@ -52,3 +54,3 @@

reporters: ['dots', 'saucelabs'],
reporters: ["dots", "saucelabs"],

@@ -60,11 +62,9 @@ browsers: Object.keys(customLaunchers),

sauceLabs: {
testName: require('./package.json').name
testName: require("./package.json").name
},
coverageReporter: {
reporters: [
{ type: 'lcov' }
]
reporters: [{ type: "lcov" }]
}
});
};
};

@@ -1,24 +0,21 @@

const webpack = require('webpack')
const webpack = require("webpack");
module.exports = function(config) {
config.set({
frameworks: ['mocha', 'sinon-chai'],
files: [
'https://player.vimeo.com/api/player.js',
'plugins/**/*.test.js'
],
browsers: ['Chrome'],
frameworks: ["mocha", "sinon-chai"],
files: ["https://player.vimeo.com/api/player.js", "plugins/**/*.test.js"],
browsers: ["Chrome"],
client: {
mocha: {
reporter: 'html'
reporter: "html"
}
},
webpack: {
devtool: 'inline-source-map'
devtool: "inline-source-map"
},
preprocessors: {
'plugins/**/*.test.js': ['webpack', 'sourcemap', 'babel']
"plugins/**/*.test.js": ["webpack", "sourcemap", "babel"]
},
reporters: ['mocha']
reporters: ["mocha"]
});
};
};
export default class VideoPlugin {
constructor(name, version) {
this.pluginName = name
this.pluginName = name;
}
track(event, properties) {

@@ -11,4 +11,4 @@ window.analytics.track(event, properties, {

}
})
});
}
}
}
{
"name": "@segment/analytics.js-video-plugins",
"version": "0.0.10",
"version": "0.1.0",
"description": "",
"scripts": {
"lint": "prettier --check *.js **/*.js **/**/*.js",
"fmt": "prettier --write *.js **/*.js **/**/*.js",
"build": "npx webpack",
"commit": "git cz",
"release": "./scripts/release.sh",

@@ -36,7 +37,8 @@ "test": "karma start",

"mocha": "^5.2.0",
"prettier": "^1.19.1",
"sinon": "^6.1.5",
"sinon-chai": "^3.2.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5"
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
},

@@ -43,0 +45,0 @@ "repository": {

@@ -1,4 +0,4 @@

import VimeoAnalytics from './vimeo'
import YouTubeAnalytics from './youtube'
import VimeoAnalytics from "./vimeo";
import YouTubeAnalytics from "./youtube";
export { VimeoAnalytics, YouTubeAnalytics }
export { VimeoAnalytics, YouTubeAnalytics };

@@ -1,15 +0,15 @@

import fetch from 'unfetch';
import Plugin from '../../lib/plugin'
import fetch from "unfetch";
import Plugin from "../../lib/plugin";
export default class VimeoAnalytics extends Plugin {
constructor(player, authToken) {
super('VimeoAnalytics')
this.authToken = authToken
this.player = player
super("VimeoAnalytics");
this.authToken = authToken;
this.player = player;
this.metadata = {
content: {},
playback: { videoPlayer: 'Vimeo' }
}
this.mostRecentHeartbeat = 0
this.isPaused = false
playback: { videoPlayer: "Vimeo" }
};
this.mostRecentHeartbeat = 0;
this.isPaused = false;
}

@@ -24,13 +24,14 @@

timeupdate: this.trackHeartbeat
}
};
for (let event in events) {
this.registerHandler(event, events[event])
this.registerHandler(event, events[event]);
}
this.player.getVideoId()
this.player
.getVideoId()
.then(id => {
this.retrieveMetadata({ id })
this.retrieveMetadata({ id });
})
.catch(console.error)
.catch(console.error);
}

@@ -41,5 +42,5 @@

this.player.on(event, data => {
this.updateMetadata(data)
handler.call(this, data)
})
this.updateMetadata(data);
handler.call(this, data);
});
}

@@ -49,7 +50,7 @@

if (this.isPaused) {
this.track('Video Playback Resumed', this.metadata.playback)
this.isPaused = false
this.track("Video Playback Resumed", this.metadata.playback);
this.isPaused = false;
} else {
this.track('Video Playback Started', this.metadata.playback)
this.track('Video Content Started', this.metadata.content)
this.track("Video Playback Started", this.metadata.playback);
this.track("Video Content Started", this.metadata.content);
}

@@ -59,4 +60,4 @@ }

trackEnded() {
this.track('Video Playback Completed', this.metadata.playback)
this.track('Video Content Completed', this.metadata.content)
this.track("Video Playback Completed", this.metadata.playback);
this.track("Video Content Completed", this.metadata.content);
}

@@ -68,4 +69,4 @@

trackHeartbeat() {
const mostRecentHeartbeat = this.mostRecentHeartbeat
const currentPosition = this.metadata.playback.position
const mostRecentHeartbeat = this.mostRecentHeartbeat;
const currentPosition = this.metadata.playback.position;
if (

@@ -75,4 +76,4 @@ currentPosition !== mostRecentHeartbeat &&

) {
this.track('Video Content Playing', this.metadata.content)
this.mostRecentHeartbeat = Math.floor(currentPosition)
this.track("Video Content Playing", this.metadata.content);
this.mostRecentHeartbeat = Math.floor(currentPosition);
}

@@ -82,4 +83,4 @@ }

trackPause() {
this.isPaused = true
this.track('Video Playback Paused', this.metadata.playback)
this.isPaused = true;
this.track("Video Playback Paused", this.metadata.playback);
}

@@ -90,3 +91,3 @@

return new Promise((resolve, reject) => {
const videoId = data.id
const videoId = data.id;
fetch(`https://api.vimeo.com/videos/${videoId}`, {

@@ -99,20 +100,19 @@ headers: {

if (res.ok) {
return res.json()
return res.json();
}
return reject(res)
return reject(res);
})
.then(json => {
this.metadata.content.title = json.name
this.metadata.content.description = json.description
this.metadata.content.publisher = json.user.name
this.metadata.content.title = json.name;
this.metadata.content.description = json.description;
this.metadata.content.publisher = json.user.name;
this.metadata.playback.position = 0
this.metadata.playback.totalLength = json.duration
this.metadata.playback.position = 0;
this.metadata.playback.totalLength = json.duration;
})
.catch(err => {
console.error('Request to Vimeo API Failed with: ', err)
return reject(err)
})
})
console.error("Request to Vimeo API Failed with: ", err);
return reject(err);
});
});
}

@@ -123,9 +123,12 @@

return new Promise((resolve, reject) => {
this.player.getVolume().then(volume => {
if (volume) this.metadata.playback.sound = volume * 100
this.metadata.playback.position = data.seconds
resolve()
}).catch(reject)
})
this.player
.getVolume()
.then(volume => {
if (volume) this.metadata.playback.sound = volume * 100;
this.metadata.playback.position = data.seconds;
resolve();
})
.catch(reject);
});
}
}
}

@@ -1,8 +0,8 @@

import VimeoAnalytics from '.'
import assert from 'assert'
const sandbox = sinon.createSandbox()
import VimeoAnalytics from ".";
import assert from "assert";
const sandbox = sinon.createSandbox();
describe('VimeoAnalytics', () => {
let player
let vimeoAnalytics
describe("VimeoAnalytics", () => {
let player;
let vimeoAnalytics;

@@ -12,36 +12,39 @@ beforeEach(() => {

track: sandbox.spy()
}
};
const html =
'<iframe id="video-player" src="https://player.vimeo.com/video/76979871" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'
document.body.insertAdjacentHTML('afterbegin', html)
const iframe = document.querySelector('iframe')
player = new Vimeo.Player(iframe, { autoplay: true, muted: true })
vimeoAnalytics = new VimeoAnalytics(player, 'b203643ead884f6b47b193c88f328c4a')
vimeoAnalytics.track = sandbox.spy()
vimeoAnalytics.initialize()
})
'<iframe id="video-player" src="https://player.vimeo.com/video/76979871" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>';
document.body.insertAdjacentHTML("afterbegin", html);
const iframe = document.querySelector("iframe");
player = new Vimeo.Player(iframe, { autoplay: true, muted: true });
vimeoAnalytics = new VimeoAnalytics(
player,
"b203643ead884f6b47b193c88f328c4a"
);
vimeoAnalytics.track = sandbox.spy();
vimeoAnalytics.initialize();
});
afterEach(() => {
player.unload().then(() => {
clearEnv('video-player')
sandbox.reset()
})
})
clearEnv("video-player");
sandbox.reset();
});
});
describe('registerHandler', () => {
it('should call player.on for any event with the given handler', () => {
sandbox.spy(player, 'on')
vimeoAnalytics.registerHandler('play', () => {})
player.on.should.have.been.calledOnce
describe("registerHandler", () => {
it("should call player.on for any event with the given handler", () => {
sandbox.spy(player, "on");
vimeoAnalytics.registerHandler("play", () => {});
player.on.should.have.been.calledOnce;
player.on.should.have.been.calledWithMatch(
sinon.match('play'),
sinon.match("play"),
sinon.match.func
)
})
})
);
});
});
describe('initialize()', () => {
it('should initiate all supported event handlers', () => {
sandbox.spy(vimeoAnalytics, 'registerHandler')
describe("initialize()", () => {
it("should initiate all supported event handlers", () => {
sandbox.spy(vimeoAnalytics, "registerHandler");

@@ -54,5 +57,5 @@ const events = {

timeupdate: vimeoAnalytics.trackHeartbeat
}
};
vimeoAnalytics.initialize()
vimeoAnalytics.initialize();

@@ -63,57 +66,65 @@ for (let event in events) {

events[event]
)
);
}
})
})
});
});
describe('Event Handlers', () => {
describe('retrieveMetadata()', () => {
it('should successfully retrieve metadata for a given video', () => {
describe("Event Handlers", () => {
describe("retrieveMetadata()", () => {
it("should successfully retrieve metadata for a given video", () => {
vimeoAnalytics.retrieveMetadata({ id: 76979871 }).then(() => {
assert.equal(vimeoAnalytics.metadata.content.title, 'The New Vimeo Player (You Know, For Videos)')
assert(typeof vimeoAnalytics.metadata.content.description === 'string')
assert.equal(vimeoAnalytics.metadata.content.publisher, 'Vimeo Staff')
assert.equal(vimeoAnalytics.metadata.playback.videoPlayer, 'Vimeo')
assert.equal(vimeoAnalytics.metadata.playback.position, 0)
assert.equal(vimeoAnalytics.metadata.playback.totalLength, 62)
})
})
})
assert.equal(
vimeoAnalytics.metadata.content.title,
"The New Vimeo Player (You Know, For Videos)"
);
assert(
typeof vimeoAnalytics.metadata.content.description === "string"
);
assert.equal(
vimeoAnalytics.metadata.content.publisher,
"Vimeo Staff"
);
assert.equal(vimeoAnalytics.metadata.playback.videoPlayer, "Vimeo");
assert.equal(vimeoAnalytics.metadata.playback.position, 0);
assert.equal(vimeoAnalytics.metadata.playback.totalLength, 62);
});
});
});
describe('trackPlay()', () => {
it('should track play events as Video Playback Started events', () => {
vimeoAnalytics.trackPlay()
describe("trackPlay()", () => {
it("should track play events as Video Playback Started events", () => {
vimeoAnalytics.trackPlay();
vimeoAnalytics.track.firstCall.should.have.been.calledWith(
'Video Playback Started',
"Video Playback Started",
vimeoAnalytics.metadata.playback
)
})
);
});
it('should not track Video Playback Started events if playback is paused', () => {
vimeoAnalytics.isPaused = true
vimeoAnalytics.trackPlay()
it("should not track Video Playback Started events if playback is paused", () => {
vimeoAnalytics.isPaused = true;
vimeoAnalytics.trackPlay();
vimeoAnalytics.track.should.not.have.been.calledWith(
'Video Playback Started'
)
})
"Video Playback Started"
);
});
it('should track play events as Video Playback Resumed events if the video was previously paused', () => {
vimeoAnalytics.isPaused = true
vimeoAnalytics.trackPlay()
it("should track play events as Video Playback Resumed events if the video was previously paused", () => {
vimeoAnalytics.isPaused = true;
vimeoAnalytics.trackPlay();
vimeoAnalytics.track.should.have.been.calledWith(
'Video Playback Resumed',
"Video Playback Resumed",
vimeoAnalytics.metadata.playback
)
})
);
});
it('should track play events as Video Content Started events', () => {
vimeoAnalytics.trackPlay()
it("should track play events as Video Content Started events", () => {
vimeoAnalytics.trackPlay();
vimeoAnalytics.track.should.have.been.calledWith(
'Video Content Started',
"Video Content Started",
vimeoAnalytics.metadata.content
)
})
})
);
});
});
describe('trackPause()', () => {
describe("trackPause()", () => {
const pauseData = {

@@ -123,27 +134,27 @@ duration: 61.857,

seconds: 3.034
}
};
it('should set isPaused to true', () => {
it("should set isPaused to true", () => {
vimeoAnalytics.updateMetadata(pauseData).then(() => {
vimeoAnalytics.trackPause()
assert(vimeoAnalytics.isPaused === true)
})
})
vimeoAnalytics.trackPause();
assert(vimeoAnalytics.isPaused === true);
});
});
it('should track pause events as Video Playback Paused events', () => {
it("should track pause events as Video Playback Paused events", () => {
vimeoAnalytics.updateMetadata(pauseData).then(() => {
vimeoAnalytics.trackPause()
vimeoAnalytics.track.should.have.been.calledOnce
vimeoAnalytics.trackPause();
vimeoAnalytics.track.should.have.been.calledOnce;
vimeoAnalytics.track.should.have.been.calledWith(
'Video Playback Paused',
"Video Playback Paused",
vimeoAnalytics.metadata.playback
)
})
})
})
);
});
});
});
describe('trackHeartbeat()', () => {
describe("trackHeartbeat()", () => {
const getData = seconds => {
seconds = seconds || 0
const duration = 61.857
seconds = seconds || 0;
const duration = 61.857;
return {

@@ -153,59 +164,61 @@ percent: seconds / duration,

seconds
}
}
};
};
it('should not track a heartbeat event if it has not been 10 seconds since the last heartbeat', () => {
vimeoAnalytics.mostRecentHeartbeat = 10
vimeoAnalytics.trackHeartbeat(getData(15.02))
analytics.track.should.not.have.been.called
})
it("should not track a heartbeat event if it has not been 10 seconds since the last heartbeat", () => {
vimeoAnalytics.mostRecentHeartbeat = 10;
vimeoAnalytics.trackHeartbeat(getData(15.02));
analytics.track.should.not.have.been.called;
});
it('should track a single heartbeat event at 10 second intervals', () => {
vimeoAnalytics.mostRecentHeartbeat = 10
vimeoAnalytics.updateMetadata(getData(20.03)).then(() => {
vimeoAnalytics.trackHeartbeat()
return vimeoAnalytics.updateMetadata(getData(22.07))
})
it("should track a single heartbeat event at 10 second intervals", () => {
vimeoAnalytics.mostRecentHeartbeat = 10;
vimeoAnalytics
.updateMetadata(getData(20.03))
.then(() => {
vimeoAnalytics.trackHeartbeat()
vimeoAnalytics.track.should.have.been.calledOnce
vimeoAnalytics.trackHeartbeat();
return vimeoAnalytics.updateMetadata(getData(22.07));
})
.then(() => {
vimeoAnalytics.trackHeartbeat();
vimeoAnalytics.track.should.have.been.calledOnce;
vimeoAnalytics.track.should.have.been.calledWith(
'Video Content Playing',
"Video Content Playing",
vimeoAnalytics.metadata.content
)
})
})
);
});
});
it('should update the mostRecentHeartbeat flag when a new heartbeat is triggered', () => {
vimeoAnalytics.mostRecentHeartbeat = 20
it("should update the mostRecentHeartbeat flag when a new heartbeat is triggered", () => {
vimeoAnalytics.mostRecentHeartbeat = 20;
vimeoAnalytics.updateMetadata(getData(30)).then(() => {
vimeoAnalytics.trackHeartbeat()
assert.equal(vimeoAnalytics.mostRecentHeartbeat, 30)
})
})
})
vimeoAnalytics.trackHeartbeat();
assert.equal(vimeoAnalytics.mostRecentHeartbeat, 30);
});
});
});
describe('trackEnded()', () => {
it('should track end events as Video Playback Completed events', () => {
vimeoAnalytics.trackEnded()
describe("trackEnded()", () => {
it("should track end events as Video Playback Completed events", () => {
vimeoAnalytics.trackEnded();
vimeoAnalytics.track.should.have.been.calledWith(
'Video Playback Completed',
"Video Playback Completed",
vimeoAnalytics.metadata.playback
)
})
);
});
it('should track end events as Video Content Completed events', () => {
vimeoAnalytics.trackEnded()
it("should track end events as Video Content Completed events", () => {
vimeoAnalytics.trackEnded();
const contentCompletedSpy = vimeoAnalytics.track.withArgs(
'Video Content Completed',
"Video Content Completed",
vimeoAnalytics.metadata.content
)
contentCompletedSpy.should.have.been.calledOnce
})
})
})
})
);
contentCompletedSpy.should.have.been.calledOnce;
});
});
});
});
function clearEnv(id) {
document.getElementById(id).remove()
document.getElementById(id).remove();
}

@@ -1,22 +0,29 @@

import fetch from 'unfetch'
import VideoPlugin from '../../lib/plugin'
const SEEK_THRESHOLD = 2000
import fetch from "unfetch";
import VideoPlugin from "../../lib/plugin";
const SEEK_THRESHOLD = 2000;
export default class YoutubeAnalytics extends VideoPlugin {
export default class YouTubeAnalytics extends VideoPlugin {
/**
* Creates a YouTube video plugin to track events directly from the player.
*
* @param {YT.Player} player YouTube IFrame Player (docs: https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player).
* @param {String} apiKey Youtube Data API key (docs: https://developers.google.com/youtube/registering_an_application).
*/
constructor(player, apiKey) {
super('YoutubeAnalytics')
this.player = player
this.apiKey = apiKey
this.playerLoaded = false
this.playbackStarted = false
this.contentStarted = false
this.isPaused = false
this.isBuffering = false
this.isSeeking = false
this.lastRecordedTime = { // updated every event
super("YoutubeAnalytics");
this.player = player;
this.apiKey = apiKey;
this.playerLoaded = false;
this.playbackStarted = false;
this.contentStarted = false;
this.isPaused = false;
this.isBuffering = false;
this.isSeeking = false;
this.lastRecordedTime = {
// updated every event
timeReported: Date.now(),
timeElapsed: 0.0
}
this.metadata = [{ playback: { video_player: 'youtube' }, content: {} }]
this.playlistIndex = 0
};
this.metadata = [{ playback: { video_player: "youtube" }, content: {} }];
this.playlistIndex = 0;
}

@@ -26,13 +33,15 @@

// Youtube API requires listeners to exist as top-level props on window object
window.segmentYoutubeOnStateChange = this.onPlayerStateChange.bind(this)
window.segmentYoutubeOnReady = this.onPlayerReady.bind(this)
window.segmentYoutubeOnStateChange = this.onPlayerStateChange.bind(this);
window.segmentYoutubeOnReady = this.onPlayerReady.bind(this);
const { player } = this
player.addEventListener('onReady', 'segmentYoutubeOnReady')
player.addEventListener('onStateChange', 'segmentYoutubeOnStateChange')
this.player.addEventListener("onReady", "segmentYoutubeOnReady");
this.player.addEventListener(
"onStateChange",
"segmentYoutubeOnStateChange"
);
}
onPlayerReady(event) { // this fires when the player html element loads
this.retrieveMetadata()
onPlayerReady(event) {
// this fires when the player html element loads
this.retrieveMetadata();
}

@@ -42,32 +51,37 @@

onPlayerStateChange(event) {
var self = this
const currentTime = this.player.getCurrentTime()
const currentTime = this.player.getCurrentTime();
if (this.metadata[this.playlistIndex]) {
this.metadata[this.playlistIndex].playback.position = this.metadata[this.playlistIndex].content.position = currentTime
this.metadata[this.playlistIndex].playback.quality = this.player.getPlaybackQuality()
this.metadata[this.playlistIndex].playback.sound = this.player.getVolume()
this.metadata[this.playlistIndex].playback.position = this.metadata[
this.playlistIndex
].content.position = currentTime;
this.metadata[
this.playlistIndex
].playback.quality = this.player.getPlaybackQuality();
this.metadata[this.playlistIndex].playback.sound = this.player.isMuted()
? 0
: this.player.getVolume();
}
switch (event.data) {
case -1: // this fires when a video or playlist has loaded
if (this.playerLoaded) break
this.retrieveMetadata()
this.playerLoaded = true
break
case -1: // this fires when a video or playlist has loaded
if (this.playerLoaded) break;
this.retrieveMetadata();
this.playerLoaded = true;
break;
case YT.PlayerState.BUFFERING:
this.handleBuffer()
break
this.handleBuffer();
break;
case YT.PlayerState.PLAYING:
this.handlePlay()
break
this.handlePlay();
break;
case YT.PlayerState.PAUSED:
this.handlePause()
break
this.handlePause();
break;
case YT.PlayerState.ENDED:
this.handleEnd()
break
this.handleEnd();
break;
}

@@ -77,28 +91,38 @@

timeReported: Date.now(),
timeElapsed: this.player.getCurrentTime()*1000.0
}
timeElapsed: this.player.getCurrentTime() * 1000.0
};
}
// retrieve metadata from Youtube Data API using user's API Key
/**
* Retrieves the video metadata from Youtube Data API using user's API Key
* docs: https://developers.google.com/youtube/v3/docs/videos/list
*
* @returns {Promise} A promise that resolves in a metadata object from the video
* contained in the player.
*/
retrieveMetadata() {
return new Promise((resolve, reject) => {
let videoData = this.player.getVideoData()
console.log(this.player.getPlaylist())
let playlist = this.player.getPlaylist() || [videoData.video_id]
let videoData = this.player.getVideoData();
let playlist = this.player.getPlaylist() || [videoData.video_id];
const videoIds = playlist.join()
const videoIds = playlist.join();
fetch(`https://www.googleapis.com/youtube/v3/videos?id=${videoIds}&part=snippet,contentDetails&key=${this.apiKey}`)
fetch(
`https://www.googleapis.com/youtube/v3/videos?id=${videoIds}&part=snippet,contentDetails&key=${this.apiKey}`
)
.then(res => {
if (!res.ok) {
const err = new Error('Segment request to Youtube API failed (likely due to a bad API Key. Events will still be sent but will not contain video metadata)')
err.response = res
throw err
const err = new Error(
"Segment request to Youtube API failed (likely due to a bad API Key. Events will still be sent but will not contain video metadata)"
);
err.response = res;
throw err;
}
return res.json()
}).then(json => {
this.metadata = []
let total_length = 0
for (var i=0; i< playlist.length; i++) {
const videoInfo = json.items[i]
return res.json();
})
.then(json => {
this.metadata = [];
let total_length = 0;
for (var i = 0; i < playlist.length; i++) {
const videoInfo = json.items[i];
this.metadata.push({

@@ -110,29 +134,36 @@ content: {

channel: videoInfo.snippet.channelTitle,
airdate: videoInfo.snippet.publishedAt,
airdate: videoInfo.snippet.publishedAt
}
})
total_length += YTDurationToSeconds(videoInfo.contentDetails.duration)
});
total_length += YTDurationToSeconds(
videoInfo.contentDetails.duration
);
}
for (var i=0; i<playlist.length; i++) {
for (var i = 0; i < playlist.length; i++) {
this.metadata[i].playback = {
total_length,
video_player: 'youtube'
}
video_player: "youtube"
};
}
resolve()
resolve();
})
.catch(err => {
this.metadata = playlist.map((item) => { return { playback: { video_player: 'youtube' }, content: {} } })
reject(err)
})
})
this.metadata = playlist.map(item => {
return { playback: { video_player: "youtube" }, content: {} };
});
reject(err);
});
});
}
handleBuffer() {
var seekDetected = this.determineSeek()
var seekDetected = this.determineSeek();
if (!this.playbackStarted) {
this.playbackStarted = true
this.track('Video Playback Started', this.metadata[this.playlistIndex].playback)
this.playbackStarted = true;
this.track(
"Video Playback Started",
this.metadata[this.playlistIndex].playback
);
}

@@ -142,4 +173,7 @@

if (seekDetected && !this.isSeeking) {
this.isSeeking = true
this.track('Video Playback Seek Started', this.metadata[this.playlistIndex].playback)
this.isSeeking = true;
this.track(
"Video Playback Seek Started",
this.metadata[this.playlistIndex].playback
);
}

@@ -149,18 +183,38 @@

if (this.isSeeking) {
this.track('Video Playback Seek Completed', this.metadata[this.playlistIndex].playback)
this.isSeeking = false
this.track(
"Video Playback Seek Completed",
this.metadata[this.playlistIndex].playback
);
this.isSeeking = false;
}
// user clicked next video button (not possible in single video playback)
const playlist = this.player.getPlaylist()
if (playlist && this.player.getCurrentTime() === 0 && this.player.getPlaylistIndex() !== this.playlistIndex) {
this.contentStarted = false
if (this.playlistIndex === playlist.length - 1 && this.player.getPlaylistIndex() === 0) { // user skipped to end of last video in playlist
this.track('Video Playback Completed', this.metadata[this.player.getPlaylistIndex()].playback)
this.track('Video Playback Started', this.metadata[this.player.getPlaylistIndex()].playback) // playlist automatically starts from beginning
const playlist = this.player.getPlaylist();
if (
playlist &&
this.player.getCurrentTime() === 0 &&
this.player.getPlaylistIndex() !== this.playlistIndex
) {
this.contentStarted = false;
if (
this.playlistIndex === playlist.length - 1 &&
this.player.getPlaylistIndex() === 0
) {
// user skipped to end of last video in playlist
this.track(
"Video Playback Completed",
this.metadata[this.player.getPlaylistIndex()].playback
);
this.track(
"Video Playback Started",
this.metadata[this.player.getPlaylistIndex()].playback
); // playlist automatically starts from beginning
}
}
this.track('Video Playback Buffer Started', this.metadata[this.playlistIndex].playback)
this.isBuffering = true
this.track(
"Video Playback Buffer Started",
this.metadata[this.playlistIndex].playback
);
this.isBuffering = true;
}

@@ -170,16 +224,25 @@

if (!this.contentStarted) {
this.playlistIndex = this.player.getPlaylistIndex() // will return -1 if the player has a singular video instead of a playlist
if (this.playlistIndex === -1) this.playlistIndex = 0
this.track('Video Content Started', this.metadata[this.playlistIndex].content)
this.contentStarted = true
this.playlistIndex = this.player.getPlaylistIndex(); // will return -1 if the player has a singular video instead of a playlist
if (this.playlistIndex === -1) this.playlistIndex = 0;
this.track(
"Video Content Started",
this.metadata[this.playlistIndex].content
);
this.contentStarted = true;
}
if (this.isBuffering) {
this.track('Video Playback Buffer Completed', this.metadata[this.playlistIndex].playback)
this.isBuffering = false
this.track(
"Video Playback Buffer Completed",
this.metadata[this.playlistIndex].playback
);
this.isBuffering = false;
}
if (this.isPaused) {
this.track('Video Playback Resumed', this.metadata[this.playlistIndex].playback)
this.isPaused = false
this.track(
"Video Playback Resumed",
this.metadata[this.playlistIndex].playback
);
this.isPaused = false;
}

@@ -189,7 +252,10 @@ }

handlePause() {
const seekDetected = this.determineSeek()
const seekDetected = this.determineSeek();
// user seeked while video was paused, it buffered, and then finished buffering
if (this.isBuffering) {
this.track('Video Playback Buffer Completed', this.metadata[this.playlistIndex].playback)
this.isBuffering = false
this.track(
"Video Playback Buffer Completed",
this.metadata[this.playlistIndex].playback
);
this.isBuffering = false;
}

@@ -200,9 +266,15 @@

if (seekDetected) {
this.track('Video Playback Seek Started', this.metadata[this.playlistIndex].playback)
this.isSeeking = true
this.track(
"Video Playback Seek Started",
this.metadata[this.playlistIndex].playback
);
this.isSeeking = true;
}
// user clicked pause while video was playing
else {
this.track('Video Playback Paused', this.metadata[this.playlistIndex].playback)
this.isPaused = true
this.track(
"Video Playback Paused",
this.metadata[this.playlistIndex].playback
);
this.isPaused = true;
}

@@ -213,11 +285,20 @@ }

handleEnd() {
this.track('Video Content Completed', this.metadata[this.playlistIndex].content)
this.contentStarted = false
this.track(
"Video Content Completed",
this.metadata[this.playlistIndex].content
);
this.contentStarted = false;
const playlistIndex = this.player.getPlaylistIndex()
const playlist = this.player.getPlaylist()
const playlistIndex = this.player.getPlaylistIndex();
const playlist = this.player.getPlaylist();
if ((playlist && playlistIndex === playlist.length-1) || (playlistIndex === -1)) {
this.track('Video Playback Completed', this.metadata[this.playlistIndex].playback)
this.playbackStarted = false
if (
(playlist && playlistIndex === playlist.length - 1) ||
playlistIndex === -1
) {
this.track(
"Video Playback Completed",
this.metadata[this.playlistIndex].playback
);
this.playbackStarted = false;
}

@@ -228,6 +309,10 @@ }

determineSeek() {
const expectedTimeElapsed = (this.isPaused || this.isBuffering) ? 0 : Date.now() - this.lastRecordedTime.timeReported
const actualTimeElapsed = this.player.getCurrentTime()*1000.0 - this.lastRecordedTime.timeElapsed
const expectedTimeElapsed =
this.isPaused || this.isBuffering
? 0
: Date.now() - this.lastRecordedTime.timeReported;
const actualTimeElapsed =
this.player.getCurrentTime() * 1000.0 - this.lastRecordedTime.timeElapsed;
return Math.abs(expectedTimeElapsed - actualTimeElapsed) > SEEK_THRESHOLD // if the diff btwn the 2 is > the threshold we can reasonably assume a seek has occurred
return Math.abs(expectedTimeElapsed - actualTimeElapsed) > SEEK_THRESHOLD; // if the diff btwn the 2 is > the threshold we can reasonably assume a seek has occurred
}

@@ -242,11 +327,11 @@ }

if (x != null) {
return x.replace(/\D/, '');
return x.replace(/\D/, "");
}
});
var hours = (parseInt(match[0]) || 0);
var minutes = (parseInt(match[1]) || 0);
var seconds = (parseInt(match[2]) || 0);
var hours = parseInt(match[0]) || 0;
var minutes = parseInt(match[1]) || 0;
var seconds = parseInt(match[2]) || 0;
return hours * 3600 + minutes * 60 + seconds;
}

@@ -1,104 +0,132 @@

import YoutubeAnalytics from '.'
import assert from 'assert'
import fetchMock from 'fetch-mock'
const SEEK_THRESHOLD = 2000
import YouTubeAnalytics from ".";
import assert from "assert";
import fetchMock from "fetch-mock";
const SEEK_THRESHOLD = 2000;
const sampleSingleVideoApiOutput = {
body: {
items: [ { kind: 'youtube#video',
etag: '"XI7nbFXulYBIpL0ayR_gDh3eu1k/l8T3Ps_vgKQV_OC2bHkLGQ44KAk"',
id: 'M7lc1UVf-VE',
snippet:
{ publishedAt: '2013-04-10T17:25:04.000Z',
channelId: 'UC_x5XG1OV2P6uZZ5FSM9Ttw',
title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.',
channelTitle: 'Google Developers',
tags:
[ 'youtubedataapi',
'youtube',
'youtubeplayerapi',
'youtubeapi',
'video',
'Mountain View',
'gdl' ],
categoryId: '28',
liveBroadcastContent: 'none',
localized:
{ title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.' },
defaultAudioLanguage: 'en' },
contentDetails:
{ duration: 'PT22M24S',
dimension: '2d',
definition: 'hd',
caption: 'true',
licensedContent: false,
projection: 'rectangular' } } ]
items: [
{
kind: "youtube#video",
etag: '"XI7nbFXulYBIpL0ayR_gDh3eu1k/l8T3Ps_vgKQV_OC2bHkLGQ44KAk"',
id: "M7lc1UVf-VE",
snippet: {
publishedAt: "2013-04-10T17:25:04.000Z",
channelId: "UC_x5XG1OV2P6uZZ5FSM9Ttw",
title: "YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.",
channelTitle: "Google Developers",
tags: [
"youtubedataapi",
"youtube",
"youtubeplayerapi",
"youtubeapi",
"video",
"Mountain View",
"gdl"
],
categoryId: "28",
liveBroadcastContent: "none",
localized: {
title: "YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player."
},
defaultAudioLanguage: "en"
},
contentDetails: {
duration: "PT22M24S",
dimension: "2d",
definition: "hd",
caption: "true",
licensedContent: false,
projection: "rectangular"
}
}
]
}
}
};
const samplePlaylistApiOutput = {
body: {
items: [ { kind: 'youtube#video',
etag: '"XI7nbFXulYBIpL0ayR_gDh3eu1k/l8T3Ps_vgKQV_OC2bHkLGQ44KAk"',
id: 'M7lc1UVf-VE',
snippet:
{ publishedAt: '2013-04-10T17:25:04.000Z',
channelId: 'UC_x5XG1OV2P6uZZ5FSM9Ttw',
title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.',
channelTitle: 'Google Developers',
tags:
[ 'youtubedataapi',
'youtube',
'youtubeplayerapi',
'youtubeapi',
'video',
'Mountain View',
'gdl' ],
categoryId: '28',
liveBroadcastContent: 'none',
localized:
{ title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.' },
defaultAudioLanguage: 'en' },
contentDetails:
{ duration: 'PT22M24S',
dimension: '2d',
definition: 'hd',
caption: 'true',
licensedContent: false,
projection: 'rectangular' } },
{ kind: 'youtube#video',
etag: '"XI7nbFXulYBIpL0ayR_gDh3eu1k/WRztWOH0sUonKk2lGPf4GfmmTjk"',
id: 'izoct69J4v8',
snippet:
{ publishedAt: '2017-10-26T21:49:55.000Z',
channelId: 'UCTJmlYi-adThOQaXuuNYsgQ',
title: 'Introducing Personas, by Segment',
description: 'Derrick thinks Kicks Central controls the weather. Turns out, its just the corporate office using Personas by Segment to send the right offers to the right people, at exactly the right time. Learn more at segment.com/personas.',
channelTitle: 'Segment',
tags:
[ 'segment',
'personas',
'identity resolution',
'audience building',
'custom segments',
'marketing automation',
'api' ],
categoryId: '27',
liveBroadcastContent: 'none',
localized:
{ title: 'Introducing Personas, by Segment',
description: 'Derrick thinks Kicks Central controls the weather. Turns out, its just the corporate office using Personas by Segment to send the right offers to the right people, at exactly the right time. Learn more at segment.com/personas.' } },
contentDetails:
{ duration: 'PT1M30S',
dimension: '2d',
definition: 'hd',
caption: 'false',
licensedContent: false,
projection: 'rectangular' } } ]
items: [
{
kind: "youtube#video",
etag: '"XI7nbFXulYBIpL0ayR_gDh3eu1k/l8T3Ps_vgKQV_OC2bHkLGQ44KAk"',
id: "M7lc1UVf-VE",
snippet: {
publishedAt: "2013-04-10T17:25:04.000Z",
channelId: "UC_x5XG1OV2P6uZZ5FSM9Ttw",
title: "YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.",
channelTitle: "Google Developers",
tags: [
"youtubedataapi",
"youtube",
"youtubeplayerapi",
"youtubeapi",
"video",
"Mountain View",
"gdl"
],
categoryId: "28",
liveBroadcastContent: "none",
localized: {
title: "YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player."
},
defaultAudioLanguage: "en"
},
contentDetails: {
duration: "PT22M24S",
dimension: "2d",
definition: "hd",
caption: "true",
licensedContent: false,
projection: "rectangular"
}
},
{
kind: "youtube#video",
etag: '"XI7nbFXulYBIpL0ayR_gDh3eu1k/WRztWOH0sUonKk2lGPf4GfmmTjk"',
id: "izoct69J4v8",
snippet: {
publishedAt: "2017-10-26T21:49:55.000Z",
channelId: "UCTJmlYi-adThOQaXuuNYsgQ",
title: "Introducing Personas, by Segment",
description:
"Derrick thinks Kicks Central controls the weather. Turns out, its just the corporate office using Personas by Segment to send the right offers to the right people, at exactly the right time. Learn more at segment.com/personas.",
channelTitle: "Segment",
tags: [
"segment",
"personas",
"identity resolution",
"audience building",
"custom segments",
"marketing automation",
"api"
],
categoryId: "27",
liveBroadcastContent: "none",
localized: {
title: "Introducing Personas, by Segment",
description:
"Derrick thinks Kicks Central controls the weather. Turns out, its just the corporate office using Personas by Segment to send the right offers to the right people, at exactly the right time. Learn more at segment.com/personas."
}
},
contentDetails: {
duration: "PT1M30S",
dimension: "2d",
definition: "hd",
caption: "false",
licensedContent: false,
projection: "rectangular"
}
}
]
}
}
};

@@ -112,352 +140,478 @@ window.YT = {

}
}
};
describe('YoutubeAnalytics', () => {
let youtubeAnalytics
let playerTime
let playbackQuality
let volume
let playlistIndex
let player
let playlistId
let videoId = 'M7lc1UVf'
let playlist
describe("YoutubeAnalytics", () => {
let youTubeAnalytics;
let playerTime;
let playbackQuality;
let volume;
let playlistIndex;
let player;
let playlistId;
let videoId = "M7lc1UVf";
let playlist;
let muted;
beforeEach(() => {
playerTime = 0
playlistId = null
volume = 0
playbackQuality = 0
playerTime = 0;
playlistId = null;
volume = 47;
muted = false;
playbackQuality = 0;
// stub out player methods
player = {
addEventListener: function () {},
addEventListener: function() {},
getVideoData: function() {
return { video_id: videoId}
return { video_id: videoId };
},
getPlaylist: function() { return playlist },
getCurrentTime: function() { return playerTime },
getPlaybackQuality: function() { return playbackQuality },
getVolume: function() { return volume },
getPlaylistIndex: function() { return isNaN(playlistIndex) ? -1 : playlistIndex },
getPlaylistId: function() { return playlistId },
getPlaylist: function() {
return playlist;
},
getCurrentTime: function() {
return playerTime;
},
getPlaybackQuality: function() {
return playbackQuality;
},
getVolume: function() {
return volume;
},
isMuted: function() {
return muted;
},
getPlaylistIndex: function() {
return isNaN(playlistIndex) ? -1 : playlistIndex;
},
getPlaylistId: function() {
return playlistId;
},
cuePlaylist: function() {},
cueVideoById: function() {}
}
};
window.analytics = {
track: sinon.spy()
}
youtubeAnalytics = new YoutubeAnalytics(player, 'AIzaSyDxtBpYa_IAWudnLW0P79sBF-cIzBUOUpQ')
})
};
youTubeAnalytics = new YouTubeAnalytics(
player,
"AIzaSyDxtBpYa_IAWudnLW0P79sBF-cIzBUOUpQ"
);
});
describe('initialize()', () => {
it('should put listeners on the window and add the listeners to the player object', () => {
const stub = sinon.stub(player, 'addEventListener')
describe("initialize()", () => {
it("should put listeners on the window and add the listeners to the player object", () => {
const stub = sinon.stub(player, "addEventListener");
youtubeAnalytics.initialize()
youTubeAnalytics.initialize();
assert(global.segmentYoutubeOnStateChange)
assert(global.segmentYoutubeOnReady)
sinon.assert.calledWith(stub, 'onStateChange', 'segmentYoutubeOnStateChange')
sinon.assert.calledWith(stub, 'onReady', 'segmentYoutubeOnReady')
})
})
assert(global.segmentYoutubeOnStateChange);
assert(global.segmentYoutubeOnReady);
sinon.assert.calledWith(
stub,
"onStateChange",
"segmentYoutubeOnStateChange"
);
sinon.assert.calledWith(stub, "onReady", "segmentYoutubeOnReady");
});
});
describe('onPlayerReady', () => {
it('should call retrieveMetdata', () => {
const stub = sinon.stub(youtubeAnalytics, 'retrieveMetadata')
youtubeAnalytics.onPlayerReady()
describe("onPlayerReady", () => {
it("should call retrieveMetdata", () => {
const stub = sinon.stub(youTubeAnalytics, "retrieveMetadata");
youTubeAnalytics.onPlayerReady();
sinon.assert.called(stub)
})
})
sinon.assert.called(stub);
});
});
describe('Metadata Retrieval', () => {
describe("Metadata Retrieval", () => {
afterEach(() => {
fetchMock.restore()
})
it('should successfully retrieve metadata for a given video', () => {
fetchMock.mock('begin:https://www.googleapis.com', sampleSingleVideoApiOutput)
youtubeAnalytics.retrieveMetadata().then(() => {
assert.deepEqual(youtubeAnalytics.metadata,
[
{ content:
{ title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.',
keywords:
[ 'youtubedataapi',
'youtube',
'youtubeplayerapi',
'youtubeapi',
'video',
'Mountain View',
'gdl' ],
channel: 'Google Developers',
airdate: '2013-04-10T17:25:04.000Z'
},
playback: { total_length: 1344, video_player: 'youtube' }
}
])
})
})
fetchMock.restore();
});
it("should successfully retrieve metadata for a given video", () => {
fetchMock.mock(
"begin:https://www.googleapis.com",
sampleSingleVideoApiOutput
);
youTubeAnalytics.retrieveMetadata().then(() => {
assert.deepEqual(youTubeAnalytics.metadata, [
{
content: {
title:
"YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.",
keywords: [
"youtubedataapi",
"youtube",
"youtubeplayerapi",
"youtubeapi",
"video",
"Mountain View",
"gdl"
],
channel: "Google Developers",
airdate: "2013-04-10T17:25:04.000Z"
},
playback: { total_length: 1344, video_player: "youtube" }
}
]);
});
});
it('should successfully retrieve metadata for a playlist of multiple videos', () => {
fetchMock.mock('begin:https://www.googleapis.com', samplePlaylistApiOutput)
youtubeAnalytics.player.getPlaylist = function() { return ['M7lc1UVf-VE', 'izoct69J4v8'] }
youtubeAnalytics.retrieveMetadata().then(() => {
assert.deepEqual(youtubeAnalytics.metadata,
[ { content:
{ title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.',
keywords:
[ 'youtubedataapi',
'youtube',
'youtubeplayerapi',
'youtubeapi',
'video',
'Mountain View',
'gdl' ],
channel: 'Google Developers',
airdate: '2013-04-10T17:25:04.000Z' },
playback: { total_length: 1434, video_player: 'youtube' } },
{ content:
{ title: 'Introducing Personas, by Segment',
description: 'Derrick thinks Kicks Central controls the weather. Turns out, its just the corporate office using Personas by Segment to send the right offers to the right people, at exactly the right time. Learn more at segment.com/personas.',
keywords:
[ 'segment',
'personas',
'identity resolution',
'audience building',
'custom segments',
'marketing automation',
'api' ],
channel: 'Segment',
airdate: '2017-10-26T21:49:55.000Z' },
playback: { total_length: 1434, video_player: 'youtube' } } ] )
})
})
it("should successfully retrieve metadata for a playlist of multiple videos", () => {
fetchMock.mock(
"begin:https://www.googleapis.com",
samplePlaylistApiOutput
);
youTubeAnalytics.player.getPlaylist = function() {
return ["M7lc1UVf-VE", "izoct69J4v8"];
};
youTubeAnalytics.retrieveMetadata().then(() => {
assert.deepEqual(youTubeAnalytics.metadata, [
{
content: {
title:
"YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.",
keywords: [
"youtubedataapi",
"youtube",
"youtubeplayerapi",
"youtubeapi",
"video",
"Mountain View",
"gdl"
],
channel: "Google Developers",
airdate: "2013-04-10T17:25:04.000Z"
},
playback: { total_length: 1434, video_player: "youtube" }
},
{
content: {
title: "Introducing Personas, by Segment",
description:
"Derrick thinks Kicks Central controls the weather. Turns out, its just the corporate office using Personas by Segment to send the right offers to the right people, at exactly the right time. Learn more at segment.com/personas.",
keywords: [
"segment",
"personas",
"identity resolution",
"audience building",
"custom segments",
"marketing automation",
"api"
],
channel: "Segment",
airdate: "2017-10-26T21:49:55.000Z"
},
playback: { total_length: 1434, video_player: "youtube" }
}
]);
});
});
it('should not fail on API request failure', () => {
fetchMock.mock('begin:https://www.googleapis.com', 400)
youtubeAnalytics.retrieveMetadata().then(() => {
assert.deepEqual(youtubeAnalytics.metadata, [{ playback: { video_player: 'youtube' }, content: {} }])
})
})
})
it("should not fail on API request failure", () => {
fetchMock.mock("begin:https://www.googleapis.com", 400);
youTubeAnalytics.retrieveMetadata().then(() => {
assert.deepEqual(youTubeAnalytics.metadata, [
{ playback: { video_player: "youtube" }, content: {} }
]);
});
});
});
describe('onPlayerStateChange', () => {
describe("onPlayerStateChange", () => {
beforeEach(() => {
youtubeAnalytics.initialize()
youtubeAnalytics.metadata = [{ playback: {}, content: {} }]
})
it('should update playback metadata', () => {
playerTime = 4
playbackQuality = 3
volume = 2
window.segmentYoutubeOnStateChange({})
youTubeAnalytics.initialize();
youTubeAnalytics.metadata = [{ playback: {}, content: {} }];
});
it("should update playback metadata", () => {
playerTime = 4;
playbackQuality = 3;
volume = 2;
window.segmentYoutubeOnStateChange({});
assert.deepEqual(youtubeAnalytics.metadata[0].playback, { position: playerTime, quality: playbackQuality, sound: volume})
})
assert.deepEqual(youTubeAnalytics.metadata[0].playback, {
position: playerTime,
quality: playbackQuality,
sound: volume
});
});
it("should check for muted videos", () => {
playerTime = 4;
playbackQuality = 3;
volume = 100;
muted = true;
window.segmentYoutubeOnStateChange({});
assert.deepEqual(youTubeAnalytics.metadata[0].playback, {
position: playerTime,
quality: playbackQuality,
sound: 0
});
});
it('should update "last recorded time"', () => {
playerTime = 4
playerTime = 4;
const timeBeforeFunctionRun = Date.now()
const timeBeforeFunctionRun = Date.now();
window.segmentYoutubeOnStateChange({})
window.segmentYoutubeOnStateChange({});
// impossible to know what Date.now was at the time the var was set so settle for checking that the updated value
// is for a later date than Date.now before the function was run
assert(youtubeAnalytics.lastRecordedTime.timeReported >= timeBeforeFunctionRun)
assert.equal(youtubeAnalytics.lastRecordedTime.timeElapsed, playerTime * 1000.0)
})
})
assert(
youTubeAnalytics.lastRecordedTime.timeReported >= timeBeforeFunctionRun
);
assert.equal(
youTubeAnalytics.lastRecordedTime.timeElapsed,
playerTime * 1000.0
);
});
});
describe('Video Events', () => {
let stub
let playback
let content
describe("Video Events", () => {
let stub;
let playback;
let content;
beforeEach(() => {
youtubeAnalytics.initialize()
youtubeAnalytics.metadata = [{ playback: {}, content: {} }]
stub = sinon.stub(youtubeAnalytics, 'track')
playback = { position: playerTime, quality: playbackQuality, sound: volume }
youTubeAnalytics.initialize();
youTubeAnalytics.metadata = [{ playback: {}, content: {} }];
stub = sinon.stub(youTubeAnalytics, "track");
playback = {
position: playerTime,
quality: playbackQuality,
sound: volume
};
content = {
title: 'YouTube Developers Live: Embedded Web Player Customization',
description: 'On this week\'s show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.',
keywords:
[ 'youtubedataapi',
'youtube',
'youtubeplayerapi',
'youtubeapi',
'video',
'Mountain View',
'gdl' ],
channel: 'Google Developers',
airdate: '2013-04-10T17:25:04.000Z'
}
youtubeAnalytics.metadata = [{playback, content}]
})
describe('Singular Video Playback', () => {
describe('YT buffering events', () => {
title: "YouTube Developers Live: Embedded Web Player Customization",
description:
"On this week's show, Jeff Posnick covers everything you need to know about using player parameters to customize the YouTube iframe-embedded player.",
keywords: [
"youtubedataapi",
"youtube",
"youtubeplayerapi",
"youtubeapi",
"video",
"Mountain View",
"gdl"
],
channel: "Google Developers",
airdate: "2013-04-10T17:25:04.000Z"
};
youTubeAnalytics.metadata = [{ playback, content }];
});
describe("Singular Video Playback", () => {
describe("YT buffering events", () => {
it('should trigger "Playback Started","Playback Buffer Started" if video buffering for the first time', () => {
window.segmentYoutubeOnStateChange({data: YT.PlayerState.BUFFERING})
window.segmentYoutubeOnStateChange({
data: YT.PlayerState.BUFFERING
});
assert(youtubeAnalytics.isBuffering)
assert(youtubeAnalytics.playbackStarted)
sinon.assert.calledWith(stub, 'Video Playback Started', playback)
sinon.assert.calledWith(stub, 'Video Playback Buffer Started', playback)
})
assert(youTubeAnalytics.isBuffering);
assert(youTubeAnalytics.playbackStarted);
sinon.assert.calledWith(stub, "Video Playback Started", playback);
sinon.assert.calledWith(
stub,
"Video Playback Buffer Started",
playback
);
});
it('should trigger "Playback Seek Started","Playback Seek Completed",Playback Buffer Started" if a user seeks with keyboard or seeks while video is paused', () => {
youtubeAnalytics.playbackStarted = true
youTubeAnalytics.playbackStarted = true;
// simulate a keyboard seek
youtubeAnalytics.lastRecordedTime.timeElapsed = 0
youtubeAnalytics.lastRecordedTime.timeReported = Date.now()
playerTime = SEEK_THRESHOLD/1000 + 10
window.segmentYoutubeOnStateChange({data: YT.PlayerState.BUFFERING})
youTubeAnalytics.lastRecordedTime.timeElapsed = 0;
youTubeAnalytics.lastRecordedTime.timeReported = Date.now();
playerTime = SEEK_THRESHOLD / 1000 + 10;
window.segmentYoutubeOnStateChange({
data: YT.PlayerState.BUFFERING
});
assert(youtubeAnalytics.isBuffering)
assert(!youtubeAnalytics.isSeeking)
sinon.assert.calledWith(stub, 'Video Playback Seek Started', playback)
sinon.assert.calledWith(stub, 'Video Playback Seek Completed', playback)
sinon.assert.calledWith(stub, 'Video Playback Buffer Started', playback)
})
assert(youTubeAnalytics.isBuffering);
assert(!youTubeAnalytics.isSeeking);
sinon.assert.calledWith(
stub,
"Video Playback Seek Started",
playback
);
sinon.assert.calledWith(
stub,
"Video Playback Seek Completed",
playback
);
sinon.assert.calledWith(
stub,
"Video Playback Buffer Started",
playback
);
});
it('should trigger "Playback Seek Completed","Playback Buffer Started" if a seek is in progress', () => {
youtubeAnalytics.isSeeking = true
youtubeAnalytics.playbackStarted = true
window.segmentYoutubeOnStateChange({data: YT.PlayerState.BUFFERING})
youTubeAnalytics.isSeeking = true;
youTubeAnalytics.playbackStarted = true;
window.segmentYoutubeOnStateChange({
data: YT.PlayerState.BUFFERING
});
assert(youtubeAnalytics.isBuffering)
assert(!youtubeAnalytics.isSeeking)
sinon.assert.calledWith(stub, 'Video Playback Seek Completed', playback)
sinon.assert.calledWith(stub, 'Video Playback Buffer Started', playback)
})
})
assert(youTubeAnalytics.isBuffering);
assert(!youTubeAnalytics.isSeeking);
sinon.assert.calledWith(
stub,
"Video Playback Seek Completed",
playback
);
sinon.assert.calledWith(
stub,
"Video Playback Buffer Started",
playback
);
});
});
describe('YT play events', () => {
describe("YT play events", () => {
it('should trigger "Content Started" if the content hadnt started yet', () => {
window.segmentYoutubeOnStateChange({data: YT.PlayerState.PLAYING})
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PLAYING });
assert(youtubeAnalytics.contentStarted)
sinon.assert.calledWith(stub, 'Video Content Started', content)
})
assert(youTubeAnalytics.contentStarted);
sinon.assert.calledWith(stub, "Video Content Started", content);
});
it('should trigger "Playback Buffer Completed" if a buffer is in progress', () => {
youtubeAnalytics.contentStarted = true
youtubeAnalytics.isBuffering = true
youTubeAnalytics.contentStarted = true;
youTubeAnalytics.isBuffering = true;
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PLAYING })
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PLAYING });
assert(!youtubeAnalytics.isBuffering)
sinon.assert.calledWith(stub, 'Video Playback Buffer Completed', playback)
})
assert(!youTubeAnalytics.isBuffering);
sinon.assert.calledWith(
stub,
"Video Playback Buffer Completed",
playback
);
});
it('should trigger "Playback Resumed" if the video is paused', () => {
youtubeAnalytics.contentStarted = true
youtubeAnalytics.isPaused = true
youTubeAnalytics.contentStarted = true;
youTubeAnalytics.isPaused = true;
window.segmentYoutubeOnStateChange({data: YT.PlayerState.PLAYING})
assert(!youtubeAnalytics.isPaused)
sinon.assert.calledWith(stub, 'Video Playback Resumed', playback)
})
})
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PLAYING });
assert(!youTubeAnalytics.isPaused);
sinon.assert.calledWith(stub, "Video Playback Resumed", playback);
});
});
describe('YT pause events', () => {
describe("YT pause events", () => {
it('should trigger "Playback Buffer" if the video was buffering', () => {
youtubeAnalytics.isBuffering = true
window.segmentYoutubeOnStateChange({data: YT.PlayerState.PAUSED})
youTubeAnalytics.isBuffering = true;
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PAUSED });
assert(!youtubeAnalytics.isBuffering)
sinon.assert.calledWith(stub, 'Video Playback Buffer Completed', playback)
})
assert(!youTubeAnalytics.isBuffering);
sinon.assert.calledWith(
stub,
"Video Playback Buffer Completed",
playback
);
});
it('should trigger "Playback Seek Started" if a seek is detected', () => {
youtubeAnalytics.lastRecordedTime.timeElapsed = 0
youtubeAnalytics.lastRecordedTime.timeReported = Date.now()
playerTime = playback.position = SEEK_THRESHOLD/1000 + 10
youTubeAnalytics.lastRecordedTime.timeElapsed = 0;
youTubeAnalytics.lastRecordedTime.timeReported = Date.now();
playerTime = playback.position = SEEK_THRESHOLD / 1000 + 10;
window.segmentYoutubeOnStateChange({data: YT.PlayerState.PAUSED})
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PAUSED });
assert(youtubeAnalytics.isSeeking)
sinon.assert.calledWith(stub, 'Video Playback Seek Started', playback)
})
assert(youTubeAnalytics.isSeeking);
sinon.assert.calledWith(
stub,
"Video Playback Seek Started",
playback
);
});
it('should trigger "Playback Paused" if the video wasnt paused and a seek wasnt detected', () => {
window.segmentYoutubeOnStateChange({data: YT.PlayerState.PAUSED})
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.PAUSED });
assert(youtubeAnalytics.isPaused)
sinon.assert.calledWith(stub, 'Video Playback Paused', playback)
})
})
assert(youTubeAnalytics.isPaused);
sinon.assert.calledWith(stub, "Video Playback Paused", playback);
});
});
describe('YT ended events', () => {
describe("YT ended events", () => {
it('should trigger "Video Content Completed" and "Video Playback Completed" as its the only video in the playlist', () => {
window.segmentYoutubeOnStateChange({data: YT.PlayerState.ENDED})
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.ENDED });
assert(!youtubeAnalytics.contentStarted)
sinon.assert.calledWith(stub, 'Video Content Completed', content)
sinon.assert.calledWith(stub, 'Video Playback Completed', playback)
})
})
})
assert(!youTubeAnalytics.contentStarted);
sinon.assert.calledWith(stub, "Video Content Completed", content);
sinon.assert.calledWith(stub, "Video Playback Completed", playback);
});
});
});
describe('Video Playlist Playback', () => {
describe("Video Playlist Playback", () => {
beforeEach(() => {
youtubeAnalytics.metadata = [{ playback, content }, { playback, content }]
playlist = ['', '']
})
it('should set contentStarted to false if user clicks next video', () => {
playerTime = 0
playlistIndex = 1
youtubeAnalytics.playlistIndex = 0
youTubeAnalytics.metadata = [
{ playback, content },
{ playback, content }
];
playlist = ["", ""];
});
it("should set contentStarted to false if user clicks next video", () => {
playerTime = 0;
playlistIndex = 1;
youTubeAnalytics.playlistIndex = 0;
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.BUFFERING})
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.BUFFERING });
assert(!youtubeAnalytics.contentStarted)
})
assert(!youTubeAnalytics.contentStarted);
});
it('should trigger "Playback Completed" and then "Playback Started" when user clicks next video on the last video of playlist', () => {
playerTime = 0
playlistIndex = 0
youtubeAnalytics.playlistIndex = 1
youtubeAnalytics.playbackStarted = true
playerTime = 0;
playlistIndex = 0;
youTubeAnalytics.playlistIndex = 1;
youTubeAnalytics.playbackStarted = true;
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.BUFFERING })
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.BUFFERING });
assert(!youtubeAnalytics.contentStarted)
assert(youtubeAnalytics.isBuffering)
sinon.assert.calledWith(stub, 'Video Playback Completed', playback)
sinon.assert.calledWith(stub, 'Video Playback Started', playback)
sinon.assert.calledWith(stub, 'Video Playback Buffer Started', playback)
assert(!youTubeAnalytics.contentStarted);
assert(youTubeAnalytics.isBuffering);
sinon.assert.calledWith(stub, "Video Playback Completed", playback);
sinon.assert.calledWith(stub, "Video Playback Started", playback);
sinon.assert.calledWith(
stub,
"Video Playback Buffer Started",
playback
);
});
})
it('should trigger "Content Completed" but not "Playback Completed" when a video in the middle of the playlist ends', () => {
playlistIndex = 0
playlistIndex = 0;
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.ENDED })
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.ENDED });
assert(!youtubeAnalytics.contentStarted)
sinon.assert.calledWith(stub, 'Video Content Completed', content)
sinon.assert.neverCalledWith(stub, 'Video Playback Completed')
})
assert(!youTubeAnalytics.contentStarted);
sinon.assert.calledWith(stub, "Video Content Completed", content);
sinon.assert.neverCalledWith(stub, "Video Playback Completed");
});
it('should trigger "Content Completed" and "Playback Completed" when the final video ends', () => {
playlistIndex = 1
playlistIndex = 1;
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.ENDED })
window.segmentYoutubeOnStateChange({ data: YT.PlayerState.ENDED });
assert(!youtubeAnalytics.contentStarted)
sinon.assert.calledWith(stub, 'Video Content Completed', content)
sinon.assert.calledWith(stub, 'Video Playback Completed', playback)
})
})
})
})
assert(!youTubeAnalytics.contentStarted);
sinon.assert.calledWith(stub, "Video Content Completed", content);
sinon.assert.calledWith(stub, "Video Playback Completed", playback);
});
});
});
});
function clearEnv(id) {
document.getElementById(id).remove()
document.getElementById(id).remove();
}

@@ -31,2 +31,3 @@ # Youtube Analytics.js Plugin

var player;
var apiKey = 'xxxxxxxxxxxxxxxxxxxxx';
function onYouTubeIframeAPIReady() {

@@ -38,4 +39,4 @@ player = new YT.Player('player', {

});
var ytAnalytics = new window.analyticsPlugins.YoutubeAnalytics(player)
ytAnalytics.initialize()
var ytAnalytics = new window.videoPlugins.YouTubeAnalytics(player, apiKey);
ytAnalytics.initialize();
}

@@ -42,0 +43,0 @@ ```

module.exports = {
entry: './plugins/index.js',
target: 'node',
mode: 'production',
entry: "./plugins/index.js",
target: "node",
mode: "production",
output: {
libraryTarget: 'commonjs2',
path: __dirname + '/dist',
filename: 'index.js'
libraryTarget: "commonjs2",
path: __dirname + "/dist",
filename: "index.js"
},

@@ -19,5 +19,5 @@ optimization: {

use: {
loader: 'babel-loader',
loader: "babel-loader",
options: {
presets: ['babel-preset-env']
presets: ["babel-preset-env"]
}

@@ -28,2 +28,2 @@ }

}
};
};
module.exports = {
entry: './plugins/index.js',
mode: 'development',
entry: "./plugins/index.js",
mode: "development",
output: {
library: 'videoPlugins',
libraryTarget: 'umd',
path: __dirname + '/dist',
filename: 'index.js'
library: "videoPlugins",
libraryTarget: "umd",
path: __dirname + "/dist",
filename: "index.js"
},
devServer: {
contentBase: './dist'
contentBase: "./dist"
},

@@ -19,5 +19,5 @@ module: {

use: {
loader: 'babel-loader',
loader: "babel-loader",
options: {
presets: ['babel-preset-env']
presets: ["babel-preset-env"]
}

@@ -28,2 +28,2 @@ }

}
};
};

Sorry, the diff of this file is not supported yet