diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 30055b7525e..855cfdbf8da 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -5,7 +5,7 @@ import { extend, pick } from '../util/util'; import { getImage, ResourceType } from '../util/ajax'; import { Event, ErrorEvent, Evented } from '../util/evented'; import loadTileJSON from './load_tilejson'; -import { normalizeTileURL as normalizeURL, postTurnstileEvent } from '../util/mapbox'; +import { normalizeTileURL as normalizeURL, postTurnstileEvent, postMapLoadEvent } from '../util/mapbox'; import TileBounds from './tile_bounds'; import Texture from '../render/texture'; @@ -70,6 +70,7 @@ class RasterTileSource extends Evented implements Source { if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); postTurnstileEvent(tileJSON.tiles); + postMapLoadEvent(tileJSON.tiles, this.map._getMapId()); // `content` is included here to prevent a race condition where `Style#_updateSources` is called // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index cb1a46eaa3b..76c7a2168f2 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -4,7 +4,7 @@ import { Event, ErrorEvent, Evented } from '../util/evented'; import { extend, pick } from '../util/util'; import loadTileJSON from './load_tilejson'; -import { normalizeTileURL as normalizeURL, postTurnstileEvent } from '../util/mapbox'; +import { normalizeTileURL as normalizeURL, postTurnstileEvent, postMapLoadEvent } from '../util/mapbox'; import TileBounds from './tile_bounds'; import { ResourceType } from '../util/ajax'; import browser from '../util/browser'; @@ -74,6 +74,7 @@ class VectorTileSource extends Evented implements Source { if (tileJSON.bounds) this.tileBounds = new TileBounds(tileJSON.bounds, this.minzoom, this.maxzoom); postTurnstileEvent(tileJSON.tiles); + postMapLoadEvent(tileJSON.tiles, this.map._getMapId()); // `content` is included here to prevent a race condition where `Style#_updateSources` is called // before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives diff --git a/src/ui/map.js b/src/ui/map.js index 8ff6957ac91..507aa4e7e96 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -1,6 +1,6 @@ // @flow -import { extend, bindAll, warnOnce } from '../util/util'; +import { extend, bindAll, warnOnce, uniqueId } from '../util/util'; import browser from '../util/browser'; import window from '../util/window'; @@ -262,6 +262,7 @@ class Map extends Camera { _collectResourceTiming: boolean; _renderTaskQueue: TaskQueue; _controls: Array; + _mapId: number; /** * The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad. @@ -323,6 +324,7 @@ class Map extends Camera { this._collectResourceTiming = options.collectResourceTiming; this._renderTaskQueue = new TaskQueue(); this._controls = []; + this._mapId = uniqueId(); const transformRequestFn = options.transformRequest; this._transformRequest = transformRequestFn ? @@ -406,6 +408,16 @@ class Map extends Camera { }); } + /* + * Returns a unique number for this map instance which is used for the MapLoadEvent + * to make sure we only fire one event per instantiated map object. + * @private + * @returns {number} + */ + _getMapId() { + return this._mapId; + } + /** * Adds a {@link IControl} to the map, calling `control.onAdd(this)`. * diff --git a/src/util/mapbox.js b/src/util/mapbox.js index 18b68167392..eadb803b42a 100644 --- a/src/util/mapbox.js +++ b/src/util/mapbox.js @@ -5,14 +5,14 @@ import config from './config'; import browser from './browser'; import window from './window'; import { version } from '../../package.json'; -import { uuid, validateUuid, storageAvailable, warnOnce } from './util'; +import { uuid, validateUuid, storageAvailable, warnOnce, extend } from './util'; import { postData } from './ajax'; import type { RequestParameters } from './ajax'; import type { Cancelable } from '../types/cancelable'; const help = 'See https://www.mapbox.com/api-documentation/#access-tokens'; -const turnstileEventStorageKey = 'mapbox.turnstileEventData'; +const telemEventKey = 'mapbox.eventData'; type UrlObject = {| protocol: string, @@ -133,19 +133,149 @@ function formatUrl(obj: UrlObject): string { return `${obj.protocol}://${obj.authority}${obj.path}${params}`; } -export class TurnstileEvent { - eventData: { anonId: ?string, lastSuccess: ?number, accessToken: ?string}; - queue: Array; - pending: boolean +type TelemetryEventType = 'appUserTurnstile' | 'map.load'; + +class TelemetryEvent { + eventData: { lastSuccess: ?number, accessToken: ?string}; + anonId: ?string; + queue: Array; + type: TelemetryEventType; pendingRequest: ?Cancelable; - constructor() { - this.eventData = { anonId: null, lastSuccess: null, accessToken: config.ACCESS_TOKEN}; + constructor(type: TelemetryEventType) { + this.type = type; + this.anonId = null; + this.eventData = {lastSuccess: null, accessToken: config.ACCESS_TOKEN}; this.queue = []; - this.pending = false; this.pendingRequest = null; } + fetchEventData() { + const isLocalStorageAvailable = storageAvailable('localStorage'); + const storageKey = `${telemEventKey}:${config.ACCESS_TOKEN || ''}`; + const uuidKey = `${telemEventKey}.uuid:${config.ACCESS_TOKEN || ''}`; + + if (isLocalStorageAvailable) { + //Retrieve cached data + try { + const data = window.localStorage.getItem(storageKey); + if (data) { + this.eventData = JSON.parse(data); + } + + const uuid = window.localStorage.getItem(uuidKey); + if (uuid) this.anonId = uuid; + } catch (e) { + warnOnce('Unable to read from LocalStorage'); + } + } + } + + saveEventData() { + const isLocalStorageAvailable = storageAvailable('localStorage'); + const storageKey = `${telemEventKey}:${config.ACCESS_TOKEN || ''}`; + const uuidKey = `${telemEventKey}.uuid:${config.ACCESS_TOKEN || ''}`; + if (isLocalStorageAvailable) { + try { + window.localStorage.setItem(uuidKey, this.anonId); + if (this.eventData.lastSuccess) { + window.localStorage.setItem(storageKey, JSON.stringify(this.eventData)); + } + } catch (e) { + warnOnce('Unable to write to LocalStorage'); + } + } + + } + + processRequests() {} + + /* + * If any event data should be persisted after the POST request, the callback should modify eventData` + * to the values that should be saved. For this reason, the callback should be invoked prior to the call + * to TelemetryEvent#saveData + */ + postEvent(timestamp: number, additionalPayload: {[string]: any}, callback: (err: ?Error) => void) { + const eventsUrlObject: UrlObject = parseUrl(config.EVENTS_URL); + eventsUrlObject.params.push(`access_token=${config.ACCESS_TOKEN || ''}`); + const payload: Object = { + event: this.type, + created: new Date(timestamp).toISOString(), + sdkIdentifier: 'mapbox-gl-js', + sdkVersion: version, + userId: this.anonId + }; + + const finalPayload = additionalPayload ? extend(payload, additionalPayload) : payload; + const request: RequestParameters = { + url: formatUrl(eventsUrlObject), + headers: { + 'Content-Type': 'text/plain' //Skip the pre-flight OPTIONS request + }, + body: JSON.stringify([finalPayload]) + }; + + this.pendingRequest = postData(request, (error) => { + this.pendingRequest = null; + callback(error); + this.saveEventData(); + this.processRequests(); + }); + } + + queueRequest(event: number | {id: number, timestamp: number}) { + this.queue.push(event); + this.processRequests(); + } +} + +export class MapLoadEvent extends TelemetryEvent { + +success: {[number]: boolean}; + + constructor() { + super('map.load'); + this.success = {}; + } + + postMapLoadEvent(tileUrls: Array, mapId: number) { + //Enabled only when Mapbox Access Token is set and a source uses + // mapbox tiles. + if (config.ACCESS_TOKEN && + Array.isArray(tileUrls) && + tileUrls.some(url => isMapboxHTTPURL(url))) { + this.queueRequest({id: mapId, timestamp: Date.now()}); + } + } + + processRequests() { + if (this.pendingRequest || this.queue.length === 0) return; + const {id, timestamp} = this.queue.shift(); + + // Only one load event should fire per map + if (id && this.success[id]) return; + + if (!this.anonId) { + this.fetchEventData(); + } + + if (!validateUuid(this.anonId)) { + this.anonId = uuid(); + } + + this.postEvent(timestamp, {}, (err) => { + if (!err) { + if (id) this.success[id] = true; + } + }); + } +} + + +export class TurnstileEvent extends TelemetryEvent { + constructor() { + super('appUserTurnstile'); + } + postTurnstileEvent(tileUrls: Array) { //Enabled only when Mapbox Access Token is set and a source uses // mapbox tiles. @@ -156,90 +286,53 @@ export class TurnstileEvent { } } - queueRequest(date: number) { - this.queue.push(date); - this.processRequests(); - } processRequests() { if (this.pendingRequest || this.queue.length === 0) { return; } - const storageKey = `${turnstileEventStorageKey}:${config.ACCESS_TOKEN || ''}`; - const isLocalStorageAvailable = storageAvailable('localStorage'); - let dueForEvent = this.eventData.accessToken ? (this.eventData.accessToken !== config.ACCESS_TOKEN) : false; + let dueForEvent = this.eventData.accessToken ? (this.eventData.accessToken !== config.ACCESS_TOKEN) : false; //Reset event data cache if the access token changed. if (dueForEvent) { - this.eventData.anonId = this.eventData.lastSuccess = null; + this.anonId = this.eventData.lastSuccess = null; } - if ((!this.eventData.anonId || !this.eventData.lastSuccess) && - isLocalStorageAvailable) { + if (!this.anonId || !this.eventData.lastSuccess) { //Retrieve cached data - try { - const data = window.localStorage.getItem(storageKey); - if (data) { - this.eventData = JSON.parse(data); - } - } catch (e) { - warnOnce('Unable to read from LocalStorage'); - } + this.fetchEventData(); } - if (!validateUuid(this.eventData.anonId)) { - this.eventData.anonId = uuid(); + if (!validateUuid(this.anonId)) { + this.anonId = uuid(); dueForEvent = true; } - const nextUpdate = this.queue.shift(); + const nextUpdate = this.queue.shift(); // Record turnstile event once per calendar day. if (this.eventData.lastSuccess) { const lastUpdate = new Date(this.eventData.lastSuccess); const nextDate = new Date(nextUpdate); const daysElapsed = (nextUpdate - this.eventData.lastSuccess) / (24 * 60 * 60 * 1000); dueForEvent = dueForEvent || daysElapsed >= 1 || daysElapsed < -1 || lastUpdate.getDate() !== nextDate.getDate(); + } else { + dueForEvent = true; } if (!dueForEvent) { return this.processRequests(); } - const eventsUrlObject: UrlObject = parseUrl(config.EVENTS_URL); - eventsUrlObject.params.push(`access_token=${config.ACCESS_TOKEN || ''}`); - - const request: RequestParameters = { - url: formatUrl(eventsUrlObject), - headers: { - 'Content-Type': 'text/plain' // Skip the pre-flight OPTIONS request - }, - body: JSON.stringify([{ - event: 'appUserTurnstile', - created: (new Date(nextUpdate)).toISOString(), - sdkIdentifier: 'mapbox-gl-js', - sdkVersion: version, - 'enabled.telemetry': false, - userId: this.eventData.anonId - }]) - }; - - this.pendingRequest = postData(request, (error: ?Error) => { - this.pendingRequest = null; - if (!error) { + this.postEvent(nextUpdate, {"enabled.telemetry": false}, (err)=> { + if (!err) { this.eventData.lastSuccess = nextUpdate; this.eventData.accessToken = config.ACCESS_TOKEN; - if (isLocalStorageAvailable) { - try { - window.localStorage.setItem(storageKey, JSON.stringify(this.eventData)); - } catch (e) { - warnOnce('Unable to write to LocalStorage'); - } - } - this.processRequests(); } }); } } const turnstileEvent_ = new TurnstileEvent(); - export const postTurnstileEvent = turnstileEvent_.postTurnstileEvent.bind(turnstileEvent_); + +const mapLoadEvent_ = new MapLoadEvent(); +export const postMapLoadEvent = mapLoadEvent_.postMapLoadEvent.bind(mapLoadEvent_); diff --git a/test/unit/source/raster_dem_tile_source.test.js b/test/unit/source/raster_dem_tile_source.test.js index 73c45ae6d55..13038efb3e8 100644 --- a/test/unit/source/raster_dem_tile_source.test.js +++ b/test/unit/source/raster_dem_tile_source.test.js @@ -7,6 +7,7 @@ function createSource(options, transformCallback) { const source = new RasterDEMTileSource('id', options, { send: function() {} }, options.eventedParent); source.onAdd({ transform: { angle: 0, pitch: 0, showCollisionBoxes: false }, + _getMapId: () => 1, _transformRequest: transformCallback ? transformCallback : (url) => { return { url }; } }); diff --git a/test/unit/source/raster_tile_source.test.js b/test/unit/source/raster_tile_source.test.js index f20fc2bef10..a6839395f38 100644 --- a/test/unit/source/raster_tile_source.test.js +++ b/test/unit/source/raster_tile_source.test.js @@ -7,6 +7,7 @@ function createSource(options, transformCallback) { const source = new RasterTileSource('id', options, { send: function() {} }, options.eventedParent); source.onAdd({ transform: { angle: 0, pitch: 0, showCollisionBoxes: false }, + _getMapId: () => 1, _transformRequest: transformCallback ? transformCallback : (url) => { return { url }; } }); diff --git a/test/unit/source/vector_tile_source.test.js b/test/unit/source/vector_tile_source.test.js index 15120c962ee..790c53cfea5 100644 --- a/test/unit/source/vector_tile_source.test.js +++ b/test/unit/source/vector_tile_source.test.js @@ -8,6 +8,7 @@ function createSource(options, transformCallback) { const source = new VectorTileSource('id', options, { send: function() {} }, options.eventedParent); source.onAdd({ transform: { showCollisionBoxes: false }, + _getMapId: () => 1, _transformRequest: transformCallback ? transformCallback : (url) => { return { url }; } }); diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index 830d336ba3a..7d19a5d50a5 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -52,6 +52,10 @@ class StubMap extends Evented { _transformRequest(url) { return { url }; } + + _getMapId() { + return 1; + } } test('Style', (t) => { diff --git a/test/unit/ui/control/logo.test.js b/test/unit/ui/control/logo.test.js index 567ea35c486..1cee1a9e887 100644 --- a/test/unit/ui/control/logo.test.js +++ b/test/unit/ui/control/logo.test.js @@ -25,7 +25,8 @@ function createMap(t, logoPosition, logoRequired) { function createSource(options, logoRequired) { const source = new VectorTileSource('id', options, { send: function () {} }); source.onAdd({ - transform: { angle: 0, pitch: 0, showCollisionBoxes: false } + transform: { angle: 0, pitch: 0, showCollisionBoxes: false }, + _getMapId: () => 1 }); source.on('error', (e) => { throw e.error; diff --git a/test/unit/util/mapbox.test.js b/test/unit/util/mapbox.test.js index 06a320388ae..82ef089d40e 100644 --- a/test/unit/util/mapbox.test.js +++ b/test/unit/util/mapbox.test.js @@ -381,15 +381,13 @@ test("mapbox", (t) => { t.test('does not POST event when previously stored data is on the same day', (t) => { const now = +Date.now(); - - window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ - anonId: uuid(), + window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, uuid()); + window.localStorage.setItem(`mapbox.eventData:${config.ACCESS_TOKEN}`, JSON.stringify({ lastSuccess: now })); // Post 5 seconds later withFixedDate(t, now + 5, () => event.postTurnstileEvent(mapboxTileURLs)); - t.false(window.server.requests.length); t.end(); }); @@ -397,8 +395,8 @@ test("mapbox", (t) => { t.test('POSTs event when previously stored anonId is not a valid uuid', (t) => { const now = +Date.now(); - window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ - anonId: 'anonymous', + window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, 'anonymous'); + window.localStorage.setItem(`mapbox.eventData:${config.ACCESS_TOKEN}`, JSON.stringify({ lastSuccess: now })); @@ -415,8 +413,8 @@ test("mapbox", (t) => { t.test('POSTs event when previously stored timestamp is more than 24 hours in the future', (t) => { const now = +Date.now(); - window.localStorage.setItem(`mapbox.turnstileEventData:${config.ACCESS_TOKEN}`, JSON.stringify({ - anonId: uuid(), + window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, uuid()); + window.localStorage.setItem(`mapbox.eventData:${config.ACCESS_TOKEN}`, JSON.stringify({ lastSuccess: now + ms25Hours // 24-hours later })); @@ -616,6 +614,271 @@ test("mapbox", (t) => { t.end(); }); + t.test('MapLoadEvent', (t) => { + let event; + let turnstileEvent; + t.beforeEach((callback) => { + window.useFakeXMLHttpRequest(); + event = new mapbox.MapLoadEvent(); + turnstileEvent = new mapbox.TurnstileEvent(); + callback(); + }); + + t.afterEach((callback) => { + window.restore(); + callback(); + }); + + t.test('mapbox.postMapLoadEvent', (t) => { + t.ok(mapbox.postMapLoadEvent); + t.end(); + }); + + t.test('does not POST when mapboxgl.ACCESS_TOKEN is not set', (t) => { + config.ACCESS_TOKEN = null; + + event.postMapLoadEvent(mapboxTileURLs, 1); + t.equal(window.server.requests.length, 0); + t.end(); + }); + + t.test('does not POST when url does not point to mapbox.com', (t) => { + event.postMapLoadEvent(nonMapboxTileURLs, 1); + + t.equal(window.server.requests.length, 0); + t.end(); + }); + + t.test('POSTs cn event when API_URL changes to cn endpoint', (t) => { + const previousUrl = config.API_URL; + config.API_URL = 'https://api.mapbox.cn'; + + event.postMapLoadEvent(mapboxTileURLs, 1); + + const req = window.server.requests[0]; + req.respond(200); + + t.true(req.url.indexOf('https://events.mapbox.cn') > -1); + config.API_URL = previousUrl; + t.end(); + }); + + t.test('with LocalStorage available', (t) => { + let prevLocalStorage; + t.beforeEach((callback) => { + prevLocalStorage = window.localStorage; + window.localStorage = { + data: {}, + setItem: function (id, val) { + this.data[id] = String(val); + }, + getItem: function (id) { + return this.data.hasOwnProperty(id) ? this.data[id] : undefined; + }, + removeItem: function (id) { + if (this.hasOwnProperty(id)) delete this[id]; + } + }; + callback(); + }); + + t.afterEach((callback) => { + window.localStorage = prevLocalStorage; + callback(); + }); + + t.test('generates new uuid when previously stored anonId is not a valid uuid', (t) => { + window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, JSON.stringify({ + anonId: 'anonymous' + })); + + event.postMapLoadEvent(mapboxTileURLs, 1); + const req = window.server.requests[0]; + req.respond(200); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.notEqual(reqBody.userId, 'anonymous'); + t.end(); + }); + + t.test('does not POST map.load event second time within same calendar day', (t) => { + let now = +Date.now(); + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + + //Post second event + const firstEvent = now; + now += (60 * 1000); // A bit later + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(firstEvent).toISOString()); + + t.end(); + }); + + t.test('does not POST map.load event second time when clock goes backwards less than a day', (t) => { + let now = +Date.now(); + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + + //Post second event + const firstEvent = now; + now -= (60 * 1000); // A bit earlier + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(firstEvent).toISOString()); + + t.end(); + }); + + t.test('POSTs map.load event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + + event.postMapLoadEvent(mapboxTileURLs, 1); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.new.*`); + + t.end(); + }); + + t.test('uses the same uuid as TurnstileEvent', (t) => { + const anonId = uuid(); + window.localStorage.setItem(`mapbox.eventData.uuid:${config.ACCESS_TOKEN}`, anonId); + turnstileEvent.postTurnstileEvent(mapboxTileURLs); + event.postMapLoadEvent(mapboxTileURLs, 1); + + + const turnstileReq = window.server.requests[0]; + turnstileReq.respond(200); + const mapLoadReq = window.server.requests[1]; + mapLoadReq.respond(200); + const turnstileBody = JSON.parse(turnstileReq.requestBody)[0]; + const loadBody = JSON.parse(mapLoadReq.requestBody)[0]; + + t.equal(turnstileBody.userId, loadBody.userId); + t.equal(turnstileBody.userId, anonId); + const turnstileEventData = JSON.parse(window.localStorage.getItem(`mapbox.eventData:${config.ACCESS_TOKEN}`)); + t.ok(turnstileEventData.lastSuccess); + t.end(); + }); + + t.end(); + }); + + t.test('when LocalStorage is not available', (t) => { + t.test('POSTs map.load event', (t) => { + event.postMapLoadEvent(mapboxTileURLs, 1); + + const req = window.server.requests[0]; + req.respond(200); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(req.url, `${config.EVENTS_URL}?access_token=key`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'map.load'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + + t.end(); + }); + + t.test('does not POST map.load multiple times for the same map instance', (t) => { + const now = Date.now(); + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + withFixedDate(t, now + 5, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + + const req = window.server.requests[0]; + req.respond(200); + + t.equal(window.server.requests.length, 1); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(reqBody.created, new Date(now).toISOString()); + + t.end(); + }); + + t.test('POSTs map.load event when access token changes', (t) => { + config.ACCESS_TOKEN = 'pk.new.*'; + + event.postMapLoadEvent(mapboxTileURLs, 1); + + const req = window.server.requests[0]; + req.respond(200); + + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(req.url, `${config.EVENTS_URL}?access_token=pk.new.*`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'map.load'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + + t.end(); + }); + + t.test('POSTs distinct map.load for multiple maps', (t) => { + event.postMapLoadEvent(mapboxTileURLs, 1); + const now = +Date.now(); + withFixedDate(t, now, ()=> event.postMapLoadEvent(mapboxTileURLs, 2)); + + let req = window.server.requests[0]; + req.respond(200); + + req = window.server.requests[1]; + req.respond(200); + const reqBody = JSON.parse(req.requestBody)[0]; + t.equal(req.url, `${config.EVENTS_URL}?access_token=key`); + t.equal(req.method, 'POST'); + t.equal(reqBody.event, 'map.load'); + t.equal(reqBody.sdkVersion, version); + t.ok(reqBody.userId); + t.equal(reqBody.created, new Date(now).toISOString()); + + t.end(); + }); + + t.test('Queues and POSTs map.load events when triggerred in quick succession by different maps', (t) => { + const now = Date.now(); + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 1)); + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 2)); + withFixedDate(t, now, () => event.postMapLoadEvent(mapboxTileURLs, 3)); + + const reqOne = window.server.requests[0]; + reqOne.respond(200); + let reqBody = JSON.parse(reqOne.requestBody)[0]; + t.equal(reqBody.created, new Date(now).toISOString()); + + const reqTwo = window.server.requests[1]; + reqTwo.respond(200); + reqBody = JSON.parse(reqTwo.requestBody)[0]; + t.equal(reqBody.created, new Date(now).toISOString()); + + const reqThree = window.server.requests[2]; + reqThree.respond(200); + reqBody = JSON.parse(reqThree.requestBody)[0]; + t.equal(reqBody.created, new Date(now).toISOString()); + + t.end(); + }); + + t.end(); + }); + + t.end(); + }); t.end(); });