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:
Robert Raposa
2019-02-27 16:50:51 -05:00
parent b24f6db050
commit f9f1c723cd
9 changed files with 322 additions and 47 deletions

View File

@@ -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);
});
}

View 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');
});
});
});

View 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;

View 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);
});
});