Skip to content

Commit 724c900

Browse files
author
Scott Farley
authored
Merge pull request #183 from mapbox/interaction-logging
Interaction event logging
2 parents 183dd0a + 5a3edb8 commit 724c900

File tree

9 files changed

+2757
-2217
lines changed

9 files changed

+2757
-2217
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## v3.1.2
2+
3+
- Enable inaction event logging
4+
15
### v3.1.1
26

37
- [bug] Ensures proximity is passed to client [#180](https://github.com/mapbox/mapbox-gl-geocoder/pull/180)

debug/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
var mapboxgl = require('mapbox-gl');
33
var insertCss = require('insert-css');
44
var fs = require('fs');
5+
56
mapboxgl.accessToken = window.localStorage.getItem('MapboxAccessToken');
67

8+
79
var meta = document.createElement('meta');
810
meta.name = 'viewport';
911
meta.content = 'initial-scale=1,maximum-scale=1,user-scalable=no';

lib/events.js

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
'use strict';
2+
3+
var request = require('request');
4+
var crypto = require('crypto');
5+
6+
/**
7+
* Construct a new mapbox event client to send interaction events to the mapbox event service
8+
* @param {Object} options options with which to create the service
9+
* @param {String} options.accessToken the mapbox access token to make requests
10+
* @private
11+
*/
12+
function MapboxEventManager(options){
13+
this.origin = options.origin || 'https://api.mapbox.com';
14+
this.endpoint = this.origin + '/events/v2';
15+
this.access_token = options.accessToken;
16+
this.version = '0.0.1'
17+
this.sessionID = this.generateSessionID();
18+
this.userAgent = this.getUserAgent();
19+
// parse global options to be sent with each request
20+
this.countries = (options.countries) ? options.countries.split(",") : null;
21+
this.types = (options.types) ? options.types.split(",") : null;
22+
this.bbox = (options.bbox) ? options.bbox : null;
23+
this.language = (options.language) ? options.language.split(",") : null;
24+
this.limit = (options.limit) ? +options.limit : null;
25+
this.locale = navigator.language || null;
26+
this.enableEventLogging = this.shouldEnableLogging(options);
27+
// keep some state to deduplicate requests if necessary
28+
this.lastSentInput = "";
29+
this.lastSentIndex = 0;
30+
}
31+
32+
MapboxEventManager.prototype = {
33+
34+
/**
35+
* Send a search.select event to the mapbox events service
36+
* This event marks the array index of the item selected by the user out of the array of possible options
37+
* @private
38+
* @param {Object} selected the geojson feature selected by the user
39+
* @param {Object} geocoder a mapbox-gl-geocoder instance
40+
* @param {Function} callback a callback function to invoke once the event has been sent (optional)
41+
* @returns {Promise}
42+
*/
43+
select: function(selected, geocoder, callback){
44+
var resultIndex = this.getSelectedIndex(selected, geocoder);
45+
var payload = this.getEventPayload('search.select', geocoder);
46+
payload.resultIndex = resultIndex;
47+
if (resultIndex === this.lastSentIndex && payload.queryString === this.lastSentInput) {
48+
// don't log duplicate events if the user re-selected the same feature on the same search
49+
if (callback) return callback();
50+
}
51+
this.lastSentIndex = resultIndex;
52+
this.lastSentInput = payload.queryString;
53+
return this.send(payload, callback)
54+
},
55+
56+
/**
57+
* Send a search-start event to the mapbox events service
58+
* This turnstile event marks when a user starts a new search
59+
* @private
60+
* @param {Object} geocoder a mapbox-gl-geocoder instance
61+
* @param {Function} callback
62+
* @returns {Promise}
63+
*/
64+
start: function(geocoder, callback){
65+
var payload = this.getEventPayload('search.start', geocoder);
66+
return this.send(payload, callback);
67+
},
68+
69+
/**
70+
* Send an event to the events service
71+
*
72+
* The event is skipped if the instance is not enabled to send logging events
73+
*
74+
* @private
75+
* @param {Object} payload the http POST body of the event
76+
* @returns {Promise}
77+
*/
78+
send: function(payload, callback){
79+
if (!callback) callback = function(){return};
80+
if (!this.enableEventLogging){
81+
return callback();
82+
}
83+
var options = this.getRequestOptions(payload);
84+
this.request(options, function(err,res){
85+
if (err) return this.handleError(err, callback);
86+
if (res.statusCode != 204) return this.handleError(res, callback);
87+
if (callback) return callback();
88+
})
89+
},
90+
/**
91+
* Get http request options
92+
* @private
93+
* @param {*} payload
94+
*/
95+
getRequestOptions: function(payload){
96+
var options = {
97+
// events must be sent with POST
98+
method: "POST",
99+
url: this.endpoint,
100+
headers:{
101+
'Content-Type': 'application/json'
102+
},
103+
qs: {
104+
access_token: this.access_token
105+
},
106+
body:JSON.stringify([payload]) //events are arrays
107+
}
108+
return options
109+
},
110+
111+
/**
112+
* Get the event payload to send to the events service
113+
* Most payload properties are shared across all events
114+
* @private
115+
* @param {String} event the name of the event to send to the events service. Valid options are 'search.start', 'search.select', 'search.feedback'.
116+
* @param {Object} geocoder a mapbox-gl-geocoder instance
117+
* @returns {Object} an event payload
118+
*/
119+
getEventPayload: function(event, geocoder){
120+
var proximity;
121+
if (!geocoder.options.proximity) proximity = null;
122+
else proximity = [geocoder.options.proximity.longitude, geocoder.options.proximity.latitude ];
123+
124+
var zoom = (geocoder._map) ? geocoder._map.getZoom() : null;
125+
return {
126+
event: event,
127+
created: +new Date(),
128+
sessionIdentifier: this.sessionID,
129+
country: this.countries,
130+
userAgent: this.userAgent,
131+
language: this.language,
132+
bbox: this.bbox,
133+
types: this.types,
134+
endpoint: 'mapbox.places',
135+
// fuzzyMatch: search.fuzzy, //todo --> add to plugin
136+
proximity: proximity,
137+
limit: geocoder.options.limit,
138+
// routing: search.routing, //todo --> add to plugin
139+
queryString: geocoder.inputString,
140+
mapZoom: zoom,
141+
keyboardLocale: this.locale
142+
}
143+
},
144+
145+
/**
146+
* Wraps the request function for easier testing
147+
* Make an http request and invoke a callback
148+
* @private
149+
* @param {Object} opts options describing the http request to be made
150+
* @param {Function} callback the callback to invoke when the http request is completed
151+
*/
152+
request: function(opts, callback){
153+
request(opts, function(err, res){
154+
return callback(err, res);
155+
})
156+
},
157+
158+
/**
159+
* Handle an error that occurred while making a request
160+
* @param {Object} err an error instance to log
161+
* @private
162+
*/
163+
handleError: function(err, callback){
164+
if (callback) return callback(err);
165+
},
166+
167+
/**
168+
* Generate a session ID to be returned with all of the searches made by this geocoder instance
169+
* ID is random and cannot be tracked across sessions
170+
* @private
171+
*/
172+
generateSessionID: function(){
173+
var sha = crypto.createHash('sha256');
174+
sha.update(Math.random().toString());
175+
return sha.digest('hex')
176+
},
177+
178+
/**
179+
* Get a user agent string to send with the request to the events service
180+
* @private
181+
*/
182+
getUserAgent: function(){
183+
return 'mapbox-gl-geocoder.' + this.version + "." + navigator.userAgent;
184+
},
185+
186+
/**
187+
* Get the 0-based numeric index of the item that the user selected out of the list of options
188+
* @private
189+
* @param {Object} selected the geojson feature selected by the user
190+
* @param {Object} geocoder a Mapbox-GL-Geocoder instance
191+
* @returns {Number} the index of the selected result
192+
*/
193+
getSelectedIndex: function(selected, geocoder){
194+
var results = geocoder._typeahead.data;
195+
var selectedID = selected.id;
196+
var resultIDs = results.map(function (feature){
197+
return feature.id;
198+
});
199+
var selectedIdx = resultIDs.indexOf(selectedID);
200+
return selectedIdx;
201+
},
202+
203+
/**
204+
* Check whether events should be logged
205+
* Clients using a localGeocoder or an origin other than mapbox should not have events logged
206+
* @private
207+
*/
208+
shouldEnableLogging: function(options){
209+
if (!this.origin.includes('api.mapbox.com')) return false;
210+
// hard to make sense of events when a local instance is suplementing results from origin
211+
if (options.localGeocoder) return false;
212+
// hard to make sense of events when a custom filter is in use
213+
if (options.filter) return false;
214+
return true;
215+
}
216+
}
217+
218+
219+
220+
module.exports = MapboxEventManager;

lib/index.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var EventEmitter = require('events').EventEmitter;
77
var exceptions = require('./exceptions');
88
var MapboxClient = require('@mapbox/mapbox-sdk');
99
var mbxGeocoder = require('@mapbox/mapbox-sdk/services/geocoding');
10+
var MapboxEventManager = require('./events');
1011
var geocoderService;
1112

1213
/**
@@ -49,6 +50,8 @@ var geocoderService;
4950
function MapboxGeocoder(options) {
5051
this._eventEmitter = new EventEmitter();
5152
this.options = extend({}, this.options, options);
53+
this.inputString = '';
54+
this.fresh = true;
5255
}
5356

5457
MapboxGeocoder.prototype = {
@@ -59,7 +62,8 @@ MapboxGeocoder.prototype = {
5962
trackProximity: false,
6063
minLength: 2,
6164
reverseGeocode: false,
62-
limit: 5
65+
limit: 5,
66+
origin: 'https://api.mapbox.com'
6367
},
6468

6569
onAdd: function(map) {
@@ -68,10 +72,12 @@ MapboxGeocoder.prototype = {
6872
geocoderService = mbxGeocoder(
6973
MapboxClient({
7074
accessToken: this.options.accessToken,
71-
origin: this.options.origin || 'https://api.mapbox.com'
75+
origin: this.options.origin
7276
})
7377
);
7478

79+
this.eventManager = new MapboxEventManager(this.options);
80+
7581
this._onChange = this._onChange.bind(this);
7682
this._onKeyDown = this._onKeyDown.bind(this);
7783
this._onQueryResult = this._onQueryResult.bind(this);
@@ -144,6 +150,7 @@ MapboxGeocoder.prototype = {
144150
? e.target.shadowRoot.activeElement
145151
: e.target;
146152
if (!target.value) {
153+
this.fresh = true;
147154
return (this._clearEl.style.display = 'none');
148155
}
149156

@@ -180,13 +187,14 @@ MapboxGeocoder.prototype = {
180187
}
181188
}
182189
this._eventEmitter.emit('result', { result: selected });
190+
this.eventManager.select(selected, this);
183191
}
184192
},
185193

186194
_geocode: function(searchInput) {
187195
this._loadingEl.style.display = 'block';
188196
this._eventEmitter.emit('loading', { query: searchInput });
189-
197+
this.inputString = searchInput;
190198
// Possible config proprerties to pass to client
191199
var keys = [
192200
'bbox',
@@ -272,6 +280,10 @@ MapboxGeocoder.prototype = {
272280
}
273281

274282
res.config = config;
283+
if (this.fresh){
284+
this.eventManager.start(this);
285+
this.fresh = false;
286+
}
275287
this._eventEmitter.emit('results', res);
276288
this._typeahead.update(res.features);
277289
}.bind(this)
@@ -307,6 +319,8 @@ MapboxGeocoder.prototype = {
307319
this._inputEl.focus();
308320
this._clearEl.style.display = 'none';
309321
this._eventEmitter.emit('clear');
322+
// reset the turnstile event
323+
this.fresh = true;
310324
},
311325

312326
_onQueryResult: function(response) {

0 commit comments

Comments
 (0)