diff --git a/lib/api/api-router.js b/lib/api/api-router.js index 3923a2bb..479d25ed 100644 --- a/lib/api/api-router.js +++ b/lib/api/api-router.js @@ -57,7 +57,6 @@ const user = require('./middlewares/user'); const sendResponse = require('./middlewares/send-response'); const syntaxError = require('./middlewares/syntax-error'); const errorMiddleware = require('./middlewares/error-middleware'); -const pubSubMetrics = require('./middlewares/pubsub-metrics'); const MapRouter = require('./map/map-router'); const TemplateRouter = require('./template/template-router'); @@ -161,7 +160,10 @@ module.exports = class ApiRouter { }); namedMapProviderCacheReporter.start(); + const metricsBackend = new PubSubMetricsBackend(serverOptions.pubSubMetrics); + const collaborators = { + config: serverOptions, analysisStatusBackend, attributesBackend, dataviewBackend, @@ -181,13 +183,13 @@ module.exports = class ApiRouter { layergroupMetadata, namedMapProviderCache, tablesExtentBackend, - clusterBackend + clusterBackend, + metricsBackend }; + this.metadataBackend = metadataBackend; this.mapRouter = new MapRouter({ collaborators }); this.templateRouter = new TemplateRouter({ collaborators }); - this.metadataBackend = metadataBackend; - this.pubSubMetricsBackend = new PubSubMetricsBackend(this.serverOptions.pubSubMetrics); } route (app, routes) { @@ -220,11 +222,6 @@ module.exports = class ApiRouter { apiRouter.use(sendResponse()); apiRouter.use(syntaxError()); apiRouter.use(errorMiddleware()); - apiRouter.use(pubSubMetrics({ - enabled: this.serverOptions.pubSubMetrics.enabled, - metricsBackend: this.pubSubMetricsBackend, - logger: global.logger - })); paths.forEach(path => app.use(path, apiRouter)); }); diff --git a/lib/api/map/anonymous-map-controller.js b/lib/api/map/anonymous-map-controller.js index 508e1cd8..356ef338 100644 --- a/lib/api/map/anonymous-map-controller.js +++ b/lib/api/map/anonymous-map-controller.js @@ -23,6 +23,7 @@ const mapError = require('../middlewares/map-error'); const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider'); const rateLimit = require('../middlewares/rate-limit'); const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; +const metrics = require('../middlewares/pubsub-metrics'); module.exports = class AnonymousMapController { /** @@ -39,6 +40,7 @@ module.exports = class AnonymousMapController { * @constructor */ constructor ( + config, pgConnection, templateMaps, mapBackend, @@ -49,8 +51,10 @@ module.exports = class AnonymousMapController { mapConfigAdapter, statsBackend, authBackend, - layergroupMetadata + layergroupMetadata, + metricsBackend ) { + this.config = config; this.pgConnection = pgConnection; this.templateMaps = templateMaps; this.mapBackend = mapBackend; @@ -62,6 +66,7 @@ module.exports = class AnonymousMapController { this.statsBackend = statsBackend; this.authBackend = authBackend; this.layergroupMetadata = layergroupMetadata; + this.metricsBackend = metricsBackend; } route (mapRouter) { @@ -76,8 +81,26 @@ module.exports = class AnonymousMapController { const includeQuery = true; const label = 'ANONYMOUS LAYERGROUP'; const addContext = true; + const metricsTags = { + event: 'map_view', + attributes: { map_type: 'anonymous' }, + from: { + req: { + query: { client: 'client' } + }, + res: { + body: { layergroupid: 'map_id' } + } + } + }; return [ + metrics({ + enabled: this.config.pubSubMetrics.enabled, + metricsBackend: this.metricsBackend, + logger: global.logger, + tags: metricsTags + }), credentials(), authorize(this.authBackend), dbConnSetup(this.pgConnection), diff --git a/lib/api/map/map-router.js b/lib/api/map/map-router.js index c5070627..2ebccb54 100644 --- a/lib/api/map/map-router.js +++ b/lib/api/map/map-router.js @@ -15,6 +15,7 @@ const ClusteredFeaturesLayergroupController = require('./clustered-features-laye module.exports = class MapRouter { constructor ({ collaborators }) { const { + config, analysisStatusBackend, attributesBackend, dataviewBackend, @@ -34,7 +35,8 @@ module.exports = class MapRouter { layergroupMetadata, namedMapProviderCache, tablesExtentBackend, - clusterBackend + clusterBackend, + metricsBackend } = collaborators; this.analysisLayergroupController = new AnalysisLayergroupController( @@ -85,6 +87,7 @@ module.exports = class MapRouter { ); this.anonymousMapController = new AnonymousMapController( + config, pgConnection, templateMaps, mapBackend, @@ -95,10 +98,12 @@ module.exports = class MapRouter { mapConfigAdapter, statsBackend, authBackend, - layergroupMetadata + layergroupMetadata, + metricsBackend ); this.previewTemplateController = new PreviewTemplateController( + config, namedMapProviderCache, previewBackend, surrogateKeysCache, @@ -106,7 +111,8 @@ module.exports = class MapRouter { metadataBackend, pgConnection, authBackend, - userLimitsBackend + userLimitsBackend, + metricsBackend ); this.analysesController = new AnalysesCatalogController( diff --git a/lib/api/map/preview-template-controller.js b/lib/api/map/preview-template-controller.js index 989bb8e1..832dfacb 100644 --- a/lib/api/map/preview-template-controller.js +++ b/lib/api/map/preview-template-controller.js @@ -12,6 +12,7 @@ const lastModifiedHeader = require('../middlewares/last-modified-header'); const checkStaticImageFormat = require('../middlewares/check-static-image-format'); const rateLimit = require('../middlewares/rate-limit'); const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; +const metrics = require('../middlewares/pubsub-metrics'); const DEFAULT_ZOOM_CENTER = { zoom: 1, @@ -27,6 +28,7 @@ function numMapper (n) { module.exports = class PreviewTemplateController { constructor ( + config, namedMapProviderCache, previewBackend, surrogateKeysCache, @@ -34,8 +36,10 @@ module.exports = class PreviewTemplateController { metadataBackend, pgConnection, authBackend, - userLimitsBackend + userLimitsBackend, + metricsBackend ) { + this.config = config; this.namedMapProviderCache = namedMapProviderCache; this.previewBackend = previewBackend; this.surrogateKeysCache = surrogateKeysCache; @@ -44,6 +48,7 @@ module.exports = class PreviewTemplateController { this.pgConnection = pgConnection; this.authBackend = authBackend; this.userLimitsBackend = userLimitsBackend; + this.metricsBackend = metricsBackend; } route (mapRouter) { @@ -51,7 +56,23 @@ module.exports = class PreviewTemplateController { } middlewares () { + const metricsTags = { + event: 'map_view', + attributes: { map_type: 'static' }, + from: { + req: { + query: { client: 'client' } + } + } + }; + return [ + metrics({ + enabled: this.config.pubSubMetrics.enabled, + metricsBackend: this.metricsBackend, + logger: global.logger, + tags: metricsTags + }), credentials(), authorize(this.authBackend), dbConnSetup(this.pgConnection), diff --git a/lib/api/middlewares/pubsub-metrics.js b/lib/api/middlewares/pubsub-metrics.js index 8ef538a8..ecd77644 100644 --- a/lib/api/middlewares/pubsub-metrics.js +++ b/lib/api/middlewares/pubsub-metrics.js @@ -3,55 +3,58 @@ const EVENT_VERSION = '1'; const MAX_LENGTH = 100; -function pubSubMetrics ({ enabled, metricsBackend, logger }) { +module.exports = function pubSubMetrics ({ enabled, metricsBackend, logger, tags }) { if (!enabled) { return function pubSubMetricsDisabledMiddleware (req, res, next) { next(); }; } + if (!tags || !tags.event) { + throw new Error('Missing required "event" parameter to report metrics'); + } + return function pubSubMetricsMiddleware (req, res, next) { res.on('finish', () => { - const { event, attributes } = getEventData(req, res); + const { event, attributes } = getEventData(req, res, tags); - if (event) { - metricsBackend.send(event, attributes) - .then(() => logger.debug(`PubSubTracker: event '${event}' published succesfully`)) - .catch((error) => logger.error(`ERROR: pubsub middleware failed to publish event '${event}': ${error.message}`)); - } + metricsBackend.send(event, attributes) + .then(() => logger.debug(`PubSubTracker: event '${event}' published succesfully`)) + .catch((error) => logger.error(`ERROR: pubsub middleware failed to publish event '${event}': ${error.message}`)); }); return next(); }; -} +}; -function getEventData (req, res) { - const event = normalizedField(req.get('Carto-Event')); - const eventSource = normalizedField(req.get('Carto-Event-Source')); - const eventGroupId = normalizedField(req.get('Carto-Event-Group-Id')); +function getEventData (req, res, tags) { + const event = tags.event; + const extra = {}; + if (tags.from) { + if (tags.from.req) { + Object.assign(extra, getFromReq(req, tags.from.req)); + } - if (!event || !eventSource) { - return { event: undefined, attributes: undefined }; + if (tags.from.res) { + Object.assign(extra, getFromRes(res, tags.from.res)); + } } - const attributes = { - event_source: eventSource, - user_id: res.locals.userId, - response_code: res.statusCode.toString(), - source_domain: req.hostname, + const attributes = Object.assign({}, { + metrics_event: normalizedField(req.get('Carto-Event')), + event_source: normalizedField(req.get('Carto-Event-Source')), + event_group_id: normalizedField(req.get('Carto-Event-Group-Id')), event_time: new Date().toISOString(), + user_id: res.locals.userId, + user_agent: req.get('User-Agent'), + response_code: res.statusCode.toString(), + response_time: getResponseTime(res), + source_domain: req.hostname, event_version: EVENT_VERSION - }; + }, tags.attributes, extra); - if (eventGroupId) { - attributes.event_group_id = eventGroupId; - } - - const responseTime = getResponseTime(res); - - if (responseTime) { - attributes.response_time = responseTime.toString(); - } + // remove undefined properties + Object.keys(attributes).forEach(key => attributes[key] === undefined && delete attributes[key]); return { event, attributes }; } @@ -75,7 +78,47 @@ function getResponseTime (res) { return undefined; } - return stats.total; + return stats.total.toString(); } -module.exports = pubSubMetrics; +function getFromReq (req, { query = {}, body = {}, params = {}, headers = {} } = {}) { + const extra = {}; + + for (const [queryParam, eventName] of Object.entries(query)) { + extra[eventName] = req.query[queryParam]; + } + + for (const [bodyParam, eventName] of Object.entries(body)) { + extra[eventName] = req.body[bodyParam]; + } + + for (const [pathParam, eventName] of Object.entries(params)) { + extra[eventName] = req.params[pathParam]; + } + + for (const [header, eventName] of Object.entries(headers)) { + extra[eventName] = req.get(header); + } + + return extra; +} + +function getFromRes (res, { body = {}, headers = {}, locals = {} } = {}) { + const extra = {}; + + if (res.body) { + for (const [bodyParam, eventName] of Object.entries(body)) { + extra[eventName] = res.body[bodyParam]; + } + } + + for (const [header, eventName] of Object.entries(headers)) { + extra[eventName] = res.get(header); + } + + for (const [localParam, eventName] of Object.entries(locals)) { + extra[eventName] = res.locals[localParam]; + } + + return extra; +} diff --git a/lib/api/template/named-template-controller.js b/lib/api/template/named-template-controller.js index b7b3a865..e8891455 100644 --- a/lib/api/template/named-template-controller.js +++ b/lib/api/template/named-template-controller.js @@ -21,6 +21,7 @@ const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider'); const rateLimit = require('../middlewares/rate-limit'); const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; +const metrics = require('../middlewares/pubsub-metrics'); module.exports = class NamedMapController { /** @@ -38,6 +39,7 @@ module.exports = class NamedMapController { * @constructor */ constructor ( + config, pgConnection, templateMaps, mapBackend, @@ -48,8 +50,10 @@ module.exports = class NamedMapController { mapConfigAdapter, statsBackend, authBackend, - layergroupMetadata + layergroupMetadata, + metricsBackend ) { + this.config = config; this.pgConnection = pgConnection; this.templateMaps = templateMaps; this.mapBackend = mapBackend; @@ -61,6 +65,7 @@ module.exports = class NamedMapController { this.statsBackend = statsBackend; this.authBackend = authBackend; this.layergroupMetadata = layergroupMetadata; + this.metricsBackend = metricsBackend; } route (templateRouter) { @@ -74,8 +79,26 @@ module.exports = class NamedMapController { const includeQuery = false; const label = 'NAMED MAP LAYERGROUP'; const addContext = false; + const metricsTags = { + event: 'map_view', + attributes: { map_type: 'named' }, + from: { + req: { + query: { client: 'client' } + }, + res: { + body: { layergroupid: 'map_id' } + } + } + }; return [ + metrics({ + enabled: this.config.pubSubMetrics.enabled, + metricsBackend: this.metricsBackend, + logger: global.logger, + tags: metricsTags + }), credentials(), authorize(this.authBackend), dbConnSetup(this.pgConnection), diff --git a/lib/api/template/template-router.js b/lib/api/template/template-router.js index 81d7973f..7b254f9c 100644 --- a/lib/api/template/template-router.js +++ b/lib/api/template/template-router.js @@ -9,6 +9,7 @@ const TileTemplateController = require('./tile-template-controller'); module.exports = class TemplateRouter { constructor ({ collaborators }) { const { + config, pgConnection, templateMaps, mapBackend, @@ -21,10 +22,12 @@ module.exports = class TemplateRouter { authBackend, layergroupMetadata, namedMapProviderCache, - tileBackend + tileBackend, + metricsBackend } = collaborators; this.namedMapController = new NamedMapController( + config, pgConnection, templateMaps, mapBackend, @@ -35,7 +38,8 @@ module.exports = class TemplateRouter { mapConfigAdapter, statsBackend, authBackend, - layergroupMetadata + layergroupMetadata, + metricsBackend ); this.tileTemplateController = new TileTemplateController( diff --git a/test/integration/pubsub-metrics-test.js b/test/integration/pubsub-metrics-test.js index 553d157f..1ded5193 100644 --- a/test/integration/pubsub-metrics-test.js +++ b/test/integration/pubsub-metrics-test.js @@ -16,22 +16,25 @@ const mapConfig = { } ] }; -const template = { - version: '0.0.1', - name: 'metrics-template', - layergroup: { - version: '1.8.0', - layers: [ - { - type: 'cartodb', - options: { - sql: TestClient.SQL.ONE_POINT, - cartocss: TestClient.CARTOCSS.POINTS, - cartocss_version: '2.3.0' + +function templateBuilder ({ name }) { + return { + version: '0.0.1', + name: `metrics-template-${name}`, + layergroup: { + version: '1.8.0', + layers: [ + { + type: 'cartodb', + options: { + sql: TestClient.SQL.ONE_POINT, + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } } - } - ] - } + ] + } + }; }; describe('pubsub metrics middleware', function () { @@ -88,12 +91,14 @@ describe('pubsub metrics middleware', function () { }); it('should send event for map requests', function (done) { - const expectedEvent = 'event-test'; + const expectedEvent = 'map_view'; + const expectedMetricsEvent = 'event-test'; const expectedEventSource = 'event-source-test'; const expectedEventGroupId = '1'; const expectedResponseCode = '200'; + const expectedMapType = 'anonymous'; const extraHeaders = { - 'Carto-Event': expectedEvent, + 'Carto-Event': expectedMetricsEvent, 'Carto-Event-Source': expectedEventSource, 'Carto-Event-Group-Id': expectedEventGroupId }; @@ -108,20 +113,24 @@ describe('pubsub metrics middleware', function () { assert.strictEqual(typeof body.layergroupid, 'string'); assert.ok(this.pubSubMetricsBackendSendMethodCalled); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.event, expectedEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.metrics_event, expectedMetricsEvent); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_source, expectedEventSource); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_group_id, expectedEventGroupId); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.response_code, expectedResponseCode); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.map_type, expectedMapType); return testClient.drain(done); }); }); it('should normalized headers type and length', function (done) { + const expectedEvent = 'map_view'; const eventLong = 'If you are sending a text this long in a header you kind of deserve the worst, honestly. I mean this is not a header, it is almost a novel, and you do not see any Novel cookie here, right?'; - const expectedEvent = eventLong.trim().substr(0, 100); + const expectedMetricsEvent = eventLong.trim().substr(0, 100); const expectedEventGroupId = '1'; const expectedEventSource = 'test'; const expectedResponseCode = '200'; + const expectedMapType = 'anonymous'; const extraHeaders = { 'Carto-Event': eventLong, 'Carto-Event-Source': 'test', @@ -138,27 +147,41 @@ describe('pubsub metrics middleware', function () { assert.strictEqual(typeof body.layergroupid, 'string'); assert.ok(this.pubSubMetricsBackendSendMethodCalled); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.event, expectedEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.metrics_event, expectedMetricsEvent); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_source, expectedEventSource); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_group_id, expectedEventGroupId); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.response_code, expectedResponseCode); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.map_type, expectedMapType); return testClient.drain(done); }); }); it('should send event when error', function (done) { - const expectedEvent = 'event-test'; + const expectedEvent = 'map_view'; + const expectedMetricsEvent = 'event-test'; const expectedEventSource = 'event-source-test'; const expectedEventGroupId = '1'; const expectedResponseCode = '400'; + const expectedMapType = 'anonymous'; const extraHeaders = { - 'Carto-Event': expectedEvent, + 'Carto-Event': expectedMetricsEvent, 'Carto-Event-Source': expectedEventSource, 'Carto-Event-Group-Id': expectedEventGroupId }; const overrideServerOptions = { pubSubMetrics: { enabled: true, topic: 'topic-test' } }; - const emptyMapConfig = {}; - const testClient = new TestClient(emptyMapConfig, apikey, extraHeaders, overrideServerOptions); + const mapConfigMissingCartoCSS = { + version: '1.8.0', + layers: [ + { + options: { + sql: TestClient.SQL.ONE_POINT, + cartocss: TestClient.CARTOCSS.POINTS + } + } + ] + }; + const testClient = new TestClient(mapConfigMissingCartoCSS, apikey, extraHeaders, overrideServerOptions); const params = { response: { status: 400 } }; testClient.getLayergroup(params, (err, body) => { @@ -168,15 +191,17 @@ describe('pubsub metrics middleware', function () { assert.ok(this.pubSubMetricsBackendSendMethodCalled); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.event, expectedEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.metrics_event, expectedMetricsEvent); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_source, expectedEventSource); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_group_id, expectedEventGroupId); assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.response_code, expectedResponseCode); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.map_type, expectedMapType); return testClient.drain(done); }); }); - it('should send event for tile requests', function (done) { + it.skip('should send event for tile requests', function (done) { const expectedEvent = 'event-tile-test'; const expectedEventSource = 'event-source-tile-test'; const expectedEventGroupId = '12345'; @@ -204,7 +229,7 @@ describe('pubsub metrics middleware', function () { }); }); - it('should send event for errored tile requests', function (done) { + it.skip('should send event for errored tile requests', function (done) { const expectedEvent = 'event-tile-test'; const expectedEventSource = 'event-source-tile-test'; const expectedEventGroupId = '12345'; @@ -241,7 +266,98 @@ describe('pubsub metrics middleware', function () { }); }); - it('should send event for named map tile requests', function (done) { + it('should send event for named map requests', function (done) { + const expectedEvent = 'map_view'; + const expectedMetricsEvent = 'event-test'; + const expectedEventSource = 'event-source-test'; + const expectedEventGroupId = '1'; + const expectedResponseCode = '200'; + const expectedMapType = 'named'; + const extraHeaders = { + 'Carto-Event': expectedMetricsEvent, + 'Carto-Event-Source': expectedEventSource, + 'Carto-Event-Group-Id': expectedEventGroupId + }; + const overrideServerOptions = { pubSubMetrics: { enabled: true, topic: 'topic-test' } }; + const template = templateBuilder({ name: 'map' }); + const testClient = new TestClient(template, apikey, extraHeaders, overrideServerOptions); + + testClient.getLayergroup((err, body) => { + if (err) { + return done(err); + } + + assert.strictEqual(typeof body.layergroupid, 'string'); + assert.ok(this.pubSubMetricsBackendSendMethodCalled); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.event, expectedEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.metrics_event, expectedMetricsEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_source, expectedEventSource); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_group_id, expectedEventGroupId); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.response_code, expectedResponseCode); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.map_type, expectedMapType); + + return testClient.drain(done); + }); + }); + + it('should send event for errored named map requests', function (done) { + const expectedEvent = 'map_view'; + const expectedMetricsEvent = 'event-test'; + const expectedEventSource = 'event-source-test'; + const expectedEventGroupId = '1'; + const expectedResponseCode = '400'; + const expectedMapType = 'named'; + const extraHeaders = { + 'Carto-Event': expectedMetricsEvent, + 'Carto-Event-Source': expectedEventSource, + 'Carto-Event-Group-Id': expectedEventGroupId + }; + const overrideServerOptions = { pubSubMetrics: { enabled: true, topic: 'topic-test' } }; + const templateMissingCartoCSS = { + version: '0.0.1', + name: 'metrics-template', + layergroup: { + version: '1.8.0', + layers: [ + { + type: 'cartodb', + options: { + sql: TestClient.SQL.ONE_POINT, + cartocss: TestClient.CARTOCSS.POINTS + } + } + ] + } + }; + + const testClient = new TestClient(templateMissingCartoCSS, apikey, extraHeaders, overrideServerOptions); + + const params = { + response: { + status: 400, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } + } + }; + testClient.getLayergroup(params, (err, body) => { + if (err) { + return done(err); + } + + assert.ok(this.pubSubMetricsBackendSendMethodCalled); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.event, expectedEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.metrics_event, expectedMetricsEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_source, expectedEventSource); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_group_id, expectedEventGroupId); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.response_code, expectedResponseCode); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.map_type, expectedMapType); + + return testClient.drain(done); + }); + }); + + it.skip('should send event for named map tile requests', function (done) { const expectedEvent = 'event-named-map-tile-test'; const expectedEventSource = 'event-source-named-map-tile-test'; const expectedEventGroupId = '1'; @@ -252,6 +368,7 @@ describe('pubsub metrics middleware', function () { 'Carto-Event-Group-Id': expectedEventGroupId }; const overrideServerOptions = { pubSubMetrics: { enabled: true, topic: 'topic-test' } }; + const template = templateBuilder({ name: 'tile' }); const testClient = new TestClient(template, apikey, extraHeaders, overrideServerOptions); testClient.getTile(0, 0, 0, (err, body) => { @@ -268,4 +385,37 @@ describe('pubsub metrics middleware', function () { return testClient.drain(done); }); }); + + it('should send event for static named map requests', function (done) { + const expectedEvent = 'map_view'; + const expectedMetricsEvent = 'event-test'; + const expectedEventSource = 'event-source-test'; + const expectedEventGroupId = '1'; + const expectedResponseCode = '200'; + const expectedMapType = 'static'; + const extraHeaders = { + 'Carto-Event': expectedMetricsEvent, + 'Carto-Event-Source': expectedEventSource, + 'Carto-Event-Group-Id': expectedEventGroupId + }; + const overrideServerOptions = { pubSubMetrics: { enabled: true, topic: 'topic-test' } }; + const template = templateBuilder({ name: 'preview' }); + const testClient = new TestClient(template, apikey, extraHeaders, overrideServerOptions); + + testClient.getPreview(640, 480, {}, (err, res, body) => { + if (err) { + return done(err); + } + + assert.ok(this.pubSubMetricsBackendSendMethodCalled); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.event, expectedEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.metrics_event, expectedMetricsEvent); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_source, expectedEventSource); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.event_group_id, expectedEventGroupId); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.response_code, expectedResponseCode); + assert.strictEqual(this.pubSubMetricsBackendSendMethodCalledWith.attributes.map_type, expectedMapType); + + return testClient.drain(done); + }); + }); }); diff --git a/test/support/test-client.js b/test/support/test-client.js index cdb59eaf..0639d402 100644 --- a/test/support/test-client.js +++ b/test/support/test-client.js @@ -942,16 +942,8 @@ TestClient.prototype.getLayergroup = function (params, callback) { params = {}; } - if (!params.response) { - params.response = { - status: 200, - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - }; - } - - var url = '/api/v1/map'; + let url = '/api/v1/map'; + const urlNamed = url + '/named'; const headers = Object.assign({ host: 'localhost', 'Content-Type': 'application/json' }, self.extraHeaders); const queryParams = {}; @@ -968,30 +960,109 @@ TestClient.prototype.getLayergroup = function (params, callback) { url += '?' + qs.stringify(queryParams); } - assert.response(self.server, - { - url: url, - method: 'POST', - headers, - data: JSON.stringify(self.mapConfig) - }, - params.response, - function (res, err) { - var parsedBody; - // If there is a response, we are still interested in catching the created keys - // to be able to delete them on the .drain() method. - if (res) { - parsedBody = JSON.parse(res.body); - if (parsedBody.layergroupid) { - self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; - self.keysToDelete['user:localhost:mapviews:global'] = 5; - } - } - if (err) { - return callback(err); + var layergroupId; + + if (params.layergroupid) { + layergroupId = params.layergroupid; + } + + step( + function createTemplate () { + var next = this; + + if (!self.template) { + return next(); } - return callback(null, parsedBody); + if (!self.apiKey) { + return next(new Error('apiKey param is mandatory to create a new template')); + } + + params.placeholders = params.placeholders || {}; + + assert.response(self.server, + { + url: urlNamed + '?' + qs.stringify({ api_key: self.apiKey }), + method: 'POST', + headers, + data: JSON.stringify(self.template) + }, + { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } + }, + function (res, err) { + if (err) { + return next(err); + } + return next(null, JSON.parse(res.body).template_id); + } + ); + }, + function createLayergroup (err, templateId) { + var next = this; + + if (err) { + return next(err); + } + + if (layergroupId) { + return next(null, layergroupId); + } + + const data = templateId ? params.placeholders : self.mapConfig; + + if (!params.response) { + params.response = { + status: 200, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } + }; + } + + const queryParams = {}; + + if (self.apiKey) { + queryParams.api_key = self.apiKey; + } + + if (params.aggregation !== undefined) { + queryParams.aggregation = params.aggregation; + } + + const path = templateId + ? urlNamed + '/' + templateId + '?' + qs.stringify(queryParams) + : url; + + assert.response(self.server, + { + url: path, + method: 'POST', + headers, + data: JSON.stringify(data) + }, + params.response, + function (res, err) { + var parsedBody; + // If there is a response, we are still interested in catching the created keys + // to be able to delete them on the .drain() method. + if (res) { + parsedBody = JSON.parse(res.body); + if (parsedBody.layergroupid) { + self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0; + self.keysToDelete['user:localhost:mapviews:global'] = 5; + } + } + if (err) { + return callback(err); + } + + return callback(null, parsedBody); + } + ); } ); }; @@ -1687,3 +1758,52 @@ TestClient.prototype.getTemplate = function (params, callback) { return callback(err, res, body); }); }; + +TestClient.prototype.getPreview = function (width, height, params = {}, callback) { + this.createTemplate({}, (err, res, template) => { + if (err) { + return callback(err); + } + + params = Object.assign({ api_key: this.apiKey }, params); + const url = `/api/v1/map/static/named/${template.template_id}/${width}/${height}.png?${qs.stringify(params)}`; + const headers = Object.assign({ host: 'localhost' }, this.extraHeaders); + + const requestOptions = { + url: url, + method: 'GET', + headers, + encoding: 'binary' + }; + + const expectedResponse = Object.assign({ + status: 200, + headers: { + 'Content-Type': 'image/png' + } + }, params.response || {}); + + assert.response(this.server, requestOptions, expectedResponse, (res, err) => { + if (err) { + return callback(err); + } + + this.keysToDelete['user:localhost:mapviews:global'] = 5; + + let body; + switch (res.headers['content-type']) { + case 'image/png': + body = mapnik.Image.fromBytes(Buffer.from(res.body, 'binary')); + break; + case 'application/json; charset=utf-8': + body = JSON.parse(res.body); + break; + default: + body = res.body; + break; + } + + return callback(null, res, body); + }); + }); +};