mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
Improve Superset logger (#6879)
* enable beacon * add middleware * add unit tests
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('Dashboard', () => {
|
||||
addSliceToDashboard() {},
|
||||
removeSliceFromDashboard() {},
|
||||
runQuery() {},
|
||||
logEvent() {},
|
||||
},
|
||||
initMessages: [],
|
||||
dashboardState,
|
||||
|
||||
@@ -54,6 +54,7 @@ describe('Chart', () => {
|
||||
refreshChart() {},
|
||||
toggleExpandSlice() {},
|
||||
addFilter() {},
|
||||
logEvent() {},
|
||||
editMode: false,
|
||||
isExpanded: false,
|
||||
supersetCanExplore: false,
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Markdown', () => {
|
||||
handleComponentDrop() {},
|
||||
updateComponents() {},
|
||||
deleteComponent() {},
|
||||
logEvent() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('Tabs', () => {
|
||||
onChangeTab() {},
|
||||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
logEvent() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
111
superset/assets/spec/javascripts/middleware/logger_spec.js
Normal file
111
superset/assets/spec/javascripts/middleware/logger_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user