Add Logging Service and event fixes.
- Original NewRelicService lifted from edx-portal. - Fixed bug with snake case of event data. - Added error handling for logEvent. - Added tests for logEvent. ARCH-430
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import apiClient from '../config/apiClient';
|
||||
import { configuration } from '../config/environment';
|
||||
import { snakeCaseObject } from '../services/utils';
|
||||
import LoggingService from '../services/LoggingService';
|
||||
|
||||
const eventLogApiBaseUrl = `${configuration.LMS_BASE_URL}/event`;
|
||||
|
||||
@@ -14,14 +15,16 @@ function handleTrackEvents(eventName, properties) {
|
||||
// Sends events to tracking log and downstream
|
||||
// TODO: Determine consistent naming for eventName vs eventType and properties v eventData.
|
||||
function logEvent(eventType, eventData) {
|
||||
snakeCaseObject(eventData, { deep: true });
|
||||
const snakeEventData = snakeCaseObject(eventData, { deep: true });
|
||||
const serverData = {
|
||||
event_type: eventType,
|
||||
event: eventData,
|
||||
event: snakeEventData,
|
||||
page: window.location.href,
|
||||
};
|
||||
// TODO: ARCH-430: Send errors to New Relic.
|
||||
return apiClient.post(eventLogApiBaseUrl, serverData);
|
||||
return apiClient.post(eventLogApiBaseUrl, serverData)
|
||||
.catch((error) => {
|
||||
LoggingService.logAPIErrorResponse(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
57
src/analytics/analytics.test.js
Normal file
57
src/analytics/analytics.test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { logEvent } from './analytics';
|
||||
import { configuration } from '../config/environment';
|
||||
import LoggingService from '../services/LoggingService';
|
||||
import apiClient from '../config/apiClient';
|
||||
|
||||
jest.mock('../services/LoggingService');
|
||||
jest.mock('../config/apiClient');
|
||||
|
||||
const eventType = 'test.event';
|
||||
const eventData = {
|
||||
testShallow: 'test-shallow',
|
||||
testObject: {
|
||||
testDeep: 'test-deep',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
apiClient.mockClear();
|
||||
LoggingService.mockClear();
|
||||
});
|
||||
|
||||
|
||||
describe('analytics logEvent', () => {
|
||||
it('posts expected data when successful', () => {
|
||||
jest.spyOn(apiClient, 'post').mockResolvedValue(undefined);
|
||||
|
||||
expect.assertions(3);
|
||||
return logEvent(eventType, eventData)
|
||||
.then(() => {
|
||||
expect(apiClient.post.mock.calls.length).toEqual(1);
|
||||
expect(apiClient.post.mock.calls[0][0]).toEqual(`${configuration.LMS_BASE_URL}/event`);
|
||||
expect(apiClient.post.mock.calls[0][1]).toEqual({
|
||||
event_type: 'test.event',
|
||||
event: {
|
||||
test_shallow: 'test-shallow',
|
||||
test_object: {
|
||||
test_deep: 'test-deep',
|
||||
},
|
||||
},
|
||||
page: window.location.href,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls LoggingService.logAPIErrorResponse on error', () => {
|
||||
LoggingService.logAPIErrorResponse = jest.fn();
|
||||
jest.spyOn(apiClient, 'post').mockRejectedValue('test-error');
|
||||
|
||||
expect.assertions(2);
|
||||
return logEvent(eventType, eventData)
|
||||
.then(() => {
|
||||
expect(LoggingService.logAPIErrorResponse.mock.calls.length).toBe(1);
|
||||
expect(LoggingService.logAPIErrorResponse.mock.calls[0][0]).toEqual('test-error');
|
||||
});
|
||||
});
|
||||
});
|
||||
32
src/services/LoggingService.js
Normal file
32
src/services/LoggingService.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Logs info and errors to NewRelic and console.
|
||||
*
|
||||
* Requires the NewRelic Browser JavaScript snippet.
|
||||
*/
|
||||
class LoggingService {
|
||||
static logInfo(message) {
|
||||
if (typeof newrelic !== 'undefined') {
|
||||
newrelic.addPageAction('INFO', { message });
|
||||
}
|
||||
}
|
||||
|
||||
static logError(error) {
|
||||
if (typeof newrelic !== 'undefined') {
|
||||
newrelic.noticeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
static logAPIErrorResponse(error) {
|
||||
let { message } = error;
|
||||
if (error.response) {
|
||||
message = `${error.response.status} ${error.response.config.url} ${JSON.stringify(error.response.data)}`;
|
||||
} else if (error.request) {
|
||||
message = `${error.request.status} ${error.request.responseURL} ${error.request.responseText}`;
|
||||
} else if (error.stack) {
|
||||
message = error.stack;
|
||||
}
|
||||
this.logError(new Error(`API request failed: ${message}`));
|
||||
}
|
||||
}
|
||||
|
||||
export default LoggingService;
|
||||
66
src/services/LoggingService.test.js
Normal file
66
src/services/LoggingService.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import LoggingService from './LoggingService';
|
||||
|
||||
global.newrelic = {
|
||||
addPageAction: jest.fn(),
|
||||
noticeError: jest.fn(),
|
||||
};
|
||||
|
||||
describe('logInfo', () => {
|
||||
it('calls New Relic client to log message if the client is available', () => {
|
||||
const message = 'Test log';
|
||||
LoggingService.logInfo(message);
|
||||
expect(global.newrelic.addPageAction).toHaveBeenCalledWith('INFO', { message });
|
||||
});
|
||||
});
|
||||
|
||||
describe('logError', () => {
|
||||
it('calls New Relic client to log error if the client is available', () => {
|
||||
const error = new Error('Failed!');
|
||||
LoggingService.logError(error);
|
||||
expect(global.newrelic.noticeError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logAPIErrorResponse', () => {
|
||||
it('calls New Relic client to log error when error has request object', () => {
|
||||
const error = {
|
||||
request: {
|
||||
status: 400,
|
||||
responseURL: 'http://example.com',
|
||||
responseText: 'Very bad request',
|
||||
},
|
||||
};
|
||||
const message = `${error.request.status} ${error.request.responseURL} ${error.request.responseText}`;
|
||||
const expectedError = new Error(`API request failed: ${message}`);
|
||||
LoggingService.logAPIErrorResponse(error);
|
||||
expect(global.newrelic.noticeError).toHaveBeenCalledWith(expectedError);
|
||||
});
|
||||
it('calls New Relic client to log error when error has response object', () => {
|
||||
const error = {
|
||||
response: {
|
||||
status: 400,
|
||||
config: {
|
||||
url: 'http://example.com',
|
||||
},
|
||||
data: {
|
||||
detail: 'Very bad request',
|
||||
},
|
||||
},
|
||||
};
|
||||
const message = `${error.response.status} ${error.response.config.url} ${JSON.stringify(error.response.data)}`;
|
||||
const expectedError = new Error(`API request failed: ${message}`);
|
||||
LoggingService.logAPIErrorResponse(error);
|
||||
expect(global.newrelic.noticeError).toHaveBeenCalledWith(expectedError);
|
||||
});
|
||||
it('calls New Relic client to log error when error has stack object', () => {
|
||||
const error = {
|
||||
stack: `TypeError: Cannot read property 'uuid' of undefined
|
||||
at portalConfiguration (webpack:///./src/data/reducers/portalConfiguration.js?:35:43)
|
||||
at combination (webpack:///./node_modules/redux/es/combineReducers.js?:125:29)
|
||||
at dispatch (webpack:///./node_modules/redux/es/createStore.js?:170:22)`,
|
||||
};
|
||||
const expectedError = new Error(`API request failed: ${error.stack}`);
|
||||
LoggingService.logAPIErrorResponse(error);
|
||||
expect(global.newrelic.noticeError).toHaveBeenCalledWith(expectedError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user