Improve Superset logger (#6879)

* enable beacon

* add middleware

* add unit tests
This commit is contained in:
Grace Guo
2019-02-15 23:10:05 -08:00
committed by GitHub
parent bd9a2c15e7
commit 47f42ed351
32 changed files with 569 additions and 552 deletions

View File

@@ -19,7 +19,7 @@
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import { Logger } from '../../../src/logger';
import { LOG_EVENT } from '../../../src/logger/actions';
import * as exploreUtils from '../../../src/explore/exploreUtils';
import * as actions from '../../../src/chart/chartAction';
@@ -27,7 +27,6 @@ describe('chart actions', () => {
const MOCK_URL = '/mockURL';
let dispatch;
let urlStub;
let loggerStub;
const setupDefaultFetchMock = () => {
fetchMock.post(MOCK_URL, { json: {} }, { overwriteRoutes: true });
@@ -44,12 +43,10 @@ describe('chart actions', () => {
urlStub = sinon
.stub(exploreUtils, 'getExploreUrlAndPayload')
.callsFake(() => ({ url: MOCK_URL, payload: {} }));
loggerStub = sinon.stub(Logger, 'append');
});
afterEach(() => {
urlStub.restore();
loggerStub.restore();
fetchMock.resetHistory();
});
@@ -58,7 +55,7 @@ describe('chart actions', () => {
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(4);
expect(dispatch.callCount).toBe(5);
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
@@ -70,7 +67,7 @@ describe('chart actions', () => {
const actionThunk = actions.runQuery({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(4);
expect(dispatch.callCount).toBe(5);
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
expect(dispatch.args[1][0].type).toBe(actions.TRIGGER_QUERY);
@@ -82,7 +79,7 @@ describe('chart actions', () => {
const actionThunk = actions.runQuery({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(4);
expect(dispatch.callCount).toBe(5);
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
expect(dispatch.args[2][0].type).toBe(actions.UPDATE_QUERY_FORM_DATA);
@@ -90,14 +87,29 @@ describe('chart actions', () => {
});
});
it('should dispatch logEvent async action', () => {
const actionThunk = actions.runQuery({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(5);
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
expect(typeof dispatch.args[3][0]).toBe('function');
dispatch.args[3][0](dispatch);
expect(dispatch.callCount).toBe(6);
expect(dispatch.args[5][0].type).toBe(LOG_EVENT);
return Promise.resolve();
});
});
it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
const actionThunk = actions.runQuery({});
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, success
expect(dispatch.callCount).toBe(4);
expect(dispatch.callCount).toBe(5);
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED);
expect(loggerStub.callCount).toBe(1);
expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED);
return Promise.resolve();
});
@@ -112,10 +124,8 @@ describe('chart actions', () => {
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, fail
expect(dispatch.callCount).toBe(4);
expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
expect(loggerStub.callCount).toBe(1);
expect(loggerStub.args[0][1].error_details).toBe('timeout');
expect(dispatch.callCount).toBe(5);
expect(dispatch.args[4][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
setupDefaultFetchMock();
return Promise.resolve();
@@ -130,13 +140,11 @@ describe('chart actions', () => {
return actionThunk(dispatch).then(() => {
// chart update, trigger query, update form data, fail
expect(dispatch.callCount).toBe(4);
const updateFailedAction = dispatch.args[3][0];
expect(dispatch.callCount).toBe(5);
const updateFailedAction = dispatch.args[4][0];
expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED);
expect(updateFailedAction.queryResponse.error).toBe('misc error');
expect(loggerStub.callCount).toBe(1);
expect(loggerStub.args[0][1].error_details).toBe('misc error');
setupDefaultFetchMock();
return Promise.resolve();

View File

@@ -40,6 +40,7 @@ describe('Dashboard', () => {
addSliceToDashboard() {},
removeSliceFromDashboard() {},
runQuery() {},
logEvent() {},
},
initMessages: [],
dashboardState,

View File

@@ -54,6 +54,7 @@ describe('Chart', () => {
refreshChart() {},
toggleExpandSlice() {},
addFilter() {},
logEvent() {},
editMode: false,
isExpanded: false,
supersetCanExplore: false,

View File

@@ -51,6 +51,7 @@ describe('Markdown', () => {
handleComponentDrop() {},
updateComponents() {},
deleteComponent() {},
logEvent() {},
};
function setup(overrideProps) {

View File

@@ -52,6 +52,7 @@ describe('Tabs', () => {
onChangeTab() {},
deleteComponent() {},
updateComponents() {},
logEvent() {},
};
function setup(overrideProps) {

View File

@@ -1,179 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import fetchMock from 'fetch-mock';
import { Logger, ActionLog } from '../../src/logger';
describe('ActionLog', () => {
it('should be a constructor', () => {
const newLogger = new ActionLog({});
expect(newLogger instanceof ActionLog).toBe(true);
});
it(
'should set the eventNames, impressionId, source, sourceId, and sendNow init parameters',
() => {
const eventNames = [];
const impressionId = 'impressionId';
const source = 'source';
const sourceId = 'sourceId';
const sendNow = true;
const log = new ActionLog({ eventNames, impressionId, source, sourceId, sendNow });
expect(log.eventNames).toBe(eventNames);
expect(log.impressionId).toBe(impressionId);
expect(log.source).toBe(source);
expect(log.sourceId).toBe(sourceId);
expect(log.sendNow).toBe(sendNow);
},
);
it('should set attributes with the setAttribute method', () => {
const log = new ActionLog({});
expect(log.test).toBeUndefined();
log.setAttribute('test', 'testValue');
expect(log.test).toBe('testValue');
});
it('should track added events', () => {
const log = new ActionLog({});
const eventName = 'myEventName';
const eventBody = { test: 'event' };
expect(log.events[eventName]).toBeUndefined();
log.addEvent(eventName, eventBody);
expect(log.events[eventName]).toHaveLength(1);
expect(log.events[eventName][0]).toMatchObject(eventBody);
Logger.end(log);
});
});
describe('Logger', () => {
const logEndpoint = 'glob:*/superset/log/*';
fetchMock.post(logEndpoint, 'success');
afterEach(fetchMock.resetHistory);
it('should add events when .append(eventName, eventBody) is called', () => {
const eventName = 'testEvent';
const eventBody = { test: 'event' };
const log = new ActionLog({ eventNames: [eventName] });
Logger.start(log);
Logger.append(eventName, eventBody);
expect(log.events[eventName]).toHaveLength(1);
expect(log.events[eventName][0]).toMatchObject(eventBody);
Logger.end(log);
});
describe('.send()', () => {
const eventNames = ['test'];
function setup(overrides = {}) {
const log = new ActionLog({ eventNames, ...overrides });
return log;
}
it('should POST an event to /superset/log/ when called', (done) => {
const log = setup();
Logger.start(log);
Logger.append(eventNames[0], { test: 'event' });
expect(log.events[eventNames[0]]).toHaveLength(1);
Logger.end(log);
setTimeout(() => {
expect(fetchMock.calls(logEndpoint)).toHaveLength(1);
done();
}, 0);
});
it("should flush the log's events", () => {
const log = setup();
Logger.start(log);
Logger.append(eventNames[0], { test: 'event' });
const event = log.events[eventNames[0]][0];
expect(event).toMatchObject({ test: 'event' });
Logger.end(log);
expect(log.events).toEqual({});
});
it(
'should include ts, start_offset, event_name, impression_id, source, and source_id in every event',
(done) => {
const config = {
eventNames: ['event1', 'event2'],
impressionId: 'impress_me',
source: 'superset',
sourceId: 'lolz',
};
const log = setup(config);
Logger.start(log);
Logger.append('event1', { key: 'value' });
Logger.append('event2', { foo: 'bar' });
Logger.end(log);
setTimeout(() => {
const calls = fetchMock.calls(logEndpoint);
expect(calls).toHaveLength(1);
const options = calls[0][1];
const events = JSON.parse(options.body.get('events'));
expect(events).toHaveLength(2);
expect(events[0]).toMatchObject({
key: 'value',
event_name: 'event1',
impression_id: config.impressionId,
source: config.source,
source_id: config.sourceId,
});
expect(events[1]).toMatchObject({
foo: 'bar',
event_name: 'event2',
impression_id: config.impressionId,
source: config.source,
source_id: config.sourceId,
});
expect(typeof events[0].ts).toBe('number');
expect(typeof events[1].ts).toBe('number');
expect(typeof events[0].start_offset).toBe('number');
expect(typeof events[1].start_offset).toBe('number');
done();
}, 0);
},
);
it(
'should send() a log immediately if .append() is called with sendNow=true',
(done) => {
const log = setup();
Logger.start(log);
Logger.append(eventNames[0], { test: 'event' }, true);
setTimeout(() => {
expect(fetchMock.calls(logEndpoint)).toHaveLength(1);
Logger.end(log); // flush logs
done();
}, 0);
},
);
});
});

View File

@@ -0,0 +1,111 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import sinon from 'sinon';
import { SupersetClient } from '@superset-ui/connection';
import logger from '../../../src/middleware/loggerMiddleware';
import { LOG_EVENT } from '../../../src/logger/actions';
import { LOG_ACTIONS_LOAD_CHART } from '../../../src/logger/LogUtils';
describe('logger middleware', () => {
const next = sinon.spy();
const mockStore = {
getState: () => ({
dashboardInfo: {
id: 1,
},
impressionId: 'impression_id',
}),
};
const action = {
type: LOG_EVENT,
payload: {
eventName: LOG_ACTIONS_LOAD_CHART,
eventData: {
key: 'value',
start_offset: 100,
},
},
};
let postStub;
beforeEach(() => {
postStub = sinon.stub(SupersetClient, 'post');
});
afterEach(() => {
next.reset();
postStub.restore();
});
it('should listen to LOG_EVENT action type', () => {
const action1 = {
type: 'ACTION_TYPE',
payload: {
some: 'data',
},
};
logger(mockStore)(next)(action1);
expect(next.callCount).toBe(1);
});
it('should POST an event to /superset/log/ when called', () => {
const clock = sinon.useFakeTimers();
logger(mockStore)(next)(action);
expect(next.callCount).toBe(0);
clock.tick(2000);
expect(SupersetClient.post.callCount).toBe(1);
expect(SupersetClient.post.getCall(0).args[0].endpoint).toMatch('/superset/log/');
});
it(
'should include ts, start_offset, event_name, impression_id, source, and source_id in every event',
() => {
const clock = sinon.useFakeTimers();
logger(mockStore)(next)(action);
clock.tick(2000);
expect(SupersetClient.post.callCount).toBe(1);
const events = SupersetClient.post.getCall(0).args[0].postPayload.events;
const mockEventdata = action.payload.eventData;
const mockEventname = action.payload.eventName;
expect(events[0]).toMatchObject({
key: mockEventdata.key,
event_name: mockEventname,
impression_id: mockStore.getState().impressionId,
source: 'dashboard',
source_id: mockStore.getState().dashboardInfo.id,
event_type: 'timing',
});
expect(typeof events[0].ts).toBe('number');
expect(typeof events[0].start_offset).toBe('number');
},
);
it('should debounce a few log requests to one', () => {
const clock = sinon.useFakeTimers();
logger(mockStore)(next)(action);
logger(mockStore)(next)(action);
logger(mockStore)(next)(action);
clock.tick(2000);
expect(SupersetClient.post.callCount).toBe(1);
expect(SupersetClient.post.getCall(0).args[0].postPayload.events).toHaveLength(3);
});
});