Skip to content

Commit

Permalink
Merge pull request #22 from liorrozen/individual-tokens
Browse files Browse the repository at this point in the history
Individual tokens
  • Loading branch information
liorrozen authored Feb 28, 2018
2 parents 538d163 + 496743b commit 7daf845
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 77 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var options = {
token: "Replace with your facebook access token",
period: "day",
metrics: ["page_views"],
itemList: ["replace with your facebook page id"],
itemList: [{id: page_id, token: page_token}],
}

var pageStream = new FacebookInsightStream( options )
Expand Down
158 changes: 88 additions & 70 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
// doc: this module is a facebook-insights read stream built over node readable stream
// it provide stream api to read insights data from facebook accounts,
// currently supporting only pages-insight, posts-insights and app-insights.

module.exports = FacebookInsightStream;

var util = require( "util" );
var sugar = require( "sugar" );
var stream = require( "stream" );
var extend = require( "extend" );
var request = require( "request" );
var Promise = require( "bluebird" );
const queryString = require('query-string')
var util = require( 'util' );
var sugar = require( 'sugar' );
var stream = require( 'stream' );
var extend = require( 'extend' );
var request = require( 'request' );
var Promise = require( 'bluebird' );

request = Promise.promisifyAll( request )

var BASEURL = "https://graph.facebook.com/v2.10";
var BASEURL = 'https://graph.facebook.com/v2.10';
// Missing data is flagged by the error code 100
// GraphMethodException error:
// Object with ID 'some_id' does not exist,
Expand All @@ -23,20 +18,24 @@ var BASEURL = "https://graph.facebook.com/v2.10";
var MISSING_ERROR_CODE = 100;
var NOT_SUPPORTED_CODE = 3001;

//edge url for each node type
// edge url for each node type
var EDGEMAP = {
page: "insights",
app: "app_insights",
post: "insights"
page: 'insights',
app: 'app_insights',
post: 'insights'
}

util.inherits( FacebookInsightStream, stream.Readable )
/** this module is a facebook-insights read stream built over node readable
* stream. It provide stream api to read insights data from facebook accounts,
* currently supporting only pages-insight, posts-insights and app-insights.
*/
function FacebookInsightStream( options ) {
stream.Readable.call( this, { objectMode: true } );
var listItems = options.itemList;
var isFunction = typeof listItems === 'function';
if ( !isFunction ) {
listItems = function () { return options.itemList }
listItems = () => options.itemList
}

options.listItems = listItems
Expand Down Expand Up @@ -86,36 +85,37 @@ FacebookInsightStream.prototype._init = function ( callback ) {

var path = [
BASEURL,
"{id}",
'{id}',
options.edge,
"{metric}",
].join( "/" )
'{metric}',
].join( '/' )

var hasEvents = options.events && options.events.length;
var breakdowns = options.breakdowns;

let query = queryString.stringify({
since: options.pastdays ? since : undefined,
let queryObj = {
since: options.pastdays ? since : '',
until: until,
period: options.period,
access_token: options.token,
event_name: hasEvents ? '{ev}' : undefined,
aggregateBy: options.aggregate ? '{agg}' : undefined
})
event_name: hasEvents ? '{ev}' : '',
aggregateBy: options.aggregate ? '{agg}' : ''
}
// Build a query string from the object keys
let query = Object.keys(queryObj).map(key => {
return `${key}=${queryObj[key]}`
}).join('&')

if ( breakdowns && breakdowns.length ) {
for ( var i = 0; i < breakdowns.length; i += 1 ) {
query += "&breakdowns[{index}]={breakdown}".assign( {
index: i,
breakdown: breakdowns[ i ]
});
query += `&breakdowns[${i}]=${breakdowns[i]}`
}
}

// this url is urlPattern shared by all the requests
// each request using thie pattern should replace the
// {id} and {metric} place holders with real values
this.url = [ path, query ].join( "?" )
this.url = [ path, query ].join( '?' )

// options.itemlist is a function that can return either array of items or
// or a promise that resolved with array of items
Expand All @@ -141,50 +141,52 @@ FacebookInsightStream.prototype._init = function ( callback ) {
}

FacebookInsightStream.prototype._initItem = function ( item ) {
var id = typeof item === 'string' ? item : item.id
var options = this.options;
var model = {
base: BASEURL,
id: item,
token: options.token
id: id,
token: item.token || options.token
};

var url = strReplace( "{base}/{id}?access_token={token}", model )
var url = `${model.base}/${model.id}?access_token=${model.token}`

var title = "FACEBOOK " + options.node.toUpperCase();
var title = 'FACEBOOK ' + options.node.toUpperCase();
console.log( new Date().toISOString(), title, url )

return request.getAsync( url )
return FacebookInsightStream._apiCall(url)
.bind( this )
.get( 1 )
.then( JSON.parse )
.then( errorHandler.bind( null, options ) )
.then( function ( data ) {
var result = {
id: item,
name: data.name || data.message || data.story
id: id,
name: data.name || data.message || data.story,
token: item.token || options.token
}
if ( options.node === 'post' ) {
result.createdTime = data.created_time
result.createdTime = data.created_time
}
return result
})
.catch( SkippedError, function ( error ) {
console.warn( "facebook-insights skipped error", error );
console.warn( 'facebook-insights skipped error', error );
})
.catch( function ( error ) {
var retry = this._initItem.bind( this, item );
return this.handleError( error, retry )
})
}

// _collect will be called once for each metric, the insight api request
// single api call for each metric, wich result in a list of values ( value per day)
// so in attempt to create one table with all the metrics,
// we are buffering each result in a key value map, with key for
// each day in the collected time range, and appending each value
// of the current metric to the appropriate key in the buffer.
// finally we generating single row for each day.

/* _collect will be called once for each metric, the insight api request
* single api call for each metric, wich result in a list of values
* (value per day) so in attempt to create one table with all the metrics,
* we are buffering each result in a key value map, with key for
* each day in the collected time range, and appending each value
* of the current metric to the appropriate key in the buffer.
* finally we generating single row for each day.
*/
FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, events ) {
var options = this.options;
var hasEvents = events && events.length;
Expand All @@ -195,20 +197,20 @@ FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, eve

// if the key is constructed with numerous attributes,
// take the datetime information
row.date = key.split( "__" )[ 0 ];
row[ options.node + "Id" ] = item.id;
row[ options.node + "Name" ] = item.name;
row.date = key.split( '__' )[ 0 ];
row[ options.node + 'Id' ] = item.id;
row[ options.node + 'Name' ] = item.name;
// set created_time for posts
if ( options.node === 'post' ) {
row[ 'created_time' ] = item.createdTime;
}
return row;
})

this.emit( "progress", {
this.emit( 'progress', {
total: this.total,
loaded: ++this.loaded,
message: "{{remaining}} " + options.node + "s remaining"
message: '{{remaining}} ' + options.node + 's remaining'
})
return data;
}
Expand All @@ -230,21 +232,22 @@ FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, eve
}

var url = strReplace( this.url, model );
var title = "FACEBOOK " + options.node.toUpperCase();
url = url.replace(/access_token=.*?&/, `access_token=${item.token}&`)
var title = 'FACEBOOK ' + options.node.toUpperCase();

console.log( new Date().toISOString(), title, url );

return request.getAsync( url )
return FacebookInsightStream._apiCall(url)
.get( 1 )
.then( JSON.parse )
.then( errorHandler.bind( null, options ) )
.get( "data" )
.get( 'data' )
.bind( this )
.then( function ( data ) {
// in case that there is no data for a given metric
// we will skip to the next metric
if ( ! data.length ) {
var error = new Error( "No data found for the metric " + _metric );
var error = new Error('No data found for metric ' + _metric);
error.skip = true;
throw error;
}
Expand All @@ -258,10 +261,8 @@ FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, eve
// the same date therefore we need to identify unique
// keys for the buffer by the date and different breakdowns
// we're using the '__' to later seperate the date
Object.keys( val.breakdowns || {} ).forEach( function ( b ){
key += "__{breakdown}".assign( {
breakdown: val.breakdowns[ b ]
});
Object.keys( val.breakdowns || {} ).forEach( function ( b ) {
key += `__${val.breakdowns[b]}`
});

buffer[ key ] || ( buffer[ key ] = {} )
Expand Down Expand Up @@ -292,7 +293,7 @@ FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, eve
}
})
.catch( SkippedError, function ( error ) {
console.warn( "facebook-insights skipped error", error );
console.warn( 'facebook-insights skipped error', error );
})
.then( function () {
// remove the current paramater when done
Expand All @@ -304,7 +305,7 @@ FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, eve
return this._collect( metrics, item, buffer, events );
})
.catch( function ( error ) {
var retry = this._collect.bind( this, metrics, item, buffer, events );
var retry = this._collect.bind(this, metrics, item, buffer, events);
return this.handleError( error, retry );
})
}
Expand All @@ -318,7 +319,7 @@ FacebookInsightStream.prototype._collect = function ( metrics, item, buffer, eve
* Overide this method to create your own error handling and retrying mechanism
*
* @param {Error} error
* @param {Function} retry the function that should be invoke to retry the process
* @param {Function} retry - the function that should be invoked on retry
*/
FacebookInsightStream.prototype.handleError = function ( error, retry ) {
if ( error.retry === true ) {
Expand All @@ -328,12 +329,26 @@ FacebookInsightStream.prototype.handleError = function ( error, retry ) {
}
}

// predicate-based error filter
/**
* Helper function to make api calls - this abstraction is used to
* simplify testing
*
* @param {String} url
* @returns {Promise}
*/
FacebookInsightStream._apiCall = function (url) {
return request.getAsync( url )
}

/**
*/
function SkippedError ( error ) {
return error.skip === true;
}

function errorHandler ( options, body ) {
/**
*/
function errorHandler ( options, body ) {
if ( body.error ) {
var missingItem = body.error.code === MISSING_ERROR_CODE
body.error.skip = (options.ignoreMissing && missingItem)
Expand All @@ -345,21 +360,24 @@ function errorHandler ( options, body ) {
}
}

/**
*/
function strReplace ( string, model ) {
Object.keys( model ).each( function ( name ) {
string = string.replace( "{" + name + "}", model[ name ] );
string = string.replace( '{' + name + '}', model[ name ] );
})

return string;
}

/**
*/
function aggregationType ( ev ) {
var events = [ "fb_ad_network_imp", "fb_ad_network_click" ];
var events = [ 'fb_ad_network_imp', 'fb_ad_network_click' ];

var shouldUseCount = ev && events.indexOf( ev ) > -1;
if ( shouldUseCount ) {
return "COUNT"
return 'COUNT'
}

return "SUM";
return 'SUM';
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "facebook-insight-stream",
"version": "1.0.12",
"version": "2.0.0",
"description": "Readable stream for reading facebook insights",
"main": "index.js",
"scripts": {
Expand All @@ -23,11 +23,11 @@
"dependencies": {
"bluebird": "2.9.32",
"extend": "3.0.0",
"query-string": "^5.0.1",
"request": "2.53.0",
"sugar": "1.4.1"
},
"devDependencies": {
"mocha": "2.3.4"
"mocha": "2.3.4",
"sinon": "4.3.0"
}
}
Loading

0 comments on commit 7daf845

Please sign in to comment.