diff --git a/services/web/app/src/Features/Analytics/AnalyticsController.js b/services/web/app/src/Features/Analytics/AnalyticsController.js index 30329616f0..f18cee429d 100644 --- a/services/web/app/src/Features/Analytics/AnalyticsController.js +++ b/services/web/app/src/Features/Analytics/AnalyticsController.js @@ -32,6 +32,7 @@ module.exports = { } const userId = SessionManager.getLoggedInUserId(req.session) || req.sessionID + delete req.body._csrf AnalyticsManager.recordEvent(userId, req.params.event, req.body) res.sendStatus(202) }, diff --git a/services/web/frontend/js/infrastructure/event-tracking.js b/services/web/frontend/js/infrastructure/event-tracking.js index efd67c51cc..8e1e2629f6 100644 --- a/services/web/frontend/js/infrastructure/event-tracking.js +++ b/services/web/frontend/js/infrastructure/event-tracking.js @@ -1,4 +1,3 @@ -import { postJSON } from './fetch-json' import sessionStorage from '../infrastructure/session-storage' const CACHE_KEY = 'mbEvents' @@ -10,11 +9,7 @@ export function send(category, action, label, value) { } export function sendMB(key, segmentation = {}) { - postJSON(`/event/${key}`, { body: segmentation, keepalive: true }).catch( - () => { - // ignore errors - } - ) + sendBeacon(key, segmentation) } export function sendMBOnce(key, segmentation = {}) { @@ -39,3 +34,13 @@ export function sendMBSampled(key, body = {}, rate = 0.01) { sendMB(key, body) } } + +function sendBeacon(key, data) { + if (!navigator || !navigator.sendBeacon) return + + data._csrf = window.csrfToken + const blob = new Blob([JSON.stringify(data)], { + type: 'application/json; charset=UTF-8', + }) + navigator.sendBeacon(`/event/${key}`, blob) +} diff --git a/services/web/frontend/js/main/event.js b/services/web/frontend/js/main/event.js index 889f72ab1f..160fecde0c 100644 --- a/services/web/frontend/js/main/event.js +++ b/services/web/frontend/js/main/event.js @@ -15,6 +15,7 @@ import moment from 'moment' import App from '../base' import '../modules/localStorage' +import { sendMB } from '../infrastructure/event-tracking' const CACHE_KEY = 'mbEvents' // keep track of how many heartbeats we've sent so we can calculate how @@ -89,21 +90,7 @@ App.factory('eventTracking', function ($http, localStorage) { return (nextHeartbeat = moment().add(backoffSecs, 'seconds').toDate()) }, - sendMB(key, segmentation) { - if (segmentation == null) { - segmentation = {} - } - fetch(`/event/${key}`, { - method: 'POST', - body: JSON.stringify(segmentation), - keepalive: true, - headers: { - 'X-CSRF-Token': window.csrfToken, - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }) - }, + sendMB, sendMBSampled(key, segmentation, rate = 0.01) { if (Math.random() < rate) { diff --git a/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js b/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js index c21a4bb469..686400614f 100644 --- a/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js +++ b/services/web/test/unit/src/Analytics/AnalyticsControllerTests.js @@ -61,21 +61,28 @@ describe('AnalyticsController', function () { describe('recordEvent', function () { beforeEach(function () { + const body = { + foo: 'stuff', + _csrf: 'atoken123', + } this.req = { params: { event: 'i_did_something', }, - body: 'stuff', + body, sessionID: 'sessionIDHere', session: {}, } + + this.expectedData = Object.assign({}, body) + delete this.expectedData._csrf }) it('should use the user_id', function (done) { this.SessionManager.getLoggedInUserId.returns('1234') this.controller.recordEvent(this.req, this.res) this.AnalyticsManager.recordEvent - .calledWith('1234', this.req.params.event, this.req.body) + .calledWith('1234', this.req.params.event, this.expectedData) .should.equal(true) done() }) @@ -83,7 +90,23 @@ describe('AnalyticsController', function () { it('should use the session id', function (done) { this.controller.recordEvent(this.req, this.res) this.AnalyticsManager.recordEvent - .calledWith(this.req.sessionID, this.req.params.event, this.req.body) + .calledWith( + this.req.sessionID, + this.req.params.event, + this.expectedData + ) + .should.equal(true) + done() + }) + + it('should remove the CSRF token before sending to the manager', function (done) { + this.controller.recordEvent(this.req, this.res) + this.AnalyticsManager.recordEvent + .calledWith( + this.req.sessionID, + this.req.params.event, + this.expectedData + ) .should.equal(true) done() })