From ec64fa8de2b9612b95fe2ba1ef62c08867a247ca Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Fri, 20 Mar 2026 16:24:11 -0400 Subject: [PATCH 1/5] feat: implement Rokt reporting service in kit - Add ErrorReportingService with Rokt-specific endpoints, headers, rate limiting - Add LoggingService for informational log reporting - Add RateLimiter for per-severity rate limiting - Register services with core SDK during initForwarder() - Add comprehensive test coverage for reporting, logging, and rate limiting --- src/Rokt-Kit.js | 296 +++++++++++++++++++ test/src/tests.js | 707 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1003 insertions(+) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index b1574fd..8458b7b 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -257,6 +257,40 @@ var constructor = function () { self.domain = domain; + // Register reporting services with the core SDK + var reportingConfig = { + loggingUrl: settings.loggingUrl, + errorUrl: settings.errorUrl, + isLoggingEnabled: + settings.isLoggingEnabled === 'true' || + settings.isLoggingEnabled === true, + }; + var errorReportingService = new ErrorReportingService( + reportingConfig, + generateIntegrationName(launcherOptions.integrationName), + window.__rokt_li_guid__ + ); + var loggingService = new LoggingService( + reportingConfig, + errorReportingService, + generateIntegrationName(launcherOptions.integrationName) + ); + + self.errorReportingService = errorReportingService; + self.loggingService = loggingService; + + if ( + window.mParticle && + window.mParticle.registerErrorReportingService + ) { + window.mParticle.registerErrorReportingService( + errorReportingService + ); + } + if (window.mParticle && window.mParticle.registerLoggingService) { + window.mParticle.registerLoggingService(loggingService); + } + if (testMode) { self.testHelpers = { generateLauncherScript: generateLauncherScript, @@ -855,6 +889,252 @@ function isString(value) { return typeof value === 'string'; } +// --- Reporting Services --- + +var ErrorCodes = { + UNKNOWN_ERROR: 'UNKNOWN_ERROR', + UNHANDLED_EXCEPTION: 'UNHANDLED_EXCEPTION', + IDENTITY_REQUEST: 'IDENTITY_REQUEST', +}; + +var WSDKErrorSeverity = { + ERROR: 'ERROR', + INFO: 'INFO', + WARNING: 'WARNING', +}; + +var DEFAULT_LOGGING_URL = 'apps.rokt-api.com/v1/log'; +var DEFAULT_ERROR_URL = 'apps.rokt-api.com/v1/errors'; +var RATE_LIMIT_PER_SEVERITY = 10; + +function RateLimiter() { + this._logCount = {}; +} + +RateLimiter.prototype.incrementAndCheck = function (severity) { + var count = this._logCount[severity] || 0; + var newCount = count + 1; + this._logCount[severity] = newCount; + return newCount > RATE_LIMIT_PER_SEVERITY; +}; + +function ErrorReportingService( + config, + sdkVersion, + launcherInstanceGuid, + rateLimiter +) { + var self = this; + self._loggingUrl = + 'https://' + ((config && config.loggingUrl) || DEFAULT_LOGGING_URL); + self._errorUrl = + 'https://' + ((config && config.errorUrl) || DEFAULT_ERROR_URL); + self._isLoggingEnabled = (config && config.isLoggingEnabled) || false; + self._sdkVersion = sdkVersion || ''; + self._launcherInstanceGuid = launcherInstanceGuid; + self._rateLimiter = rateLimiter || new RateLimiter(); + self._store = null; + self._reporter = 'mp-wsdk'; + self._isEnabled = _isReportingEnabled(self); +} + +function _isReportingEnabled(svc) { + return ( + _isDebugModeEnabled() || + (_isRoktDomainPresent() && svc._isLoggingEnabled) + ); +} + +function _isRoktDomainPresent() { + return typeof window !== 'undefined' && Boolean(window['ROKT_DOMAIN']); +} + +function _isDebugModeEnabled() { + return ( + typeof window !== 'undefined' && + window.location && + window.location.search && + window.location.search + .toLowerCase() + .indexOf('mp_enable_logging=true') !== -1 + ); +} + +function _getUrl() { + return typeof window !== 'undefined' && window.location + ? window.location.href + : undefined; +} + +function _getUserAgent() { + return typeof window !== 'undefined' && window.navigator + ? window.navigator.userAgent + : undefined; +} + +function _getVersion(svc) { + var integrationName = + svc._store && typeof svc._store.getIntegrationName === 'function' + ? svc._store.getIntegrationName() + : null; + return integrationName || 'mParticle_wsdkv_' + svc._sdkVersion; +} + +function _getIntegration(svc) { + var integrationName = + svc._store && typeof svc._store.getIntegrationName === 'function' + ? svc._store.getIntegrationName() + : null; + return integrationName || 'mp-wsdk'; +} + +function _getHeaders(svc) { + var headers = { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + 'rokt-launcher-version': _getVersion(svc), + 'rokt-wsdk-version': 'joint', + }; + + if (svc._launcherInstanceGuid) { + headers['rokt-launcher-instance-guid'] = svc._launcherInstanceGuid; + } + + var accountId = + svc._store && typeof svc._store.getRoktAccountId === 'function' + ? svc._store.getRoktAccountId() + : null; + if (accountId) { + headers['rokt-account-id'] = accountId; + } + + return headers; +} + +function _buildLogRequest(svc, severity, msg, code, stackTrace) { + return { + additionalInformation: { + message: msg, + version: _getVersion(svc), + }, + severity: severity, + code: code || ErrorCodes.UNKNOWN_ERROR, + url: _getUrl(), + deviceInfo: _getUserAgent(), + stackTrace: stackTrace, + reporter: svc._reporter, + integration: _getIntegration(svc), + }; +} + +function _canSendLog(svc, severity) { + return svc._isEnabled && !svc._rateLimiter.incrementAndCheck(severity); +} + +function _sendToServer(svc, url, severity, msg, code, stackTrace) { + if (!_canSendLog(svc, severity)) { + return; + } + + try { + var logRequest = _buildLogRequest(svc, severity, msg, code, stackTrace); + var payload = { + method: 'POST', + headers: _getHeaders(svc), + body: JSON.stringify(logRequest), + }; + fetch(url, payload).catch(function (error) { + console.error('ErrorReportingService: Failed to send log', error); + }); + } catch (error) { + console.error('ErrorReportingService: Failed to send log', error); + } +} + +ErrorReportingService.prototype.setStore = function (store) { + this._store = store; +}; + +ErrorReportingService.prototype.report = function (error) { + if (!error) { + return; + } + var severity = error.severity || WSDKErrorSeverity.ERROR; + var url = + severity === WSDKErrorSeverity.INFO ? this._loggingUrl : this._errorUrl; + _sendToServer( + this, + url, + severity, + error.message, + error.code, + error.stackTrace + ); +}; + +function LoggingService(config, errorReportingService, sdkVersion) { + var self = this; + self._loggingUrl = + 'https://' + ((config && config.loggingUrl) || DEFAULT_LOGGING_URL); + self._errorReportingService = errorReportingService; + self._sdkVersion = sdkVersion || ''; + self._store = null; + self._reporter = 'mp-wsdk'; + self._isLoggingEnabled = (config && config.isLoggingEnabled) || false; + self._rateLimiter = new RateLimiter(); + self._isEnabled = _isReportingEnabled(self); + self._launcherInstanceGuid = + errorReportingService && errorReportingService._launcherInstanceGuid; +} + +LoggingService.prototype.setStore = function (store) { + this._store = store; +}; + +LoggingService.prototype.log = function (entry) { + if (!entry) { + return; + } + var self = this; + if (!_canSendLog(self, WSDKErrorSeverity.INFO)) { + return; + } + + try { + var logRequest = _buildLogRequest( + self, + WSDKErrorSeverity.INFO, + entry.message, + entry.code + ); + var payload = { + method: 'POST', + headers: _getHeaders(self), + body: JSON.stringify(logRequest), + }; + fetch(self._loggingUrl, payload).catch(function (error) { + console.error('LoggingService: Failed to send log', error); + if (self._errorReportingService) { + self._errorReportingService.report({ + message: + 'LoggingService: Failed to send log: ' + error.message, + code: ErrorCodes.UNKNOWN_ERROR, + severity: WSDKErrorSeverity.ERROR, + }); + } + }); + } catch (error) { + console.error('LoggingService: Failed to send log', error); + if (self._errorReportingService) { + self._errorReportingService.report({ + message: 'LoggingService: Failed to send log: ' + error.message, + code: ErrorCodes.UNKNOWN_ERROR, + severity: WSDKErrorSeverity.ERROR, + }); + } + } +}; + if (window && window.mParticle && window.mParticle.addForwarder) { window.mParticle.addForwarder({ name: name, @@ -863,6 +1143,22 @@ if (window && window.mParticle && window.mParticle.addForwarder) { }); } +// Expose reporting classes for kit consumers and tests +if (typeof window !== 'undefined') { + window.RoktReporting = { + ErrorReportingService: ErrorReportingService, + LoggingService: LoggingService, + RateLimiter: RateLimiter, + ErrorCodes: ErrorCodes, + WSDKErrorSeverity: WSDKErrorSeverity, + }; +} + module.exports = { register: register, + ErrorReportingService: ErrorReportingService, + LoggingService: LoggingService, + RateLimiter: RateLimiter, + ErrorCodes: ErrorCodes, + WSDKErrorSeverity: WSDKErrorSeverity, }; diff --git a/test/src/tests.js b/test/src/tests.js index 2d05873..f80fccd 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -5,6 +5,9 @@ const packageVersion = require('../../package.json').version; const sdkVersion = 'mParticle_wsdkv_1.2.3'; const kitVersion = 'kitv_' + packageVersion; +// Reporting service classes are exposed on window by the kit IIFE +// They will be accessed as window.ErrorReportingService, etc. + const waitForCondition = async (conditionFn, timeout = 200, interval = 10) => { return new Promise((resolve, reject) => { const startTime = Date.now(); @@ -22,6 +25,16 @@ const waitForCondition = async (conditionFn, timeout = 200, interval = 10) => { }; describe('Rokt Forwarder', () => { + // Reporting service classes from the kit IIFE (loaded via window.RoktReporting) + var ErrorReportingService = + window.RoktReporting && window.RoktReporting.ErrorReportingService; + var LoggingService = + window.RoktReporting && window.RoktReporting.LoggingService; + var RateLimiter = window.RoktReporting && window.RoktReporting.RateLimiter; + var ErrorCodes = window.RoktReporting && window.RoktReporting.ErrorCodes; + var WSDKErrorSeverity = + window.RoktReporting && window.RoktReporting.WSDKErrorSeverity; + var EventType = { Unknown: 0, Navigation: 1, @@ -5009,4 +5022,698 @@ describe('Rokt Forwarder', () => { (controlIframe === undefined).should.be.true(); }); }); + + describe('ErrorReportingService', () => { + var originalFetch; + var mockFetch; + var fetchCalls; + var originalROKT_DOMAIN; + var originalLocation; + var originalNavigator; + + beforeEach(() => { + fetchCalls = []; + mockFetch = function (url, options) { + fetchCalls.push({ url: url, options: options }); + return Promise.resolve({ ok: true }); + }; + originalFetch = window.fetch; + window.fetch = mockFetch; + + originalROKT_DOMAIN = window.ROKT_DOMAIN; + window.ROKT_DOMAIN = 'set'; + + originalLocation = window.location; + originalNavigator = window.navigator; + }); + + afterEach(() => { + window.fetch = originalFetch; + window.ROKT_DOMAIN = originalROKT_DOMAIN; + try { + Object.defineProperty(window, 'location', { + writable: true, + value: originalLocation, + }); + } catch (_e) {} + try { + Object.defineProperty(window, 'navigator', { + writable: true, + value: originalNavigator, + }); + } catch (_e) {} + }); + + it('should send error reports to the errors endpoint', () => { + var service = new ErrorReportingService( + { + errorUrl: 'test.com/v1/errors', + loggingUrl: 'test.com/v1/log', + isLoggingEnabled: true, + }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test error', + code: ErrorCodes.UNHANDLED_EXCEPTION, + severity: WSDKErrorSeverity.ERROR, + stackTrace: 'stack', + }); + + fetchCalls.length.should.equal(1); + fetchCalls[0].url.should.equal('https://test.com/v1/errors'); + var body = JSON.parse(fetchCalls[0].options.body); + body.severity.should.equal('ERROR'); + body.code.should.equal('UNHANDLED_EXCEPTION'); + body.stackTrace.should.equal('stack'); + body.reporter.should.equal('mp-wsdk'); + }); + + it('should send warning reports to the errors endpoint', () => { + var service = new ErrorReportingService( + { + errorUrl: 'test.com/v1/errors', + loggingUrl: 'test.com/v1/log', + isLoggingEnabled: true, + }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test warning', + code: ErrorCodes.UNHANDLED_EXCEPTION, + severity: WSDKErrorSeverity.WARNING, + }); + + fetchCalls.length.should.equal(1); + fetchCalls[0].url.should.equal('https://test.com/v1/errors'); + var body = JSON.parse(fetchCalls[0].options.body); + body.severity.should.equal('WARNING'); + }); + + it('should send info reports to the logging endpoint', () => { + var service = new ErrorReportingService( + { + errorUrl: 'test.com/v1/errors', + loggingUrl: 'test.com/v1/log', + isLoggingEnabled: true, + }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'info message', + code: ErrorCodes.UNHANDLED_EXCEPTION, + severity: WSDKErrorSeverity.INFO, + }); + + fetchCalls.length.should.equal(1); + fetchCalls[0].url.should.equal('https://test.com/v1/log'); + var body = JSON.parse(fetchCalls[0].options.body); + body.severity.should.equal('INFO'); + }); + + it('should not send when ROKT_DOMAIN is missing and feature flag is off', () => { + window.ROKT_DOMAIN = undefined; + var service = new ErrorReportingService( + { isLoggingEnabled: false }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'should not send', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls.length.should.equal(0); + }); + + it('should not send when feature flag is off and debug mode is off', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: false }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'should not send', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls.length.should.equal(0); + }); + + it('should send when debug mode is enabled even without ROKT_DOMAIN', () => { + window.ROKT_DOMAIN = undefined; + // Set debug mode via location search using history.pushState + var originalSearch = window.location.search; + window.history.pushState( + {}, + '', + window.location.pathname + '?mp_enable_logging=true' + ); + + var service = new ErrorReportingService( + { isLoggingEnabled: false }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'debug message', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls.length.should.equal(1); + + // Restore original URL + window.history.pushState( + {}, + '', + window.location.pathname + originalSearch + ); + }); + + it('should include correct headers', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls.length.should.equal(1); + var headers = fetchCalls[0].options.headers; + headers['Accept'].should.equal('text/plain;charset=UTF-8'); + headers['Content-Type'].should.equal('application/json'); + headers['rokt-launcher-instance-guid'].should.equal('test-guid'); + headers['rokt-wsdk-version'].should.equal('joint'); + }); + + it('should omit rokt-launcher-instance-guid header when guid is undefined', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + undefined + ); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls.length.should.equal(1); + var headers = fetchCalls[0].options.headers; + ( + headers['rokt-launcher-instance-guid'] === undefined + ).should.be.true(); + }); + + it('should include rokt-account-id header when store provides it', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + service.setStore({ + getRoktAccountId: function () { + return '1234567890'; + }, + getIntegrationName: function () { + return null; + }, + }); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.WARNING, + }); + + fetchCalls.length.should.equal(1); + fetchCalls[0].options.headers['rokt-account-id'].should.equal( + '1234567890' + ); + }); + + it('should not include rokt-account-id header when store does not provide it', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + service.setStore({ + getRoktAccountId: function () { + return null; + }, + getIntegrationName: function () { + return null; + }, + }); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls.length.should.equal(1); + ( + fetchCalls[0].options.headers['rokt-account-id'] === undefined + ).should.be.true(); + }); + + it('should use default UNKNOWN_ERROR code when code is not provided', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.ERROR, + }); + + var body = JSON.parse(fetchCalls[0].options.body); + body.code.should.equal('UNKNOWN_ERROR'); + }); + + it('should use default Rokt URLs when not configured', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test error', + severity: WSDKErrorSeverity.ERROR, + }); + + fetchCalls[0].url.should.equal( + 'https://apps.rokt-api.com/v1/errors' + ); + + service.report({ + message: 'test info', + severity: WSDKErrorSeverity.INFO, + }); + + fetchCalls[1].url.should.equal('https://apps.rokt-api.com/v1/log'); + }); + + it('should include all required fields in the log request body', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + service.setStore({ + getRoktAccountId: function () { + return null; + }, + getIntegrationName: function () { + return 'test-integration'; + }, + }); + + service.report({ + message: 'error message', + code: ErrorCodes.IDENTITY_REQUEST, + severity: WSDKErrorSeverity.ERROR, + stackTrace: 'stack trace here', + }); + + var body = JSON.parse(fetchCalls[0].options.body); + body.additionalInformation.message.should.equal('error message'); + body.additionalInformation.version.should.equal('test-integration'); + body.severity.should.equal('ERROR'); + body.code.should.equal('IDENTITY_REQUEST'); + body.stackTrace.should.equal('stack trace here'); + body.reporter.should.equal('mp-wsdk'); + body.integration.should.equal('test-integration'); + }); + + it('should use default integration values when store has no integration name', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.ERROR, + }); + + var body = JSON.parse(fetchCalls[0].options.body); + body.reporter.should.equal('mp-wsdk'); + body.integration.should.equal('mp-wsdk'); + body.additionalInformation.version.should.equal( + 'mParticle_wsdkv_1.0.0' + ); + }); + + it('should not throw when fetch fails', (done) => { + window.fetch = function () { + return Promise.reject(new Error('Network failure')); + }; + var consoleErrors = []; + var originalConsoleError = console.error; + console.error = function () { + consoleErrors.push(Array.prototype.slice.call(arguments)); + }; + + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + service.report({ + message: 'test', + severity: WSDKErrorSeverity.ERROR, + }); + + // Wait for the promise rejection to be handled + setTimeout(function () { + consoleErrors.length.should.be.above(0); + consoleErrors[0][0].should.equal( + 'ErrorReportingService: Failed to send log' + ); + console.error = originalConsoleError; + done(); + }, 50); + }); + + it('should not send when report is called with null', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + service.report(null); + fetchCalls.length.should.equal(0); + }); + }); + + describe('LoggingService', () => { + var originalFetch; + var mockFetch; + var fetchCalls; + var originalROKT_DOMAIN; + + beforeEach(() => { + fetchCalls = []; + mockFetch = function (url, options) { + fetchCalls.push({ url: url, options: options }); + return Promise.resolve({ ok: true }); + }; + originalFetch = window.fetch; + window.fetch = mockFetch; + + originalROKT_DOMAIN = window.ROKT_DOMAIN; + window.ROKT_DOMAIN = 'set'; + }); + + afterEach(() => { + window.fetch = originalFetch; + window.ROKT_DOMAIN = originalROKT_DOMAIN; + }); + + it('should always send to the logging endpoint with severity INFO', () => { + var errorService = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + var service = new LoggingService( + { loggingUrl: 'test.com/v1/log', isLoggingEnabled: true }, + errorService, + '1.0.0' + ); + + service.log({ + message: 'log entry', + code: ErrorCodes.UNKNOWN_ERROR, + }); + + fetchCalls.length.should.equal(1); + fetchCalls[0].url.should.equal('https://test.com/v1/log'); + var body = JSON.parse(fetchCalls[0].options.body); + body.severity.should.equal('INFO'); + body.additionalInformation.message.should.equal('log entry'); + }); + + it('should report failure through ErrorReportingService on fetch error', (done) => { + var errorReports = []; + var errorService = { + _launcherInstanceGuid: 'test-guid', + report: function (error) { + errorReports.push(error); + }, + }; + window.fetch = function () { + return Promise.reject(new Error('Network failure')); + }; + var originalConsoleError = console.error; + console.error = function () {}; + + var service = new LoggingService( + { isLoggingEnabled: true }, + errorService, + '1.0.0' + ); + + service.log({ message: 'test' }); + + setTimeout(function () { + errorReports.length.should.be.above(0); + errorReports[0].severity.should.equal('ERROR'); + errorReports[0].message.should.containEql('Failed to send log'); + console.error = originalConsoleError; + done(); + }, 50); + }); + + it('should not send when log is called with null', () => { + var errorService = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + var service = new LoggingService( + { isLoggingEnabled: true }, + errorService, + '1.0.0' + ); + + service.log(null); + fetchCalls.length.should.equal(0); + }); + }); + + describe('RateLimiter', () => { + it('should allow up to 10 logs per severity then rate limit', () => { + var limiter = new RateLimiter(); + for (var i = 0; i < 10; i++) { + limiter.incrementAndCheck('ERROR').should.be.false(); + } + limiter.incrementAndCheck('ERROR').should.be.true(); + limiter.incrementAndCheck('ERROR').should.be.true(); + }); + + it('should allow up to 10 warning logs then rate limit', () => { + var limiter = new RateLimiter(); + for (var i = 0; i < 10; i++) { + limiter.incrementAndCheck('WARNING').should.be.false(); + } + limiter.incrementAndCheck('WARNING').should.be.true(); + }); + + it('should allow up to 10 info logs then rate limit', () => { + var limiter = new RateLimiter(); + for (var i = 0; i < 10; i++) { + limiter.incrementAndCheck('INFO').should.be.false(); + } + limiter.incrementAndCheck('INFO').should.be.true(); + }); + + it('should track rate limits independently per severity', () => { + var limiter = new RateLimiter(); + for (var i = 0; i < 10; i++) { + limiter.incrementAndCheck('ERROR'); + } + limiter.incrementAndCheck('ERROR').should.be.true(); + limiter.incrementAndCheck('WARNING').should.be.false(); + }); + }); + + describe('ErrorReportingService rate limiting', () => { + var originalFetch; + var fetchCalls; + var originalROKT_DOMAIN; + + beforeEach(() => { + fetchCalls = []; + originalFetch = window.fetch; + window.fetch = function (url, options) { + fetchCalls.push({ url: url, options: options }); + return Promise.resolve({ ok: true }); + }; + + originalROKT_DOMAIN = window.ROKT_DOMAIN; + window.ROKT_DOMAIN = 'set'; + }); + + afterEach(() => { + window.fetch = originalFetch; + window.ROKT_DOMAIN = originalROKT_DOMAIN; + }); + + it('should rate limit after 10 errors', () => { + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid' + ); + + for (var i = 0; i < 15; i++) { + service.report({ + message: 'error ' + i, + severity: WSDKErrorSeverity.ERROR, + }); + } + + fetchCalls.length.should.equal(10); + }); + + it('should rate limit with custom rate limiter', () => { + var count = 0; + var customLimiter = { + incrementAndCheck: function () { + return ++count > 3; + }, + }; + + var service = new ErrorReportingService( + { isLoggingEnabled: true }, + '1.0.0', + 'test-guid', + customLimiter + ); + + for (var i = 0; i < 5; i++) { + service.report({ + message: 'error ' + i, + severity: WSDKErrorSeverity.ERROR, + }); + } + + fetchCalls.length.should.equal(3); + }); + }); + + describe('Reporting service registration', () => { + it('should register services with mParticle if methods exist', async () => { + var registeredErrorService = null; + var registeredLoggingService = null; + + window.mParticle.registerErrorReportingService = function ( + service + ) { + registeredErrorService = service; + }; + window.mParticle.registerLoggingService = function (service) { + registeredLoggingService = service; + }; + + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + window.mParticle.Rokt.attachKitCalled = false; + window.mParticle.Rokt.attachKit = async (kit) => { + window.mParticle.Rokt.attachKitCalled = true; + window.mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + window.mParticle.Rokt.filters = { + userAttributesFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '123'; + }, + }, + }; + + await mParticle.forwarder.init( + { + accountId: '123456', + isLoggingEnabled: 'true', + }, + reportService.cb, + true, + null, + {} + ); + + (registeredErrorService !== null).should.be.true(); + (registeredLoggingService !== null).should.be.true(); + (typeof registeredErrorService.report).should.equal('function'); + (typeof registeredLoggingService.log).should.equal('function'); + + // Cleanup + delete window.mParticle.registerErrorReportingService; + delete window.mParticle.registerLoggingService; + }); + + it('should not throw when registration methods do not exist', async () => { + delete window.mParticle.registerErrorReportingService; + delete window.mParticle.registerLoggingService; + + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + window.mParticle.Rokt.attachKitCalled = false; + window.mParticle.Rokt.attachKit = async (kit) => { + window.mParticle.Rokt.attachKitCalled = true; + window.mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + window.mParticle.Rokt.filters = { + userAttributesFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '123'; + }, + }, + }; + + // Should not throw + await mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true, + null, + {} + ); + + window.mParticle.forwarder.isInitialized.should.not.be.undefined(); + }); + }); }); From 648c9903086886d095dac337200d47f4dafa62f9 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Mon, 23 Mar 2026 13:45:03 -0400 Subject: [PATCH 2/5] refactor: simplify reporting services, remove store pattern - Replace setStore/store pattern with constructor params (integrationName, accountId) - Fix doubled generateIntegrationName() call - Add rateLimiter param to LoggingService for symmetry - Move reporting class exposure from window.RoktReporting to testHelpers - Add temporary debug logs for manual testing --- src/Rokt-Kit.js | 89 ++++++++++++++++++++------------------- test/src/tests.js | 104 ++++++++++++++++++++++++---------------------- 2 files changed, 99 insertions(+), 94 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 8458b7b..e8db1bb 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -267,18 +267,26 @@ var constructor = function () { }; var errorReportingService = new ErrorReportingService( reportingConfig, - generateIntegrationName(launcherOptions.integrationName), - window.__rokt_li_guid__ + self.integrationName, + window.__rokt_li_guid__, + settings.accountId ); var loggingService = new LoggingService( reportingConfig, errorReportingService, - generateIntegrationName(launcherOptions.integrationName) + self.integrationName, + settings.accountId ); self.errorReportingService = errorReportingService; self.loggingService = loggingService; + console.debug('[RoktKit] Reporting config:', reportingConfig); + console.debug( + '[RoktKit] Reporting enabled:', + errorReportingService._isEnabled + ); + if ( window.mParticle && window.mParticle.registerErrorReportingService @@ -286,9 +294,13 @@ var constructor = function () { window.mParticle.registerErrorReportingService( errorReportingService ); + console.debug( + '[RoktKit] Registered ErrorReportingService with core SDK' + ); } if (window.mParticle && window.mParticle.registerLoggingService) { window.mParticle.registerLoggingService(loggingService); + console.debug('[RoktKit] Registered LoggingService with core SDK'); } if (testMode) { @@ -306,6 +318,11 @@ var constructor = function () { setAllowedOriginHash: function (hash) { _allowedOriginHash = hash; }, + ErrorReportingService: ErrorReportingService, + LoggingService: LoggingService, + RateLimiter: RateLimiter, + ErrorCodes: ErrorCodes, + WSDKErrorSeverity: WSDKErrorSeverity, }; attachLauncher(accountId, launcherOptions); return; @@ -920,8 +937,9 @@ RateLimiter.prototype.incrementAndCheck = function (severity) { function ErrorReportingService( config, - sdkVersion, + integrationName, launcherInstanceGuid, + accountId, rateLimiter ) { var self = this; @@ -930,10 +948,10 @@ function ErrorReportingService( self._errorUrl = 'https://' + ((config && config.errorUrl) || DEFAULT_ERROR_URL); self._isLoggingEnabled = (config && config.isLoggingEnabled) || false; - self._sdkVersion = sdkVersion || ''; + self._integrationName = integrationName || ''; self._launcherInstanceGuid = launcherInstanceGuid; + self._accountId = accountId || null; self._rateLimiter = rateLimiter || new RateLimiter(); - self._store = null; self._reporter = 'mp-wsdk'; self._isEnabled = _isReportingEnabled(self); } @@ -973,19 +991,11 @@ function _getUserAgent() { } function _getVersion(svc) { - var integrationName = - svc._store && typeof svc._store.getIntegrationName === 'function' - ? svc._store.getIntegrationName() - : null; - return integrationName || 'mParticle_wsdkv_' + svc._sdkVersion; + return svc._integrationName || ''; } function _getIntegration(svc) { - var integrationName = - svc._store && typeof svc._store.getIntegrationName === 'function' - ? svc._store.getIntegrationName() - : null; - return integrationName || 'mp-wsdk'; + return svc._integrationName || ''; } function _getHeaders(svc) { @@ -1000,12 +1010,8 @@ function _getHeaders(svc) { headers['rokt-launcher-instance-guid'] = svc._launcherInstanceGuid; } - var accountId = - svc._store && typeof svc._store.getRoktAccountId === 'function' - ? svc._store.getRoktAccountId() - : null; - if (accountId) { - headers['rokt-account-id'] = accountId; + if (svc._accountId) { + headers['rokt-account-id'] = svc._accountId; } return headers; @@ -1033,6 +1039,11 @@ function _canSendLog(svc, severity) { function _sendToServer(svc, url, severity, msg, code, stackTrace) { if (!_canSendLog(svc, severity)) { + console.debug( + '[RoktKit] Log suppressed (disabled or rate limited):', + severity, + msg + ); return; } @@ -1043,6 +1054,7 @@ function _sendToServer(svc, url, severity, msg, code, stackTrace) { headers: _getHeaders(svc), body: JSON.stringify(logRequest), }; + console.debug('[RoktKit] Sending', severity, 'to', url, logRequest); fetch(url, payload).catch(function (error) { console.error('ErrorReportingService: Failed to send log', error); }); @@ -1051,10 +1063,6 @@ function _sendToServer(svc, url, severity, msg, code, stackTrace) { } } -ErrorReportingService.prototype.setStore = function (store) { - this._store = store; -}; - ErrorReportingService.prototype.report = function (error) { if (!error) { return; @@ -1072,25 +1080,27 @@ ErrorReportingService.prototype.report = function (error) { ); }; -function LoggingService(config, errorReportingService, sdkVersion) { +function LoggingService( + config, + errorReportingService, + integrationName, + accountId, + rateLimiter +) { var self = this; self._loggingUrl = 'https://' + ((config && config.loggingUrl) || DEFAULT_LOGGING_URL); self._errorReportingService = errorReportingService; - self._sdkVersion = sdkVersion || ''; - self._store = null; + self._integrationName = integrationName || ''; + self._accountId = accountId || null; self._reporter = 'mp-wsdk'; self._isLoggingEnabled = (config && config.isLoggingEnabled) || false; - self._rateLimiter = new RateLimiter(); + self._rateLimiter = rateLimiter || new RateLimiter(); self._isEnabled = _isReportingEnabled(self); self._launcherInstanceGuid = errorReportingService && errorReportingService._launcherInstanceGuid; } -LoggingService.prototype.setStore = function (store) { - this._store = store; -}; - LoggingService.prototype.log = function (entry) { if (!entry) { return; @@ -1143,17 +1153,6 @@ if (window && window.mParticle && window.mParticle.addForwarder) { }); } -// Expose reporting classes for kit consumers and tests -if (typeof window !== 'undefined') { - window.RoktReporting = { - ErrorReportingService: ErrorReportingService, - LoggingService: LoggingService, - RateLimiter: RateLimiter, - ErrorCodes: ErrorCodes, - WSDKErrorSeverity: WSDKErrorSeverity, - }; -} - module.exports = { register: register, ErrorReportingService: ErrorReportingService, diff --git a/test/src/tests.js b/test/src/tests.js index f80fccd..e57cc01 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -5,8 +5,7 @@ const packageVersion = require('../../package.json').version; const sdkVersion = 'mParticle_wsdkv_1.2.3'; const kitVersion = 'kitv_' + packageVersion; -// Reporting service classes are exposed on window by the kit IIFE -// They will be accessed as window.ErrorReportingService, etc. +// Reporting service classes are exposed via testHelpers after kit init const waitForCondition = async (conditionFn, timeout = 200, interval = 10) => { return new Promise((resolve, reject) => { @@ -25,15 +24,12 @@ const waitForCondition = async (conditionFn, timeout = 200, interval = 10) => { }; describe('Rokt Forwarder', () => { - // Reporting service classes from the kit IIFE (loaded via window.RoktReporting) - var ErrorReportingService = - window.RoktReporting && window.RoktReporting.ErrorReportingService; - var LoggingService = - window.RoktReporting && window.RoktReporting.LoggingService; - var RateLimiter = window.RoktReporting && window.RoktReporting.RateLimiter; - var ErrorCodes = window.RoktReporting && window.RoktReporting.ErrorCodes; - var WSDKErrorSeverity = - window.RoktReporting && window.RoktReporting.WSDKErrorSeverity; + // Reporting service classes from testHelpers (populated after kit init) + var ErrorReportingService; + var LoggingService; + var RateLimiter; + var ErrorCodes; + var WSDKErrorSeverity; var EventType = { Unknown: 0, @@ -161,7 +157,41 @@ describe('Rokt Forwarder', () => { this.currentLauncher = function () {}; }; - before(() => {}); + before(async () => { + // Run a minimal init in testMode to populate testHelpers + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + window.mParticle.Rokt.attachKit = async (kit) => { + window.mParticle.Rokt.kit = kit; + Promise.resolve(); + }; + window.mParticle.Rokt.filters = { + userAttributesFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '123'; + }, + }, + }; + await mParticle.forwarder.init( + { accountId: '000000' }, + reportService.cb, + true, + null, + {} + ); + + var testHelpers = window.mParticle.forwarder.testHelpers; + ErrorReportingService = + testHelpers && testHelpers.ErrorReportingService; + LoggingService = testHelpers && testHelpers.LoggingService; + RateLimiter = testHelpers && testHelpers.RateLimiter; + ErrorCodes = testHelpers && testHelpers.ErrorCodes; + WSDKErrorSeverity = testHelpers && testHelpers.WSDKErrorSeverity; + }); beforeEach(() => { window.Rokt = new MockRoktForwarder(); @@ -5238,20 +5268,13 @@ describe('Rokt Forwarder', () => { ).should.be.true(); }); - it('should include rokt-account-id header when store provides it', () => { + it('should include rokt-account-id header when accountId is provided', () => { var service = new ErrorReportingService( { isLoggingEnabled: true }, - '1.0.0', - 'test-guid' + 'test-integration', + 'test-guid', + '1234567890' ); - service.setStore({ - getRoktAccountId: function () { - return '1234567890'; - }, - getIntegrationName: function () { - return null; - }, - }); service.report({ message: 'test', @@ -5264,20 +5287,12 @@ describe('Rokt Forwarder', () => { ); }); - it('should not include rokt-account-id header when store does not provide it', () => { + it('should not include rokt-account-id header when accountId is not provided', () => { var service = new ErrorReportingService( { isLoggingEnabled: true }, - '1.0.0', + 'test-integration', 'test-guid' ); - service.setStore({ - getRoktAccountId: function () { - return null; - }, - getIntegrationName: function () { - return null; - }, - }); service.report({ message: 'test', @@ -5333,17 +5348,9 @@ describe('Rokt Forwarder', () => { it('should include all required fields in the log request body', () => { var service = new ErrorReportingService( { isLoggingEnabled: true }, - '1.0.0', + 'test-integration', 'test-guid' ); - service.setStore({ - getRoktAccountId: function () { - return null; - }, - getIntegrationName: function () { - return 'test-integration'; - }, - }); service.report({ message: 'error message', @@ -5362,10 +5369,10 @@ describe('Rokt Forwarder', () => { body.integration.should.equal('test-integration'); }); - it('should use default integration values when store has no integration name', () => { + it('should use empty integration values when no integration name is provided', () => { var service = new ErrorReportingService( { isLoggingEnabled: true }, - '1.0.0', + '', 'test-guid' ); @@ -5376,10 +5383,8 @@ describe('Rokt Forwarder', () => { var body = JSON.parse(fetchCalls[0].options.body); body.reporter.should.equal('mp-wsdk'); - body.integration.should.equal('mp-wsdk'); - body.additionalInformation.version.should.equal( - 'mParticle_wsdkv_1.0.0' - ); + body.integration.should.equal(''); + body.additionalInformation.version.should.equal(''); }); it('should not throw when fetch fails', (done) => { @@ -5607,8 +5612,9 @@ describe('Rokt Forwarder', () => { var service = new ErrorReportingService( { isLoggingEnabled: true }, - '1.0.0', + 'test-integration', 'test-guid', + null, customLimiter ); From 8ef754a93c191d0967e31d9a2109e94a7713c615 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Mon, 23 Mar 2026 13:50:01 -0400 Subject: [PATCH 3/5] chore: remove temporary debug logs from reporting services --- src/Rokt-Kit.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index e8db1bb..220cd9d 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -281,12 +281,6 @@ var constructor = function () { self.errorReportingService = errorReportingService; self.loggingService = loggingService; - console.debug('[RoktKit] Reporting config:', reportingConfig); - console.debug( - '[RoktKit] Reporting enabled:', - errorReportingService._isEnabled - ); - if ( window.mParticle && window.mParticle.registerErrorReportingService @@ -294,13 +288,9 @@ var constructor = function () { window.mParticle.registerErrorReportingService( errorReportingService ); - console.debug( - '[RoktKit] Registered ErrorReportingService with core SDK' - ); } if (window.mParticle && window.mParticle.registerLoggingService) { window.mParticle.registerLoggingService(loggingService); - console.debug('[RoktKit] Registered LoggingService with core SDK'); } if (testMode) { @@ -1039,11 +1029,6 @@ function _canSendLog(svc, severity) { function _sendToServer(svc, url, severity, msg, code, stackTrace) { if (!_canSendLog(svc, severity)) { - console.debug( - '[RoktKit] Log suppressed (disabled or rate limited):', - severity, - msg - ); return; } @@ -1054,7 +1039,6 @@ function _sendToServer(svc, url, severity, msg, code, stackTrace) { headers: _getHeaders(svc), body: JSON.stringify(logRequest), }; - console.debug('[RoktKit] Sending', severity, 'to', url, logRequest); fetch(url, payload).catch(function (error) { console.error('ErrorReportingService: Failed to send log', error); }); From 1a21d817e114d5f33290897d655ab84c4f270a89 Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Mon, 23 Mar 2026 14:20:21 -0400 Subject: [PATCH 4/5] refactor: update registration calls to use underscore-prefixed methods Match core SDK rename of registerErrorReportingService/registerLoggingService to _registerErrorReportingService/_registerLoggingService. --- src/Rokt-Kit.js | 8 ++++---- test/src/tests.js | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 220cd9d..ed84d3a 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -283,14 +283,14 @@ var constructor = function () { if ( window.mParticle && - window.mParticle.registerErrorReportingService + window.mParticle._registerErrorReportingService ) { - window.mParticle.registerErrorReportingService( + window.mParticle._registerErrorReportingService( errorReportingService ); } - if (window.mParticle && window.mParticle.registerLoggingService) { - window.mParticle.registerLoggingService(loggingService); + if (window.mParticle && window.mParticle._registerLoggingService) { + window.mParticle._registerLoggingService(loggingService); } if (testMode) { diff --git a/test/src/tests.js b/test/src/tests.js index e57cc01..0804b2a 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -5634,12 +5634,12 @@ describe('Rokt Forwarder', () => { var registeredErrorService = null; var registeredLoggingService = null; - window.mParticle.registerErrorReportingService = function ( + window.mParticle._registerErrorReportingService = function ( service ) { registeredErrorService = service; }; - window.mParticle.registerLoggingService = function (service) { + window.mParticle._registerLoggingService = function (service) { registeredLoggingService = service; }; @@ -5680,13 +5680,13 @@ describe('Rokt Forwarder', () => { (typeof registeredLoggingService.log).should.equal('function'); // Cleanup - delete window.mParticle.registerErrorReportingService; - delete window.mParticle.registerLoggingService; + delete window.mParticle._registerErrorReportingService; + delete window.mParticle._registerLoggingService; }); it('should not throw when registration methods do not exist', async () => { - delete window.mParticle.registerErrorReportingService; - delete window.mParticle.registerLoggingService; + delete window.mParticle._registerErrorReportingService; + delete window.mParticle._registerLoggingService; window.Rokt = new MockRoktForwarder(); window.mParticle.Rokt = window.Rokt; From 10a3994c584fd0cb531765ab4d78226972df3eab Mon Sep 17 00:00:00 2001 From: Alexander Sapountzis Date: Mon, 23 Mar 2026 14:54:38 -0400 Subject: [PATCH 5/5] refactor: extract ReportingTransport, compose into services Extract shared transport logic (headers, rate limiting, request building, fetch) into ReportingTransport. ErrorReportingService and LoggingService now each hold a transport instance and delegate sending to it. - ErrorReportingService: owns errorUrl only, handles ERROR/WARNING - LoggingService: owns loggingUrl only, handles INFO, falls back to error reporter on failure via onError callback - Each service gets its own transport with independent rate limiting --- src/Rokt-Kit.js | 219 ++++++++++++++++++++++------------------------ test/src/tests.js | 30 +++---- 2 files changed, 116 insertions(+), 133 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index ed84d3a..58de26d 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -275,6 +275,7 @@ var constructor = function () { reportingConfig, errorReportingService, self.integrationName, + window.__rokt_li_guid__, settings.accountId ); @@ -308,6 +309,7 @@ var constructor = function () { setAllowedOriginHash: function (hash) { _allowedOriginHash = hash; }, + ReportingTransport: ReportingTransport, ErrorReportingService: ErrorReportingService, LoggingService: LoggingService, RateLimiter: RateLimiter, @@ -925,7 +927,9 @@ RateLimiter.prototype.incrementAndCheck = function (severity) { return newCount > RATE_LIMIT_PER_SEVERITY; }; -function ErrorReportingService( +// --- ReportingTransport: shared transport layer for reporting services --- + +function ReportingTransport( config, integrationName, launcherInstanceGuid, @@ -933,10 +937,6 @@ function ErrorReportingService( rateLimiter ) { var self = this; - self._loggingUrl = - 'https://' + ((config && config.loggingUrl) || DEFAULT_LOGGING_URL); - self._errorUrl = - 'https://' + ((config && config.errorUrl) || DEFAULT_ERROR_URL); self._isLoggingEnabled = (config && config.isLoggingEnabled) || false; self._integrationName = integrationName || ''; self._launcherInstanceGuid = launcherInstanceGuid; @@ -946,10 +946,67 @@ function ErrorReportingService( self._isEnabled = _isReportingEnabled(self); } -function _isReportingEnabled(svc) { +ReportingTransport.prototype.send = function ( + url, + severity, + msg, + code, + stackTrace, + onError +) { + if (!this._isEnabled || this._rateLimiter.incrementAndCheck(severity)) { + return; + } + + try { + var logRequest = { + additionalInformation: { + message: msg, + version: this._integrationName || '', + }, + severity: severity, + code: code || ErrorCodes.UNKNOWN_ERROR, + url: _getUrl(), + deviceInfo: _getUserAgent(), + stackTrace: stackTrace, + reporter: this._reporter, + integration: this._integrationName || '', + }; + var headers = { + Accept: 'text/plain;charset=UTF-8', + 'Content-Type': 'application/json', + 'rokt-launcher-version': this._integrationName || '', + 'rokt-wsdk-version': 'joint', + }; + if (this._launcherInstanceGuid) { + headers['rokt-launcher-instance-guid'] = this._launcherInstanceGuid; + } + if (this._accountId) { + headers['rokt-account-id'] = this._accountId; + } + var payload = { + method: 'POST', + headers: headers, + body: JSON.stringify(logRequest), + }; + fetch(url, payload).catch(function (error) { + console.error('ReportingTransport: Failed to send log', error); + if (onError) { + onError(error); + } + }); + } catch (error) { + console.error('ReportingTransport: Failed to send log', error); + if (onError) { + onError(error); + } + } +}; + +function _isReportingEnabled(transport) { return ( _isDebugModeEnabled() || - (_isRoktDomainPresent() && svc._isLoggingEnabled) + (_isRoktDomainPresent() && transport._isLoggingEnabled) ); } @@ -980,71 +1037,24 @@ function _getUserAgent() { : undefined; } -function _getVersion(svc) { - return svc._integrationName || ''; -} - -function _getIntegration(svc) { - return svc._integrationName || ''; -} - -function _getHeaders(svc) { - var headers = { - Accept: 'text/plain;charset=UTF-8', - 'Content-Type': 'application/json', - 'rokt-launcher-version': _getVersion(svc), - 'rokt-wsdk-version': 'joint', - }; - - if (svc._launcherInstanceGuid) { - headers['rokt-launcher-instance-guid'] = svc._launcherInstanceGuid; - } - - if (svc._accountId) { - headers['rokt-account-id'] = svc._accountId; - } - - return headers; -} - -function _buildLogRequest(svc, severity, msg, code, stackTrace) { - return { - additionalInformation: { - message: msg, - version: _getVersion(svc), - }, - severity: severity, - code: code || ErrorCodes.UNKNOWN_ERROR, - url: _getUrl(), - deviceInfo: _getUserAgent(), - stackTrace: stackTrace, - reporter: svc._reporter, - integration: _getIntegration(svc), - }; -} - -function _canSendLog(svc, severity) { - return svc._isEnabled && !svc._rateLimiter.incrementAndCheck(severity); -} - -function _sendToServer(svc, url, severity, msg, code, stackTrace) { - if (!_canSendLog(svc, severity)) { - return; - } +// --- ErrorReportingService: handles ERROR and WARNING severity --- - try { - var logRequest = _buildLogRequest(svc, severity, msg, code, stackTrace); - var payload = { - method: 'POST', - headers: _getHeaders(svc), - body: JSON.stringify(logRequest), - }; - fetch(url, payload).catch(function (error) { - console.error('ErrorReportingService: Failed to send log', error); - }); - } catch (error) { - console.error('ErrorReportingService: Failed to send log', error); - } +function ErrorReportingService( + config, + integrationName, + launcherInstanceGuid, + accountId, + rateLimiter +) { + this._transport = new ReportingTransport( + config, + integrationName, + launcherInstanceGuid, + accountId, + rateLimiter + ); + this._errorUrl = + 'https://' + ((config && config.errorUrl) || DEFAULT_ERROR_URL); } ErrorReportingService.prototype.report = function (error) { @@ -1052,11 +1062,8 @@ ErrorReportingService.prototype.report = function (error) { return; } var severity = error.severity || WSDKErrorSeverity.ERROR; - var url = - severity === WSDKErrorSeverity.INFO ? this._loggingUrl : this._errorUrl; - _sendToServer( - this, - url, + this._transport.send( + this._errorUrl, severity, error.message, error.code, @@ -1064,25 +1071,26 @@ ErrorReportingService.prototype.report = function (error) { ); }; +// --- LoggingService: handles INFO severity --- + function LoggingService( config, errorReportingService, integrationName, + launcherInstanceGuid, accountId, rateLimiter ) { - var self = this; - self._loggingUrl = + this._transport = new ReportingTransport( + config, + integrationName, + launcherInstanceGuid, + accountId, + rateLimiter + ); + this._loggingUrl = 'https://' + ((config && config.loggingUrl) || DEFAULT_LOGGING_URL); - self._errorReportingService = errorReportingService; - self._integrationName = integrationName || ''; - self._accountId = accountId || null; - self._reporter = 'mp-wsdk'; - self._isLoggingEnabled = (config && config.isLoggingEnabled) || false; - self._rateLimiter = rateLimiter || new RateLimiter(); - self._isEnabled = _isReportingEnabled(self); - self._launcherInstanceGuid = - errorReportingService && errorReportingService._launcherInstanceGuid; + this._errorReportingService = errorReportingService; } LoggingService.prototype.log = function (entry) { @@ -1090,24 +1098,13 @@ LoggingService.prototype.log = function (entry) { return; } var self = this; - if (!_canSendLog(self, WSDKErrorSeverity.INFO)) { - return; - } - - try { - var logRequest = _buildLogRequest( - self, - WSDKErrorSeverity.INFO, - entry.message, - entry.code - ); - var payload = { - method: 'POST', - headers: _getHeaders(self), - body: JSON.stringify(logRequest), - }; - fetch(self._loggingUrl, payload).catch(function (error) { - console.error('LoggingService: Failed to send log', error); + self._transport.send( + self._loggingUrl, + WSDKErrorSeverity.INFO, + entry.message, + entry.code, + undefined, + function (error) { if (self._errorReportingService) { self._errorReportingService.report({ message: @@ -1116,17 +1113,8 @@ LoggingService.prototype.log = function (entry) { severity: WSDKErrorSeverity.ERROR, }); } - }); - } catch (error) { - console.error('LoggingService: Failed to send log', error); - if (self._errorReportingService) { - self._errorReportingService.report({ - message: 'LoggingService: Failed to send log: ' + error.message, - code: ErrorCodes.UNKNOWN_ERROR, - severity: WSDKErrorSeverity.ERROR, - }); } - } + ); }; if (window && window.mParticle && window.mParticle.addForwarder) { @@ -1139,6 +1127,7 @@ if (window && window.mParticle && window.mParticle.addForwarder) { module.exports = { register: register, + ReportingTransport: ReportingTransport, ErrorReportingService: ErrorReportingService, LoggingService: LoggingService, RateLimiter: RateLimiter, diff --git a/test/src/tests.js b/test/src/tests.js index 0804b2a..87b9ea9 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -25,6 +25,7 @@ const waitForCondition = async (conditionFn, timeout = 200, interval = 10) => { describe('Rokt Forwarder', () => { // Reporting service classes from testHelpers (populated after kit init) + var _ReportingTransport; var ErrorReportingService; var LoggingService; var RateLimiter; @@ -185,6 +186,7 @@ describe('Rokt Forwarder', () => { ); var testHelpers = window.mParticle.forwarder.testHelpers; + _ReportingTransport = testHelpers && testHelpers.ReportingTransport; ErrorReportingService = testHelpers && testHelpers.ErrorReportingService; LoggingService = testHelpers && testHelpers.LoggingService; @@ -5098,7 +5100,6 @@ describe('Rokt Forwarder', () => { var service = new ErrorReportingService( { errorUrl: 'test.com/v1/errors', - loggingUrl: 'test.com/v1/log', isLoggingEnabled: true, }, '1.0.0', @@ -5125,7 +5126,6 @@ describe('Rokt Forwarder', () => { var service = new ErrorReportingService( { errorUrl: 'test.com/v1/errors', - loggingUrl: 'test.com/v1/log', isLoggingEnabled: true, }, '1.0.0', @@ -5144,11 +5144,10 @@ describe('Rokt Forwarder', () => { body.severity.should.equal('WARNING'); }); - it('should send info reports to the logging endpoint', () => { + it('should send info reports to the error endpoint (error service only handles error url)', () => { var service = new ErrorReportingService( { errorUrl: 'test.com/v1/errors', - loggingUrl: 'test.com/v1/log', isLoggingEnabled: true, }, '1.0.0', @@ -5162,7 +5161,7 @@ describe('Rokt Forwarder', () => { }); fetchCalls.length.should.equal(1); - fetchCalls[0].url.should.equal('https://test.com/v1/log'); + fetchCalls[0].url.should.equal('https://test.com/v1/errors'); var body = JSON.parse(fetchCalls[0].options.body); body.severity.should.equal('INFO'); }); @@ -5321,7 +5320,7 @@ describe('Rokt Forwarder', () => { body.code.should.equal('UNKNOWN_ERROR'); }); - it('should use default Rokt URLs when not configured', () => { + it('should use default Rokt error URL when not configured', () => { var service = new ErrorReportingService( { isLoggingEnabled: true }, '1.0.0', @@ -5336,13 +5335,6 @@ describe('Rokt Forwarder', () => { fetchCalls[0].url.should.equal( 'https://apps.rokt-api.com/v1/errors' ); - - service.report({ - message: 'test info', - severity: WSDKErrorSeverity.INFO, - }); - - fetchCalls[1].url.should.equal('https://apps.rokt-api.com/v1/log'); }); it('should include all required fields in the log request body', () => { @@ -5412,7 +5404,7 @@ describe('Rokt Forwarder', () => { setTimeout(function () { consoleErrors.length.should.be.above(0); consoleErrors[0][0].should.equal( - 'ErrorReportingService: Failed to send log' + 'ReportingTransport: Failed to send log' ); console.error = originalConsoleError; done(); @@ -5464,7 +5456,8 @@ describe('Rokt Forwarder', () => { var service = new LoggingService( { loggingUrl: 'test.com/v1/log', isLoggingEnabled: true }, errorService, - '1.0.0' + '1.0.0', + 'test-guid' ); service.log({ @@ -5482,7 +5475,6 @@ describe('Rokt Forwarder', () => { it('should report failure through ErrorReportingService on fetch error', (done) => { var errorReports = []; var errorService = { - _launcherInstanceGuid: 'test-guid', report: function (error) { errorReports.push(error); }, @@ -5496,7 +5488,8 @@ describe('Rokt Forwarder', () => { var service = new LoggingService( { isLoggingEnabled: true }, errorService, - '1.0.0' + '1.0.0', + 'test-guid' ); service.log({ message: 'test' }); @@ -5519,7 +5512,8 @@ describe('Rokt Forwarder', () => { var service = new LoggingService( { isLoggingEnabled: true }, errorService, - '1.0.0' + '1.0.0', + 'test-guid' ); service.log(null);