Skip to content

Commit

Permalink
feat(actionRecordMiddleware): issues/52 limited action log (#561)
Browse files Browse the repository at this point in the history
* actionRecordMiddleware, session store state actions
* downloadHelpers, helpers, download strings as files
  • Loading branch information
cdcabrera committed Feb 16, 2021
1 parent 7208660 commit cf460f3
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ REACT_APP_UI_DISABLED_TABLE=false
REACT_APP_UI_DISABLED_TABLE_HOSTS=false
REACT_APP_UI_DISABLED_TABLE_SUBSCRIPTIONS=false
REACT_APP_UI_LOGGER_ID=curiosity
REACT_APP_UI_LOGGER_FILE=curiosity_debug_log_{0}.json
REACT_APP_UI_WINDOW_ID=curiosity

REACT_APP_AJAX_TIMEOUT=60000
Expand Down
4 changes: 3 additions & 1 deletion config/cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"fadein",
"flyout",
"generatedid",
"HHmmss",
"ibmpower",
"ibmz",
"ipsum",
Expand Down Expand Up @@ -45,6 +46,7 @@
"uxui",
"voronoi",
"woot",
"wrappable"
"wrappable",
"YYYYMMDD"
]
}
19 changes: 19 additions & 0 deletions src/common/__tests__/__snapshots__/downloadHelpers.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DownloadHelpers should attempt to download data: data download 1`] = `
Object {
"data": "",
"fileName": "download.txt",
}
`;

exports[`DownloadHelpers should have specific functions: helpers 1`] = `
Object {
"debugLog": [Function],
"downloadData": [Function],
}
`;

exports[`DownloadHelpers should throw an error attempting to access a log: access error 1`] = `[Error: debuglog error, URL.createObjectURL is not a function]`;

exports[`DownloadHelpers should throw an error attempting to download data: download error 1`] = `[TypeError: URL.createObjectURL is not a function]`;
3 changes: 3 additions & 0 deletions src/common/__tests__/__snapshots__/helpers.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Object {
"UI_LINK_REPORT_ACCURACY_RECOMMENDATIONS": "https://access.redhat.com/solutions/subscription-watch-mismatch",
"UI_LOCALE_DEFAULT": "en-US",
"UI_LOCALE_DEFAULT_DESC": "English",
"UI_LOGGER_FILE": "curiosity_debug_log_{0}.json",
"UI_LOGGER_ID": "curiosity",
"UI_NAME": "subscriptions",
"UI_PATH": "/",
Expand Down Expand Up @@ -58,6 +59,7 @@ Object {
"UI_LINK_REPORT_ACCURACY_RECOMMENDATIONS": "https://access.redhat.com/solutions/subscription-watch-mismatch",
"UI_LOCALE_DEFAULT": "en-US",
"UI_LOCALE_DEFAULT_DESC": "English",
"UI_LOGGER_FILE": "curiosity_debug_log_{0}.json",
"UI_LOGGER_ID": "curiosity",
"UI_NAME": "subscriptions",
"UI_PATH": "/",
Expand Down Expand Up @@ -95,6 +97,7 @@ Object {
"UI_LINK_REPORT_ACCURACY_RECOMMENDATIONS": "https://access.redhat.com/solutions/subscription-watch-mismatch",
"UI_LOCALE_DEFAULT": "en-US",
"UI_LOCALE_DEFAULT_DESC": "English",
"UI_LOGGER_FILE": "curiosity_debug_log_{0}.json",
"UI_LOGGER_ID": "curiosity",
"UI_NAME": "subscriptions",
"UI_PATH": "/",
Expand Down
53 changes: 53 additions & 0 deletions src/common/__tests__/downloadHelpers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { downloadHelpers } from '../downloadHelpers';

/**
* ToDo: evaluate the clearing of timers with pending and real
* It's unclear if this is actively helping/necessary...
*/
describe('DownloadHelpers', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

it('should have specific functions', () => {
expect(downloadHelpers).toMatchSnapshot('helpers');
});

it('should throw an error attempting to download data', done => {
downloadHelpers.downloadData().catch(error => {
expect(error).toMatchSnapshot('download error');
done();
});
});

it('should throw an error attempting to access a log', done => {
downloadHelpers.debugLog().catch(error => {
expect(error).toMatchSnapshot('access error');
done();
});
});

it('should attempt to download data', done => {
window.URL = {
createObjectURL: jest.fn(),
revokeObjectURL: jest.fn()
};

downloadHelpers.downloadData().then(value => {
expect(value).toMatchSnapshot('data download');
done();
});

jest.runAllTimers();

window.URL = {
createObjectURL: undefined,
revokeObjectURL: undefined
};
});
});
72 changes: 72 additions & 0 deletions src/common/downloadHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import moment from 'moment/moment';
import { helpers } from './helpers';
import { dateHelpers } from './dateHelpers';

/**
* Download data to a file
*
* @param {object} options
* @param {string} options.data
* @param {string} options.fileName
* @param {string} options.fileType
* @returns {Promise}
*/
const downloadData = options => {
const { data = '', fileName = 'download.txt', fileType = 'text/plain' } = options || {};
return new Promise((resolve, reject) => {
try {
const { document, navigator, URL } = window;
const blob = new Blob([data], { type: fileType });

if (navigator?.msSaveBlob) {
navigator.msSaveBlob(blob, fileName);
resolve({ fileName, data });
} else {
const anchorTag = document.createElement('a');

anchorTag.href = URL.createObjectURL(blob);
anchorTag.style.display = 'none';
anchorTag.download = fileName;

document.body.appendChild(anchorTag);

anchorTag.click();

setTimeout(() => {
document.body.removeChild(anchorTag);
URL.revokeObjectURL(blob);
resolve({ fileName, data });
}, 250);
}
} catch (error) {
reject(error);
}
});
};

/**
* Download the debug log file.
*/
const debugLog = async () => {
try {
const { sessionStorage } = window;
const fileName = `${helpers.UI_LOGGER_FILE}`.replace(
'{0}',
moment(dateHelpers.getCurrentDate()).format('YYYYMMDD_HHmmss')
);
const data = JSON.stringify(JSON.parse(sessionStorage.getItem(`${helpers.UI_LOGGER_ID}`)), null, 2);

await downloadData({ data, fileName, fileType: 'application/json' });
} catch (e) {
throw new Error(`debuglog error, ${e.message}`);
}
};

const downloadHelpers = {
downloadData,
debugLog
};

helpers.browserExpose({ debugLog }, { limit: false });

export { downloadHelpers as default, downloadHelpers, downloadData, debugLog };
8 changes: 8 additions & 0 deletions src/common/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ const UI_LOCALE_DEFAULT_DESC = process.env.REACT_APP_CONFIG_SERVICE_LOCALES_DEFA
*/
const UI_LOGGER_ID = process.env.REACT_APP_UI_LOGGER_ID || 'GUI';

/**
* UI state logging file name.
*
* @type {string}
*/
const UI_LOGGER_FILE = process.env.REACT_APP_UI_LOGGER_FILE || 'debug_log_{0}.json';

/**
* UI packaged application name.
* See dotenv config files for updating.
Expand Down Expand Up @@ -281,6 +288,7 @@ const helpers = {
UI_LOCALE_DEFAULT,
UI_LOCALE_DEFAULT_DESC,
UI_LOGGER_ID,
UI_LOGGER_FILE,
UI_NAME,
UI_PATH,
UI_VERSION,
Expand Down
3 changes: 2 additions & 1 deletion src/common/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { helpers } from './helpers';
import { dateHelpers } from './dateHelpers';
import { downloadHelpers } from './downloadHelpers';

export { helpers as default, helpers, dateHelpers };
export { helpers as default, helpers, dateHelpers, downloadHelpers };
95 changes: 95 additions & 0 deletions src/redux/middleware/actionRecordMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Modify actions for privacy.
*
* @param {object} action
* @param {string} action.type
* @param {object} action.payload
* @returns {object}
*/
const sanitizeActionHeaders = ({ type, payload, ...action }) => {
if (payload) {
let updatedPayload = { ...payload, headers: {} };

if (Array.isArray(payload)) {
updatedPayload = payload.map(({ headers, ...obj }) => ({ ...obj, headers: {} }));
}

return { type, payload: updatedPayload, ...action };
}

return { type, ...action };
};

/**
* Return existing sessionStorage log.
*
* @param {string} id
* @param {number} limit
* @returns {Array}
*/
const getActions = (id, limit) => {
const { sessionStorage } = window;
const item = sessionStorage.getItem(id);
let parsedItems = (item && (JSON.parse(item) || {})?.actions) || null;

if (parsedItems?.length && limit > 0) {
parsedItems = parsedItems.slice(limit * -1);
}

return parsedItems;
};

/**
* Store actions against an id in sessionStorage.
*
* @param {object} action
* @param {object} config
* @param {number} config.id
* @param {number} config.limit
*/
const recordAction = (action, { id, limit, ...config }) => {
const { navigator, sessionStorage } = window;
const items = getActions(id, limit) || [];
const priorItem = items[items.length - 1];
const updatedAction = sanitizeActionHeaders(action);
const actionObj = {
diff: 0,
timestamp: Date.now(),
action: updatedAction
};

if (priorItem && priorItem.timestamp) {
actionObj.diff = actionObj.timestamp - priorItem.timestamp;
}

items.push(actionObj);
sessionStorage.setItem(
id,
JSON.stringify({
browser: navigator.userAgent,
timestamp: new Date(),
...config,
actions: items
})
);
};

/**
* Expose settings and record middleware.
*
* @param {object} config
* @returns {Function}
*/
const actionRecordMiddleware = (config = {}) => {
return () => next => action => {
recordAction(action, {
id: 'actionRecordMiddleware/v1',
limitResults: 100,
...config
});

return next(action);
};
};

export { actionRecordMiddleware as default, actionRecordMiddleware };
5 changes: 5 additions & 0 deletions src/redux/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import thunkMiddleware from 'redux-thunk';
import { notificationsMiddleware } from '@redhat-cloud-services/frontend-components-notifications/cjs';
import { multiActionMiddleware } from './multiActionMiddleware';
import { statusMiddleware } from './statusMiddleware';
import { actionRecordMiddleware } from './actionRecordMiddleware';
import { reduxHelpers } from '../common/reduxHelpers';

/**
Expand Down Expand Up @@ -34,6 +35,10 @@ const reduxMiddleware = [
statusMiddleware(),
multiActionMiddleware,
promiseMiddleware,
actionRecordMiddleware({
id: process.env.REACT_APP_UI_LOGGER_ID,
app: { version: process.env.REACT_APP_UI_VERSION }
}),
notificationsMiddleware(notificationsOptions)
];

Expand Down

0 comments on commit cf460f3

Please sign in to comment.