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 ;
0 commit comments