@segment/analytics.js-video-plugins
Advanced tools
Comparing version
# 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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
1883
16.74%86853
-68.77%22
4.76%6
50%