Skip to content

Commit aaa5b91

Browse files
authored
Merge pull request #529 from mapbox/update-event-schema
Update event schema
2 parents 01ccfc5 + 14be0e7 commit aaa5b91

File tree

8 files changed

+407
-580
lines changed

8 files changed

+407
-580
lines changed

API.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,16 @@ A geocoder component using the [Mapbox Geocoding API][74]
111111
* `options.minLength` **[Number][79]** Minimum number of characters to enter before results are shown. (optional, default `2`)
112112
* `options.limit` **[Number][79]** Maximum number of results to show. (optional, default `5`)
113113
* `options.language` **[string][76]?** Specify the language to use for response text and query result weighting. Options are IETF language tags comprised of a mandatory ISO 639-1 language code and optionally one or more IETF subtags for country or script. More than one value can also be specified, separated by commas. Defaults to the browser's language settings.
114-
* `options.filter` **[Function][85]?** A function which accepts a Feature in the [Carmen GeoJSON][86] format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise.
115-
* `options.localGeocoder` **[Function][85]?** A function accepting the query string which performs local geocoding to supplement results from the Mapbox Geocoding API. Expected to return an Array of GeoJSON Features in the [Carmen GeoJSON][86] format.
116-
* `options.externalGeocoder` **[Function][85]?** A function accepting the query string and current features list which performs geocoding to supplement results from the Mapbox Geocoding API. Expected to return a Promise which resolves to an Array of GeoJSON Features in the [Carmen GeoJSON][86] format.
114+
* `options.filter` **[Function][85]?** A function which accepts a Feature in the [extended GeoJSON][86] format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise.
115+
* `options.localGeocoder` **[Function][85]?** A function accepting the query string which performs local geocoding to supplement results from the Mapbox Geocoding API. Expected to return an Array of GeoJSON Features in the [extended GeoJSON][86] format.
116+
* `options.externalGeocoder` **[Function][85]?** A function accepting the query string and current features list which performs geocoding to supplement results from the Mapbox Geocoding API. Expected to return a Promise which resolves to an Array of GeoJSON Features in the [extended GeoJSON][86] format.
117117
* `options.reverseMode` **(distance | score)** Set the factors that are used to sort nearby results. (optional, default `distance`)
118118
* `options.reverseGeocode` **[boolean][80]** If `true`, enable reverse geocoding mode. In reverse geocoding, search input is expected to be coordinates in the form `lat, lon`, with suggestions being the reverse geocodes. (optional, default `false`)
119119
* `options.flipCoordinates` **[boolean][80]** If `true`, search input coordinates for reverse geocoding is expected to be in the form `lon, lat` instead of the default `lat, lon`. (optional, default `false`)
120120
* `options.enableEventLogging` **[Boolean][80]** Allow Mapbox to collect anonymous usage statistics from the plugin. (optional, default `true`)
121121
* `options.marker` **([Boolean][80] | [Object][75])** If `true`, a [Marker][78] will be added to the map at the location of the user-selected result using a default set of Marker options. If the value is an object, the marker will be constructed using these options. If `false`, no marker will be added to the map. Requires that `options.mapboxgl` also be set. (optional, default `true`)
122-
* `options.render` **[Function][85]?** A function that specifies how the results should be rendered in the dropdown menu. This function should accepts a single [Carmen GeoJSON][86] object as input and return a string. Any HTML in the returned string will be rendered.
123-
* `options.getItemValue` **[Function][85]?** A function that specifies how the selected result should be rendered in the search bar. This function should accept a single [Carmen GeoJSON][86] object as input and return a string. HTML tags in the output string will not be rendered. Defaults to `(item) => item.place_name`.
122+
* `options.render` **[Function][85]?** A function that specifies how the results should be rendered in the dropdown menu. This function should accepts a single [extended GeoJSON][86] object as input and return a string. Any HTML in the returned string will be rendered.
123+
* `options.getItemValue` **[Function][85]?** A function that specifies how the selected result should be rendered in the search bar. This function should accept a single [extended GeoJSON][86] object as input and return a string. HTML tags in the output string will not be rendered. Defaults to `(item) => item.place_name`.
124124
* `options.mode` **[String][76]** A string specifying the geocoding [endpoint][87] to query. Options are `mapbox.places` and `mapbox.places-permanent`. The `mapbox.places-permanent` mode requires an enterprise license for permanent geocodes. (optional, default `mapbox.places`)
125125
* `options.localGeocoderOnly` **[Boolean][80]** If `true`, indicates that the `localGeocoder` results should be the only ones returned to the user. If `false`, indicates that the `localGeocoder` results should be combined with those from the Mapbox API with the `localGeocoder` results ranked higher. (optional, default `false`)
126126
* `options.autocomplete` **[Boolean][80]** Specify whether to return autocomplete results or not. When autocomplete is enabled, results will be included that start with the requested string, rather than just responses that match it exactly. (optional, default `true`)
@@ -186,7 +186,7 @@ Set input
186186
#### Parameters
187187

188188
* `searchInput` **[string][76]** location name or other search input
189-
- `showSuggestions` **[boolean][80]** display suggestion on setInput call (optional, default `false`)
189+
* `showSuggestions` **[boolean][80]** display suggestion on setInput call (optional, default `false`)
190190

191191
Returns **[MapboxGeocoder][2]** this
192192

@@ -213,7 +213,7 @@ Set the render function used in the results dropdown
213213

214214
#### Parameters
215215

216-
* `fn` **[Function][85]** The function to use as a render function. This function accepts a single [Carmen GeoJSON][86] object as input and returns a string.
216+
* `fn` **[Function][85]** The function to use as a render function. This function accepts a single [extended GeoJSON][86] object as input and returns a string.
217217

218218
Returns **[MapboxGeocoder][2]** this
219219

@@ -380,7 +380,7 @@ Set the filter function used by the plugin.
380380

381381
#### Parameters
382382

383-
* `filter` **[Function][85]** A function which accepts a Feature in the [Carmen GeoJSON][86] format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise.
383+
* `filter` **[Function][85]** A function which accepts a Feature in the [extended GeoJSON][86] format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise.
384384

385385
Returns **[MapboxGeocoder][2]** this
386386

@@ -681,7 +681,7 @@ Returns **[object][75]** 
681681

682682
[85]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
683683

684-
[86]: https://github.com/mapbox/carmen/blob/master/carmen-geojson.md
684+
[86]: https://docs.mapbox.com/api/search/geocoding-v5/#geocoding-response-object
685685

686686
[87]: https://docs.mapbox.com/api/search/#endpoints
687687

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## HEAD
22

3+
### Features / Improvements 🚀
4+
5+
- Updates event service to latest schema
6+
7+
### Dependency update
8+
9+
- Bumps `mapbox-sdk-js` to v0.16.1
10+
311
## 5.0.2
412

513
### Bug fixes 🐛

lib/events.js

Lines changed: 158 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ function MapboxEventManager(options) {
1313
this.origin = options.origin || 'https://api.mapbox.com';
1414
this.endpoint = 'events/v2';
1515
this.access_token = options.accessToken;
16-
this.version = '0.2.0'
17-
this.sessionID = this.generateSessionID();
16+
this.version = '0.3.0'
17+
this.pluginSessionID = this.generateSessionID();
18+
this.sessionIncrementer = 0;
1819
this.userAgent = this.getUserAgent();
1920

2021
this.options = options;
@@ -48,18 +49,14 @@ MapboxEventManager.prototype = {
4849
* @returns {Promise}
4950
*/
5051
select: function(selected, geocoder){
51-
var resultIndex = this.getSelectedIndex(selected, geocoder);
52-
var payload = this.getEventPayload('search.select', geocoder);
53-
payload.resultIndex = resultIndex;
54-
payload.resultPlaceName = selected.place_name;
55-
payload.resultId = selected.id;
56-
if ((resultIndex === this.lastSentIndex && payload.queryString === this.lastSentInput) || resultIndex == -1) {
52+
var payload = this.getEventPayload('search.select', geocoder, { selectedFeature: selected });
53+
if (!payload) return; // reject malformed event
54+
if ((payload.resultIndex === this.lastSentIndex && payload.queryString === this.lastSentInput) || payload.resultIndex == -1) {
5755
// don't log duplicate events if the user re-selected the same feature on the same search
5856
return;
5957
}
60-
this.lastSentIndex = resultIndex;
58+
this.lastSentIndex = payload.resultIndex;
6159
this.lastSentInput = payload.queryString;
62-
if (!payload.queryString) return; // will be rejected
6360
return this.push(payload)
6461
},
6562

@@ -72,7 +69,7 @@ MapboxEventManager.prototype = {
7269
*/
7370
start: function(geocoder){
7471
var payload = this.getEventPayload('search.start', geocoder);
75-
if (!payload.queryString) return; // will be rejected
72+
if (!payload) return; // reject malformed event
7673
return this.push(payload);
7774
},
7875

@@ -91,9 +88,8 @@ MapboxEventManager.prototype = {
9188
// don't send events for keys that don't change the input
9289
// TAB, ESC, LEFT, RIGHT, ENTER, UP, DOWN
9390
if (keyEvent.metaKey || [9, 27, 37, 39, 13, 38, 40].indexOf(keyEvent.keyCode) !== -1) return;
94-
var payload = this.getEventPayload('search.keystroke', geocoder);
95-
payload.lastAction = keyEvent.key;
96-
if (!payload.queryString) return; // will be rejected
91+
var payload = this.getEventPayload('search.keystroke', geocoder, { key: keyEvent.key });
92+
if (!payload) return; // reject malformed event
9793
return this.push(payload);
9894
},
9995

@@ -146,26 +142,43 @@ MapboxEventManager.prototype = {
146142
* @private
147143
* @param {String} event the name of the event to send to the events service. Valid options are 'search.start', 'search.select', 'search.feedback'.
148144
* @param {Object} geocoder a mapbox-gl-geocoder instance
145+
* @param {Object} eventArgs Additional arguments needed for certain event types
146+
* @param {Object} eventArgs.key The key pressed by the user
147+
* @param {Object} eventArgs.selectedFeature GeoJSON Feature selected by the user
149148
* @returns {Object} an event payload
150149
*/
151-
getEventPayload: function (event, geocoder) {
150+
getEventPayload: function (event, geocoder, eventArgs = {}) {
151+
// Make sure required arguments are present for certain event types
152+
if (
153+
(event === 'search.select' && !eventArgs.selectedFeature) ||
154+
(event === 'search.keystroke' && !eventArgs.key)
155+
) {
156+
return null;
157+
}
158+
152159
// Handle proximity, whether null, lat/lng coordinate object, or 'ip'
153160
var proximity;
154161
if (!geocoder.options.proximity) {
155162
proximity = null;
156163
} else if (typeof geocoder.options.proximity === 'object') {
157164
proximity = [geocoder.options.proximity.longitude, geocoder.options.proximity.latitude];
158165
} else if (geocoder.options.proximity === 'ip') {
159-
proximity = [999,999]; // Alias for 'ip' in event logs
166+
var ipProximityHeader = geocoder._headers ? geocoder._headers['ip-proximity'] : null;
167+
if (ipProximityHeader && typeof ipProximityHeader === 'string') {
168+
proximity = ipProximityHeader.split(',').map(parseFloat);
169+
} else {
170+
proximity = [999,999]; // Alias for 'ip' in event logs
171+
}
160172
} else {
161173
proximity = geocoder.options.proximity;
162174
}
163175

164176
var zoom = (geocoder._map) ? geocoder._map.getZoom() : undefined;
165177
var payload = {
166178
event: event,
179+
version: this.getEventSchemaVersion(event),
167180
created: +new Date(),
168-
sessionIdentifier: this.sessionID,
181+
sessionIdentifier: this.getSessionId(),
169182
country: this.countries,
170183
userAgent: this.userAgent,
171184
language: this.language,
@@ -185,11 +198,43 @@ MapboxEventManager.prototype = {
185198
// get the text in the search bar
186199
if (event === "search.select"){
187200
payload.queryString = geocoder.inputString;
188-
}else if (event != "search.select" && geocoder._inputEl){
201+
} else if (event != "search.select" && geocoder._inputEl){
189202
payload.queryString = geocoder._inputEl.value;
190-
}else{
203+
} else {
191204
payload.queryString = geocoder.inputString;
192205
}
206+
207+
// add additional properties for certain event types
208+
if (['search.keystroke', 'search.select'].includes(event)) {
209+
payload.path = 'geocoding/v5/mapbox.places';
210+
}
211+
if (event === 'search.keystroke' && eventArgs.key) {
212+
payload.lastAction = eventArgs.key;
213+
} else if (event === 'search.select' && eventArgs.selectedFeature) {
214+
var selected = eventArgs.selectedFeature;
215+
var resultIndex = this.getSelectedIndex(selected, geocoder);
216+
payload.resultIndex = resultIndex;
217+
payload.resultPlaceName = selected.place_name;
218+
payload.resultId = selected.id;
219+
if (selected.properties) {
220+
payload.resultMapboxId = selected.properties.mapbox_id;
221+
}
222+
if (geocoder._typeahead) {
223+
var results = geocoder._typeahead.data;
224+
if (results && results.length > 0) {
225+
payload.suggestionIds = this.getSuggestionIds(results);
226+
payload.suggestionNames = this.getSuggestionNames(results);
227+
payload.suggestionTypes = this.getSuggestionTypes(results);
228+
payload.suggestionSources = this.getSuggestionSources(results);
229+
}
230+
}
231+
}
232+
233+
// Finally, validate that required properties are present for API compatibility
234+
if (!this.validatePayload(payload)) {
235+
return null;
236+
}
237+
193238
return payload;
194239
},
195240

@@ -239,6 +284,15 @@ MapboxEventManager.prototype = {
239284
return nanoid();
240285
},
241286

287+
/**
288+
* Get the a unique session ID for the current plugin session and increment the session counter.
289+
*
290+
* @returns {String} The session ID
291+
*/
292+
getSessionId: function(){
293+
return this.pluginSessionID + '.' + this.sessionIncrementer;
294+
},
295+
242296
/**
243297
* Get a user agent string to send with the request to the events service
244298
* @private
@@ -265,6 +319,91 @@ MapboxEventManager.prototype = {
265319
return selectedIdx;
266320
},
267321

322+
getSuggestionIds: function (results) {
323+
return results.map(function (feature) {
324+
if (feature.properties) {
325+
return feature.properties.mapbox_id || '';
326+
}
327+
return feature.id || '';
328+
});
329+
},
330+
331+
getSuggestionNames: function (results) {
332+
return results.map(function (feature) {
333+
return feature.place_name || '';
334+
});
335+
},
336+
337+
getSuggestionTypes: function (results) {
338+
return results.map(function (feature) {
339+
if (feature.place_type && Array.isArray(feature.place_type)) {
340+
return feature.place_type[0] || '';
341+
}
342+
return '';
343+
});
344+
},
345+
346+
getSuggestionSources: function (results) {
347+
return results.map(function (feature) {
348+
return feature._source || '';
349+
});
350+
},
351+
352+
/**
353+
* Get the correct schema version for the event
354+
* @private
355+
* @param {String} event Name of the event
356+
* @returns
357+
*/
358+
getEventSchemaVersion: function(event) {
359+
if (['search.keystroke', 'search.select'].includes(event)) {
360+
return '2.2';
361+
} else {
362+
return '2.0';
363+
}
364+
},
365+
366+
/**
367+
* Checks if a payload has all the required properties for the event type
368+
* @private
369+
* @param {Object} payload
370+
* @returns
371+
*/
372+
validatePayload: function(payload) {
373+
if (!payload || !payload.event) return false;
374+
375+
var searchStartRequiredProps = ['event', 'created', 'sessionIdentifier', 'queryString'];
376+
var searchKeystrokeRequiredProps = ['event', 'created', 'sessionIdentifier', 'queryString', 'lastAction'];
377+
var searchSelectRequiredProps = ['event', 'created', 'sessionIdentifier', 'queryString', 'resultIndex', 'path', 'suggestionIds'];
378+
379+
var event = payload.event;
380+
if (event === 'search.start') {
381+
return this.objectHasRequiredProps(payload, searchStartRequiredProps);
382+
} else if (event === 'search.keystroke') {
383+
return this.objectHasRequiredProps(payload, searchKeystrokeRequiredProps);
384+
} else if (event === 'search.select') {
385+
return this.objectHasRequiredProps(payload, searchSelectRequiredProps);
386+
}
387+
388+
return true;
389+
},
390+
391+
/**
392+
* Checks of an object has all the required properties
393+
* @private
394+
* @param {Object} obj
395+
* @param {Array<String>} requiredProps
396+
* @returns
397+
*/
398+
objectHasRequiredProps: function(obj, requiredProps) {
399+
return requiredProps.every(function(prop) {
400+
if (prop === 'queryString') {
401+
return typeof obj[prop] === 'string' && obj[prop].length > 0;
402+
}
403+
return obj[prop] !== undefined;
404+
});
405+
},
406+
268407
/**
269408
* Check whether events should be logged
270409
* Clients using a localGeocoder or an origin other than mapbox should not have events logged
@@ -273,10 +412,6 @@ MapboxEventManager.prototype = {
273412
shouldEnableLogging: function(options){
274413
if (options.enableEventLogging === false) return false;
275414
if (options.origin && options.origin !== 'https://api.mapbox.com') return false;
276-
// hard to make sense of events when a local instance is suplementing results from origin
277-
if (options.localGeocoder) return false;
278-
// hard to make sense of events when a custom filter is in use
279-
if (options.filter) return false;
280415
return true;
281416
},
282417

0 commit comments

Comments
 (0)