Skip to content

Commit

Permalink
Merge pull request #529 from mapbox/update-event-schema
Browse files Browse the repository at this point in the history
Update event schema
  • Loading branch information
mpothier authored Sep 13, 2024
2 parents 01ccfc5 + 14be0e7 commit aaa5b91
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 580 deletions.
18 changes: 9 additions & 9 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@ A geocoder component using the [Mapbox Geocoding API][74]
* `options.minLength` **[Number][79]** Minimum number of characters to enter before results are shown. (optional, default `2`)
* `options.limit` **[Number][79]** Maximum number of results to show. (optional, default `5`)
* `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.
* `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.
* `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.
* `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.
* `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.
* `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.
* `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.
* `options.reverseMode` **(distance | score)** Set the factors that are used to sort nearby results. (optional, default `distance`)
* `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`)
* `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`)
* `options.enableEventLogging` **[Boolean][80]** Allow Mapbox to collect anonymous usage statistics from the plugin. (optional, default `true`)
* `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`)
* `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.
* `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`.
* `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.
* `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`.
* `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`)
* `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`)
* `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`)
Expand Down Expand Up @@ -186,7 +186,7 @@ Set input
#### Parameters

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

Returns **[MapboxGeocoder][2]** this

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

#### Parameters

* `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.
* `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.

Returns **[MapboxGeocoder][2]** this

Expand Down Expand Up @@ -380,7 +380,7 @@ Set the filter function used by the plugin.

#### Parameters

* `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.
* `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.

Returns **[MapboxGeocoder][2]** this

Expand Down Expand Up @@ -681,7 +681,7 @@ Returns **[object][75]** 

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

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

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

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## HEAD

### Features / Improvements 🚀

- Updates event service to latest schema

### Dependency update

- Bumps `mapbox-sdk-js` to v0.16.1

## 5.0.2

### Bug fixes 🐛
Expand Down
181 changes: 158 additions & 23 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ function MapboxEventManager(options) {
this.origin = options.origin || 'https://api.mapbox.com';
this.endpoint = 'events/v2';
this.access_token = options.accessToken;
this.version = '0.2.0'
this.sessionID = this.generateSessionID();
this.version = '0.3.0'
this.pluginSessionID = this.generateSessionID();
this.sessionIncrementer = 0;
this.userAgent = this.getUserAgent();

this.options = options;
Expand Down Expand Up @@ -48,18 +49,14 @@ MapboxEventManager.prototype = {
* @returns {Promise}
*/
select: function(selected, geocoder){
var resultIndex = this.getSelectedIndex(selected, geocoder);
var payload = this.getEventPayload('search.select', geocoder);
payload.resultIndex = resultIndex;
payload.resultPlaceName = selected.place_name;
payload.resultId = selected.id;
if ((resultIndex === this.lastSentIndex && payload.queryString === this.lastSentInput) || resultIndex == -1) {
var payload = this.getEventPayload('search.select', geocoder, { selectedFeature: selected });
if (!payload) return; // reject malformed event
if ((payload.resultIndex === this.lastSentIndex && payload.queryString === this.lastSentInput) || payload.resultIndex == -1) {
// don't log duplicate events if the user re-selected the same feature on the same search
return;
}
this.lastSentIndex = resultIndex;
this.lastSentIndex = payload.resultIndex;
this.lastSentInput = payload.queryString;
if (!payload.queryString) return; // will be rejected
return this.push(payload)
},

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

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

Expand Down Expand Up @@ -146,26 +142,43 @@ MapboxEventManager.prototype = {
* @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
* @param {Object} eventArgs Additional arguments needed for certain event types
* @param {Object} eventArgs.key The key pressed by the user
* @param {Object} eventArgs.selectedFeature GeoJSON Feature selected by the user
* @returns {Object} an event payload
*/
getEventPayload: function (event, geocoder) {
getEventPayload: function (event, geocoder, eventArgs = {}) {
// Make sure required arguments are present for certain event types
if (
(event === 'search.select' && !eventArgs.selectedFeature) ||
(event === 'search.keystroke' && !eventArgs.key)
) {
return null;
}

// Handle proximity, whether null, lat/lng coordinate object, or 'ip'
var proximity;
if (!geocoder.options.proximity) {
proximity = null;
} else if (typeof geocoder.options.proximity === 'object') {
proximity = [geocoder.options.proximity.longitude, geocoder.options.proximity.latitude];
} else if (geocoder.options.proximity === 'ip') {
proximity = [999,999]; // Alias for 'ip' in event logs
var ipProximityHeader = geocoder._headers ? geocoder._headers['ip-proximity'] : null;
if (ipProximityHeader && typeof ipProximityHeader === 'string') {
proximity = ipProximityHeader.split(',').map(parseFloat);
} else {
proximity = [999,999]; // Alias for 'ip' in event logs
}
} else {
proximity = geocoder.options.proximity;
}

var zoom = (geocoder._map) ? geocoder._map.getZoom() : undefined;
var payload = {
event: event,
version: this.getEventSchemaVersion(event),
created: +new Date(),
sessionIdentifier: this.sessionID,
sessionIdentifier: this.getSessionId(),
country: this.countries,
userAgent: this.userAgent,
language: this.language,
Expand All @@ -185,11 +198,43 @@ MapboxEventManager.prototype = {
// get the text in the search bar
if (event === "search.select"){
payload.queryString = geocoder.inputString;
}else if (event != "search.select" && geocoder._inputEl){
} else if (event != "search.select" && geocoder._inputEl){
payload.queryString = geocoder._inputEl.value;
}else{
} else {
payload.queryString = geocoder.inputString;
}

// add additional properties for certain event types
if (['search.keystroke', 'search.select'].includes(event)) {
payload.path = 'geocoding/v5/mapbox.places';
}
if (event === 'search.keystroke' && eventArgs.key) {
payload.lastAction = eventArgs.key;
} else if (event === 'search.select' && eventArgs.selectedFeature) {
var selected = eventArgs.selectedFeature;
var resultIndex = this.getSelectedIndex(selected, geocoder);
payload.resultIndex = resultIndex;
payload.resultPlaceName = selected.place_name;
payload.resultId = selected.id;
if (selected.properties) {
payload.resultMapboxId = selected.properties.mapbox_id;
}
if (geocoder._typeahead) {
var results = geocoder._typeahead.data;
if (results && results.length > 0) {
payload.suggestionIds = this.getSuggestionIds(results);
payload.suggestionNames = this.getSuggestionNames(results);
payload.suggestionTypes = this.getSuggestionTypes(results);
payload.suggestionSources = this.getSuggestionSources(results);
}
}
}

// Finally, validate that required properties are present for API compatibility
if (!this.validatePayload(payload)) {
return null;
}

return payload;
},

Expand Down Expand Up @@ -239,6 +284,15 @@ MapboxEventManager.prototype = {
return nanoid();
},

/**
* Get the a unique session ID for the current plugin session and increment the session counter.
*
* @returns {String} The session ID
*/
getSessionId: function(){
return this.pluginSessionID + '.' + this.sessionIncrementer;
},

/**
* Get a user agent string to send with the request to the events service
* @private
Expand All @@ -265,6 +319,91 @@ MapboxEventManager.prototype = {
return selectedIdx;
},

getSuggestionIds: function (results) {
return results.map(function (feature) {
if (feature.properties) {
return feature.properties.mapbox_id || '';
}
return feature.id || '';
});
},

getSuggestionNames: function (results) {
return results.map(function (feature) {
return feature.place_name || '';
});
},

getSuggestionTypes: function (results) {
return results.map(function (feature) {
if (feature.place_type && Array.isArray(feature.place_type)) {
return feature.place_type[0] || '';
}
return '';
});
},

getSuggestionSources: function (results) {
return results.map(function (feature) {
return feature._source || '';
});
},

/**
* Get the correct schema version for the event
* @private
* @param {String} event Name of the event
* @returns
*/
getEventSchemaVersion: function(event) {
if (['search.keystroke', 'search.select'].includes(event)) {
return '2.2';
} else {
return '2.0';
}
},

/**
* Checks if a payload has all the required properties for the event type
* @private
* @param {Object} payload
* @returns
*/
validatePayload: function(payload) {
if (!payload || !payload.event) return false;

var searchStartRequiredProps = ['event', 'created', 'sessionIdentifier', 'queryString'];
var searchKeystrokeRequiredProps = ['event', 'created', 'sessionIdentifier', 'queryString', 'lastAction'];
var searchSelectRequiredProps = ['event', 'created', 'sessionIdentifier', 'queryString', 'resultIndex', 'path', 'suggestionIds'];

var event = payload.event;
if (event === 'search.start') {
return this.objectHasRequiredProps(payload, searchStartRequiredProps);
} else if (event === 'search.keystroke') {
return this.objectHasRequiredProps(payload, searchKeystrokeRequiredProps);
} else if (event === 'search.select') {
return this.objectHasRequiredProps(payload, searchSelectRequiredProps);
}

return true;
},

/**
* Checks of an object has all the required properties
* @private
* @param {Object} obj
* @param {Array<String>} requiredProps
* @returns
*/
objectHasRequiredProps: function(obj, requiredProps) {
return requiredProps.every(function(prop) {
if (prop === 'queryString') {
return typeof obj[prop] === 'string' && obj[prop].length > 0;
}
return obj[prop] !== undefined;
});
},

/**
* Check whether events should be logged
* Clients using a localGeocoder or an origin other than mapbox should not have events logged
Expand All @@ -273,10 +412,6 @@ MapboxEventManager.prototype = {
shouldEnableLogging: function(options){
if (options.enableEventLogging === false) return false;
if (options.origin && options.origin !== 'https://api.mapbox.com') return false;
// 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
if (options.filter) return false;
return true;
},

Expand Down
Loading

0 comments on commit aaa5b91

Please sign in to comment.