Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dependencies for events logging #189

Merged
merged 4 commits into from
Mar 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 107 additions & 99 deletions lib/events.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
'use strict';

var request = require('request');
var crypto = require('crypto');

var shajs = require('sha.js')
/**
* Construct a new mapbox event client to send interaction events to the mapbox event service
* @param {Object} options options with which to create the service
* @param {String} options.accessToken the mapbox access token to make requests
* @private
*/
function MapboxEventManager(options){
function MapboxEventManager(options) {
this.origin = options.origin || 'https://api.mapbox.com';
this.endpoint = this.origin + '/events/v2';
this.endpoint = '/events/v2';
this.access_token = options.accessToken;
this.version = '0.0.1'
this.sessionID = this.generateSessionID();
this.userAgent = this.getUserAgent();
// parse global options to be sent with each request
this.countries = (options.countries) ? options.countries.split(",") : null;
this.types = (options.types) ? options.types.split(",") : null;
this.countries = (options.countries) ? options.countries.split(",") : null;
this.types = (options.types) ? options.types.split(",") : null;
this.bbox = (options.bbox) ? options.bbox : null;
this.language = (options.language) ? options.language.split(",") : null;
this.limit = (options.limit) ? +options.limit : null;
Expand All @@ -32,15 +29,15 @@ function MapboxEventManager(options){
MapboxEventManager.prototype = {

/**
* Send a search.select event to the mapbox events service
* This event marks the array index of the item selected by the user out of the array of possible options
* @private
* @param {Object} selected the geojson feature selected by the user
* @param {Object} geocoder a mapbox-gl-geocoder instance
* @param {Function} callback a callback function to invoke once the event has been sent (optional)
* @returns {Promise}
*/
select: function(selected, geocoder, callback){
* Send a search.select event to the mapbox events service
* This event marks the array index of the item selected by the user out of the array of possible options
* @private
* @param {Object} selected the geojson feature selected by the user
* @param {Object} geocoder a mapbox-gl-geocoder instance
* @param {Function} callback a callback function to invoke once the event has been sent (optional)
* @returns {Promise}
*/
select: function (selected, geocoder, callback) {
var resultIndex = this.getSelectedIndex(selected, geocoder);
var payload = this.getEventPayload('search.select', geocoder);
payload.resultIndex = resultIndex;
Expand All @@ -55,36 +52,37 @@ MapboxEventManager.prototype = {
},

/**
* Send a search-start event to the mapbox events service
* This turnstile event marks when a user starts a new search
* @private
* @param {Object} geocoder a mapbox-gl-geocoder instance
* @param {Function} callback
* @returns {Promise}
*/
start: function(geocoder, callback){
* Send a search-start event to the mapbox events service
* This turnstile event marks when a user starts a new search
* @private
* @param {Object} geocoder a mapbox-gl-geocoder instance
* @param {Function} callback
* @returns {Promise}
*/
start: function (geocoder, callback) {
var payload = this.getEventPayload('search.start', geocoder);
return this.send(payload, callback);
},

/**
* Send an event to the events service
*
* The event is skipped if the instance is not enabled to send logging events
*
* @private
* @param {Object} payload the http POST body of the event
* @returns {Promise}
*/
send: function(payload, callback){
if (!callback) callback = function(){return};
if (!this.enableEventLogging){
* Send an event to the events service
*
* The event is skipped if the instance is not enabled to send logging events
*
* @private
* @param {Object} payload the http POST body of the event
* @returns {Promise}
*/
send: function (payload, callback) {
if (!callback) callback = function () {
return
};
if (!this.enableEventLogging) {
return callback();
}
var options = this.getRequestOptions(payload);
this.request(options, function(err,res){
this.request(options, function (err) {
if (err) return this.handleError(err, callback);
if (res.statusCode != 204) return this.handleError(res, callback);
if (callback) return callback();
})
},
Expand All @@ -93,121 +91,131 @@ MapboxEventManager.prototype = {
* @private
* @param {*} payload
*/
getRequestOptions: function(payload){
getRequestOptions: function (payload) {
var options = {
// events must be sent with POST
method: "POST",
url: this.endpoint,
headers:{
method: "POST",
host: this.origin,
path: this.endpoint + "?access_token=" + this.access_token,
headers: {
'Content-Type': 'application/json'
},
qs: {
access_token: this.access_token
},
body:JSON.stringify([payload]) //events are arrays
body: JSON.stringify([payload]) //events are arrays
}
return options
},

/**
* Get the event payload to send to the events service
* Most payload properties are shared across all events
* @private
* @param {String} event the name of the event to send to the events service. Valid options are 'search.start', 'search.select', 'search.feedback'.
* @param {Object} geocoder a mapbox-gl-geocoder instance
* @returns {Object} an event payload
*/
getEventPayload: function(event, geocoder){
* Get the event payload to send to the events service
* Most payload properties are shared across all events
* @private
* @param {String} event the name of the event to send to the events service. Valid options are 'search.start', 'search.select', 'search.feedback'.
* @param {Object} geocoder a mapbox-gl-geocoder instance
* @returns {Object} an event payload
*/
getEventPayload: function (event, geocoder) {
var proximity;
if (!geocoder.options.proximity) proximity = null;
else proximity = [geocoder.options.proximity.longitude, geocoder.options.proximity.latitude ];
else proximity = [geocoder.options.proximity.longitude, geocoder.options.proximity.latitude];

var zoom = (geocoder._map) ? geocoder._map.getZoom() : null;
return {
event: event,
created: +new Date(),
sessionIdentifier: this.sessionID,
country: this.countries,
country: this.countries,
userAgent: this.userAgent,
language: this.language,
bbox: this.bbox,
types: this.types,
types: this.types,
endpoint: 'mapbox.places',
// fuzzyMatch: search.fuzzy, //todo --> add to plugin
proximity: proximity,
limit: geocoder.options.limit,
// routing: search.routing, //todo --> add to plugin
queryString: geocoder.inputString,
mapZoom: zoom,
keyboardLocale: this.locale
keyboardLocale: this.locale
}
},

/**
* Wraps the request function for easier testing
* Make an http request and invoke a callback
* @private
* @param {Object} opts options describing the http request to be made
* @param {Function} callback the callback to invoke when the http request is completed
*/
request: function(opts, callback){
request(opts, function(err, res){
return callback(err, res);
})
* Wraps the request function for easier testing
* Make an http request and invoke a callback
* @private
* @param {Object} opts options describing the http request to be made
* @param {Function} callback the callback to invoke when the http request is completed
*/
request: function (opts, callback) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 ) {
if (this.status == 204){
//success
return callback(null);
}else {
return callback(this.statusText);
}
}
};
xhttp.open(opts.method, opts.host + opts.path, true);
for (var header in opts.headers){
var headerValue = opts.headers[header];
xhttp.setRequestHeader(header, headerValue)
}
xhttp.send(opts.body);
},

/**
* Handle an error that occurred while making a request
* @param {Object} err an error instance to log
* @private
*/
handleError: function(err, callback){
* Handle an error that occurred while making a request
* @param {Object} err an error instance to log
* @private
*/
handleError: function (err, callback) {
if (callback) return callback(err);
},

/**
* Generate a session ID to be returned with all of the searches made by this geocoder instance
* ID is random and cannot be tracked across sessions
* @private
*/
generateSessionID: function(){
var sha = crypto.createHash('sha256');
sha.update(Math.random().toString());
return sha.digest('hex')
* Generate a session ID to be returned with all of the searches made by this geocoder instance
* ID is random and cannot be tracked across sessions
* @private
*/
generateSessionID: function () {
return new shajs.sha256().update(Math.random().toString()).digest('hex')
},

/**
* Get a user agent string to send with the request to the events service
* @private
*/
getUserAgent: function(){
* Get a user agent string to send with the request to the events service
* @private
*/
getUserAgent: function () {
return 'mapbox-gl-geocoder.' + this.version + "." + navigator.userAgent;
},

/**
* Get the 0-based numeric index of the item that the user selected out of the list of options
* @private
* @param {Object} selected the geojson feature selected by the user
* @param {Object} geocoder a Mapbox-GL-Geocoder instance
* @returns {Number} the index of the selected result
*/
getSelectedIndex: function(selected, geocoder){
* Get the 0-based numeric index of the item that the user selected out of the list of options
* @private
* @param {Object} selected the geojson feature selected by the user
* @param {Object} geocoder a Mapbox-GL-Geocoder instance
* @returns {Number} the index of the selected result
*/
getSelectedIndex: function (selected, geocoder) {
var results = geocoder._typeahead.data;
var selectedID = selected.id;
var resultIDs = results.map(function (feature){
var resultIDs = results.map(function (feature) {
return feature.id;
});
var selectedIdx = resultIDs.indexOf(selectedID);
return selectedIdx;
},

/**
* Check whether events should be logged
* Clients using a localGeocoder or an origin other than mapbox should not have events logged
* @private
*/
shouldEnableLogging: function(options){
if (!this.origin.includes('api.mapbox.com')) return false;
* Check whether events should be logged
* Clients using a localGeocoder or an origin other than mapbox should not have events logged
* @private
*/
shouldEnableLogging: function (options) {
if (this.origin.indexOf('api.mapbox.com') == -1) return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// hard to make sense of events when a local instance is suplementing results from origin
if (options.localGeocoder) return false;
// hard to make sense of events when a custom filter is in use
Expand Down
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"dependencies": {
"@mapbox/mapbox-sdk": "^0.5.0",
"lodash.debounce": "^4.0.6",
"request": "^2.88.0",
"sha.js": "^2.4.11",
"sinon": "^7.2.3",
"suggestions": "^1.3.2",
"xtend": "^4.0.1"
Expand Down
6 changes: 3 additions & 3 deletions test/events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test('it constructs a new event manager instance with the correct properties', f
limit: 2
}
var eventsManager = new MapboxEventsManager(options)
assert.equals(eventsManager.endpoint, 'https://api.mapbox.com/events/v2', 'the correct event endpoint is set');
assert.equals(eventsManager.endpoint, '/events/v2', 'the correct event endpoint is set');
assert.equals(eventsManager.access_token, options.accessToken, 'sets the right access tokens for the request');
assert.deepEqual(eventsManager.countries, ['CA', 'US'], 'correctly parses the country parameter into an array');
assert.deepEqual(eventsManager.language, ['en', 'fr'], 'correctly parses the language parameter into an array');
Expand Down Expand Up @@ -75,9 +75,9 @@ test('get requst options', function(assert){
event: 'test.event'
};
assert.equals(eventsManager.getRequestOptions(payload).method, 'POST', 'http method is set to POST');
assert.equals(eventsManager.getRequestOptions(payload).url, 'https://api.mapbox.com/events/v2', 'http request is made to the right uri');
assert.equals(eventsManager.getRequestOptions(payload).host, 'https://api.mapbox.com', 'http request is made to the right host');
assert.equals(eventsManager.getRequestOptions(payload).path, '/events/v2?access_token=abc123', 'http request is made to the right host');
assert.deepEqual(eventsManager.getRequestOptions(payload).headers, {'Content-Type': 'application/json'}, 'content type is json');
assert.equals(eventsManager.getRequestOptions(payload).qs.access_token, 'abc123', 'http request is sent with the right access token');
assert.deepEqual(eventsManager.getRequestOptions(payload).body, JSON.stringify([payload]), 'the right payload is set for the request');
assert.end();
})
Expand Down