mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console: require async globals before render; track runtime errors (#4449)
This commit is contained in:
parent
d2aac3732a
commit
ea2f1679eb
@ -42,6 +42,9 @@ const globals = {
|
|||||||
featuresCompatibility: window.__env.serverVersion
|
featuresCompatibility: window.__env.serverVersion
|
||||||
? getFeaturesCompatibility(window.__env.serverVersion)
|
? getFeaturesCompatibility(window.__env.serverVersion)
|
||||||
: null,
|
: null,
|
||||||
|
cliUUID: window.__env.cliUUID,
|
||||||
|
hasuraUUID: '',
|
||||||
|
telemetryNotificationShown: '',
|
||||||
isProduction,
|
isProduction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,43 +18,7 @@ import getRoutes from './routes';
|
|||||||
|
|
||||||
import reducer from './reducer';
|
import reducer from './reducer';
|
||||||
import globals from './Globals';
|
import globals from './Globals';
|
||||||
import Endpoints from './Endpoints';
|
import { trackReduxAction } from './telemetry';
|
||||||
|
|
||||||
import { filterEventsBlockList, sanitiseUrl } from './telemetryFilter';
|
|
||||||
import { RUN_TIME_ERROR } from './components/Main/Actions';
|
|
||||||
|
|
||||||
/** telemetry **/
|
|
||||||
let analyticsConnection;
|
|
||||||
|
|
||||||
const analyticsUrl = Endpoints.telemetryServer;
|
|
||||||
|
|
||||||
const { consoleMode, enableTelemetry, cliUUID } = window.__env;
|
|
||||||
|
|
||||||
const telemetryEnabled =
|
|
||||||
enableTelemetry !== undefined && enableTelemetry === true;
|
|
||||||
|
|
||||||
if (telemetryEnabled) {
|
|
||||||
try {
|
|
||||||
analyticsConnection = new WebSocket(analyticsUrl);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onError = error => {
|
|
||||||
console.error('WebSocket Error for Events' + error);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const onClose = () => {
|
|
||||||
try {
|
|
||||||
analyticsConnection = new WebSocket(analyticsUrl);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
analyticsConnection.onclose = onClose();
|
|
||||||
analyticsConnection.onerror = onError();
|
|
||||||
};
|
|
||||||
|
|
||||||
function analyticsLogger({ getState }) {
|
function analyticsLogger({ getState }) {
|
||||||
return next => action => {
|
return next => action => {
|
||||||
@ -62,53 +26,9 @@ function analyticsLogger({ getState }) {
|
|||||||
const returnValue = next(action);
|
const returnValue = next(action);
|
||||||
|
|
||||||
// check if analytics tracking is enabled
|
// check if analytics tracking is enabled
|
||||||
if (telemetryEnabled) {
|
if (globals.enableTelemetry) {
|
||||||
const actionType = action.type;
|
trackReduxAction(action, getState);
|
||||||
|
|
||||||
// filter events
|
|
||||||
if (!filterEventsBlockList.includes(actionType)) {
|
|
||||||
// When the connection is open, send data to the server
|
|
||||||
if (
|
|
||||||
analyticsConnection &&
|
|
||||||
analyticsConnection.readyState === analyticsConnection.OPEN
|
|
||||||
) {
|
|
||||||
const serverVersion = getState().main.serverVersion;
|
|
||||||
const url = sanitiseUrl(window.location.pathname);
|
|
||||||
|
|
||||||
const reqBody = {
|
|
||||||
server_version: serverVersion,
|
|
||||||
event_type: actionType,
|
|
||||||
url,
|
|
||||||
console_mode: consoleMode,
|
|
||||||
cli_uuid: cliUUID,
|
|
||||||
server_uuid: getState().telemetry.hasura_uuid,
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLocationType = actionType === '@@router/LOCATION_CHANGE';
|
|
||||||
if (isLocationType) {
|
|
||||||
// capture page views
|
|
||||||
const payload = action.payload;
|
|
||||||
reqBody.url = sanitiseUrl(payload.pathname);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isErrorType = actionType === RUN_TIME_ERROR;
|
|
||||||
if (isErrorType) {
|
|
||||||
reqBody.data = action.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the data
|
|
||||||
analyticsConnection.send(
|
|
||||||
JSON.stringify({ data: reqBody, topic: globals.telemetryTopic })
|
|
||||||
);
|
|
||||||
|
|
||||||
// check for possible error events and store more data?
|
|
||||||
} else {
|
|
||||||
// retry websocket connection
|
|
||||||
// analyticsConnection = new WebSocket(analyticsUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will likely be the action itself, unless
|
// This will likely be the action itself, unless
|
||||||
// a middleware further in chain changed it.
|
// a middleware further in chain changed it.
|
||||||
return returnValue;
|
return returnValue;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import defaultState from './State';
|
import defaultState from './State';
|
||||||
import Notifications from 'react-notification-system-redux';
|
import Notifications from 'react-notification-system-redux';
|
||||||
|
import { loadConsoleOpts } from '../../telemetry/Actions';
|
||||||
|
import { fetchServerConfig } from '../Main/Actions';
|
||||||
|
|
||||||
const LOAD_REQUEST = 'App/ONGOING_REQUEST';
|
const LOAD_REQUEST = 'App/ONGOING_REQUEST';
|
||||||
const DONE_REQUEST = 'App/DONE_REQUEST';
|
const DONE_REQUEST = 'App/DONE_REQUEST';
|
||||||
@ -46,6 +48,15 @@ const showNotification = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requireAsyncGlobals = ({ dispatch }) => {
|
||||||
|
return (nextState, finalState, callback) => {
|
||||||
|
Promise.all([
|
||||||
|
dispatch(loadConsoleOpts()),
|
||||||
|
dispatch(fetchServerConfig()),
|
||||||
|
]).finally(callback);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const progressBarReducer = (state = defaultState, action) => {
|
const progressBarReducer = (state = defaultState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LOAD_REQUEST:
|
case LOAD_REQUEST:
|
||||||
@ -93,6 +104,7 @@ const progressBarReducer = (state = defaultState, action) => {
|
|||||||
...state,
|
...state,
|
||||||
modalOpen: true,
|
modalOpen: true,
|
||||||
error: true,
|
error: true,
|
||||||
|
ongoingRequest: false,
|
||||||
connectionFailed: true,
|
connectionFailed: true,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ProgressBar from 'react-progress-bar-plus';
|
import ProgressBar from 'react-progress-bar-plus';
|
||||||
import Notifications from 'react-notification-system-redux';
|
import Notifications from 'react-notification-system-redux';
|
||||||
@ -7,29 +7,33 @@ import { hot } from 'react-hot-loader';
|
|||||||
import { ThemeProvider } from 'styled-components';
|
import { ThemeProvider } from 'styled-components';
|
||||||
|
|
||||||
import ErrorBoundary from '../Error/ErrorBoundary';
|
import ErrorBoundary from '../Error/ErrorBoundary';
|
||||||
import {
|
import { telemetryNotificationShown } from '../../telemetry/Actions';
|
||||||
loadConsoleOpts,
|
|
||||||
telemetryNotificationShown,
|
|
||||||
} from '../../telemetry/Actions';
|
|
||||||
import { showTelemetryNotification } from '../../telemetry/Notifications';
|
import { showTelemetryNotification } from '../../telemetry/Notifications';
|
||||||
|
import globals from '../../Globals';
|
||||||
|
import styles from './App.scss';
|
||||||
|
|
||||||
import { theme } from '../UIKit/theme';
|
import { theme } from '../UIKit/theme';
|
||||||
|
|
||||||
class App extends Component {
|
export const GlobalContext = React.createContext(globals);
|
||||||
componentDidMount() {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
// Hide the loader once the react component is ready.
|
const App = ({
|
||||||
// NOTE: This will execute only once (since this is the parent component for all other components).
|
ongoingRequest,
|
||||||
|
percent,
|
||||||
|
intervalTime,
|
||||||
|
children,
|
||||||
|
notifications,
|
||||||
|
connectionFailed,
|
||||||
|
dispatch,
|
||||||
|
metadata,
|
||||||
|
telemetry,
|
||||||
|
}) => {
|
||||||
|
React.useEffect(() => {
|
||||||
const className = document.getElementById('content').className;
|
const className = document.getElementById('content').className;
|
||||||
document.getElementById('content').className = className + ' show';
|
document.getElementById('content').className = className + ' show';
|
||||||
document.getElementById('loading').style.display = 'none';
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
}, []);
|
||||||
|
|
||||||
dispatch(loadConsoleOpts());
|
React.useEffect(() => {
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const { telemetry, dispatch } = this.props;
|
|
||||||
if (
|
if (
|
||||||
telemetry.console_opts &&
|
telemetry.console_opts &&
|
||||||
!telemetry.console_opts.telemetryNotificationShown
|
!telemetry.console_opts.telemetryNotificationShown
|
||||||
@ -37,44 +41,25 @@ class App extends Component {
|
|||||||
dispatch(telemetryNotificationShown());
|
dispatch(telemetryNotificationShown());
|
||||||
dispatch(showTelemetryNotification());
|
dispatch(showTelemetryNotification());
|
||||||
}
|
}
|
||||||
|
}, [telemetry]);
|
||||||
|
|
||||||
|
let connectionFailMsg = null;
|
||||||
|
if (connectionFailed) {
|
||||||
|
connectionFailMsg = (
|
||||||
|
<div
|
||||||
|
className={`${styles.alertDanger} ${styles.remove_margin_bottom} alert alert-danger `}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
Hasura console is not able to reach your Hasura GraphQL engine
|
||||||
|
instance. Please ensure that your instance is running and the endpoint
|
||||||
|
is configured correctly.
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const styles = require('./App.scss');
|
<GlobalContext.Provider value={globals}>
|
||||||
const {
|
|
||||||
requestError,
|
|
||||||
error,
|
|
||||||
ongoingRequest,
|
|
||||||
percent,
|
|
||||||
intervalTime,
|
|
||||||
children,
|
|
||||||
notifications,
|
|
||||||
connectionFailed,
|
|
||||||
dispatch,
|
|
||||||
metadata,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (requestError && error) {
|
|
||||||
// console.error(requestError, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let connectionFailMsg = null;
|
|
||||||
if (connectionFailed) {
|
|
||||||
connectionFailMsg = (
|
|
||||||
<div
|
|
||||||
style={{ marginBottom: '0px' }}
|
|
||||||
className={styles.alertDanger + ' alert alert-danger'}
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
Hasura console is not able to reach your Hasura GraphQL engine
|
|
||||||
instance. Please ensure that your instance is running and the
|
|
||||||
endpoint is configured correctly.
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<ErrorBoundary metadata={metadata} dispatch={dispatch}>
|
<ErrorBoundary metadata={metadata} dispatch={dispatch}>
|
||||||
<div>
|
<div>
|
||||||
@ -92,18 +77,16 @@ class App extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
</GlobalContext.Provider>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
reqURL: PropTypes.string,
|
reqURL: PropTypes.string,
|
||||||
reqData: PropTypes.object,
|
reqData: PropTypes.object,
|
||||||
statusCode: PropTypes.number,
|
statusCode: PropTypes.number,
|
||||||
|
|
||||||
error: PropTypes.object,
|
|
||||||
ongoingRequest: PropTypes.bool,
|
ongoingRequest: PropTypes.bool,
|
||||||
requestError: PropTypes.bool,
|
|
||||||
connectionFailed: PropTypes.bool,
|
connectionFailed: PropTypes.bool,
|
||||||
|
|
||||||
intervalTime: PropTypes.number,
|
intervalTime: PropTypes.number,
|
||||||
|
@ -2,6 +2,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import styles from '../Common.scss';
|
import styles from '../Common.scss';
|
||||||
|
|
||||||
|
import { GlobalContext } from '../../App/App';
|
||||||
|
import { trackRuntimeError } from '../../../telemetry';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This is a Button HOC that takes all the props supported by <button>
|
This is a Button HOC that takes all the props supported by <button>
|
||||||
- color(default: white): color of the button; currently supports yellow, red, green, gray and white
|
- color(default: white): color of the button; currently supports yellow, red, green, gray and white
|
||||||
@ -16,7 +19,7 @@ export interface ButtonProps extends React.ComponentProps<'button'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = props => {
|
const Button: React.FC<ButtonProps> = props => {
|
||||||
const { children, size, color, className, type = 'button' } = props;
|
const { children, onClick, size, color, className, type = 'button' } = props;
|
||||||
let extendedClassName = `${className || ''} btn ${
|
let extendedClassName = `${className || ''} btn ${
|
||||||
size ? `btn-${size} ` : 'button '
|
size ? `btn-${size} ` : 'button '
|
||||||
}`;
|
}`;
|
||||||
@ -37,8 +40,29 @@ const Button: React.FC<ButtonProps> = props => {
|
|||||||
extendedClassName += 'btn-default';
|
extendedClassName += 'btn-default';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globals = React.useContext(GlobalContext);
|
||||||
|
|
||||||
|
const trackedOnClick = (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (onClick) {
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
trackRuntimeError(globals, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button {...props} className={extendedClassName} type={type}>
|
<button
|
||||||
|
{...props}
|
||||||
|
className={extendedClassName}
|
||||||
|
type={type}
|
||||||
|
onClick={onClick ? trackedOnClick : undefined}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
// TODO: make functions from this file available without imports
|
// TODO: make functions from this file available without imports
|
||||||
|
|
||||||
import { showErrorNotification } from '../../Services/Common/Notification';
|
|
||||||
|
|
||||||
/* TYPE utils */
|
/* TYPE utils */
|
||||||
|
|
||||||
export const isNotDefined = value => {
|
export const isNotDefined = value => {
|
||||||
@ -233,8 +231,9 @@ export const getConfirmation = (
|
|||||||
export const uploadFile = (
|
export const uploadFile = (
|
||||||
fileHandler,
|
fileHandler,
|
||||||
fileFormat = null,
|
fileFormat = null,
|
||||||
invalidFileHandler = null
|
invalidFileHandler = null,
|
||||||
) => dispatch => {
|
errorCallback = null
|
||||||
|
) => {
|
||||||
const fileInputElement = document.createElement('div');
|
const fileInputElement = document.createElement('div');
|
||||||
fileInputElement.innerHTML = '<input style="display:none" type="file">';
|
fileInputElement.innerHTML = '<input style="display:none" type="file">';
|
||||||
const fileInput = fileInputElement.firstChild;
|
const fileInput = fileInputElement.firstChild;
|
||||||
@ -254,12 +253,12 @@ export const uploadFile = (
|
|||||||
if (invalidFileHandler) {
|
if (invalidFileHandler) {
|
||||||
invalidFileHandler(fileName);
|
invalidFileHandler(fileName);
|
||||||
} else {
|
} else {
|
||||||
dispatch(
|
if (errorCallback) {
|
||||||
showErrorNotification(
|
errorCallback(
|
||||||
'Invalid file format',
|
'Invalid file format',
|
||||||
`Expected a ${expectedFileSuffix} file`
|
`Expected a ${expectedFileSuffix} file`
|
||||||
)
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInputElement.remove();
|
fileInputElement.remove();
|
||||||
|
@ -315,10 +315,20 @@ export const getFetchManualTriggersQuery = tableName => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getConsoleOptsQuery = () =>
|
||||||
|
generateSelectQuery(
|
||||||
|
'select',
|
||||||
|
{ name: 'hdb_version', schema: 'hdb_catalog' },
|
||||||
|
{
|
||||||
|
columns: ['hasura_uuid', 'console_state'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const getSaveRemoteRelQuery = (args, isNew) => ({
|
export const getSaveRemoteRelQuery = (args, isNew) => ({
|
||||||
type: `${isNew ? 'create' : 'update'}_remote_relationship`,
|
type: `${isNew ? 'create' : 'update'}_remote_relationship`,
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getDropRemoteRelQuery = (name, table) => ({
|
export const getDropRemoteRelQuery = (name, table) => ({
|
||||||
type: 'delete_remote_relationship',
|
type: 'delete_remote_relationship',
|
||||||
args: {
|
args: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import defaultState from './State';
|
import defaultState from './State';
|
||||||
|
import globals from '../../Globals';
|
||||||
import requestAction from '../../utils/requestAction';
|
import requestAction from '../../utils/requestAction';
|
||||||
import requestActionPlain from '../../utils/requestActionPlain';
|
import requestActionPlain from '../../utils/requestActionPlain';
|
||||||
import Endpoints, { globalCookiePolicy } from '../../Endpoints';
|
import Endpoints, { globalCookiePolicy } from '../../Endpoints';
|
||||||
@ -121,16 +122,19 @@ const fetchServerConfig = () => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
return dispatch(requestAction(url, options)).then(
|
return dispatch(requestAction(url, options)).then(
|
||||||
data => {
|
data => {
|
||||||
return dispatch({
|
dispatch({
|
||||||
type: SERVER_CONFIG_FETCH_SUCCESS,
|
type: SERVER_CONFIG_FETCH_SUCCESS,
|
||||||
data: data,
|
data: data,
|
||||||
});
|
});
|
||||||
|
globals.serverConfig = data;
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
return dispatch({
|
dispatch({
|
||||||
type: SERVER_CONFIG_FETCH_FAIL,
|
type: SERVER_CONFIG_FETCH_FAIL,
|
||||||
data: error,
|
data: error,
|
||||||
});
|
});
|
||||||
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -33,8 +33,6 @@ class ImportMetadata extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const styles = require('../Settings.scss');
|
const styles = require('../Settings.scss');
|
||||||
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
const { isImporting } = this.state;
|
const { isImporting } = this.state;
|
||||||
|
|
||||||
const handleImport = e => {
|
const handleImport = e => {
|
||||||
@ -42,7 +40,7 @@ class ImportMetadata extends Component {
|
|||||||
|
|
||||||
this.setState({ isImporting: true });
|
this.setState({ isImporting: true });
|
||||||
|
|
||||||
dispatch(uploadFile(this.importMetadata, 'json'));
|
uploadFile(this.importMetadata, 'json');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,3 +2,4 @@ export const SERVER_CONSOLE_MODE = 'server';
|
|||||||
export const CLI_CONSOLE_MODE = 'cli';
|
export const CLI_CONSOLE_MODE = 'cli';
|
||||||
|
|
||||||
export const ADMIN_SECRET_HEADER_KEY = 'x-hasura-admin-secret';
|
export const ADMIN_SECRET_HEADER_KEY = 'x-hasura-admin-secret';
|
||||||
|
export const REDUX_LOCATION_CHANGE_ACTION_TYPE = '@@router/LOCATION_CHANGE';
|
||||||
|
@ -3,12 +3,14 @@ import { Route, IndexRoute, IndexRedirect } from 'react-router';
|
|||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { App, Main, PageNotFound } from 'components';
|
|
||||||
|
|
||||||
import globals from './Globals';
|
import globals from './Globals';
|
||||||
|
|
||||||
|
import { App, Main, PageNotFound } from 'components';
|
||||||
|
|
||||||
import validateLogin from './utils/validateLogin';
|
import validateLogin from './utils/validateLogin';
|
||||||
|
|
||||||
|
import { requireAsyncGlobals } from './components/App/Actions';
|
||||||
|
|
||||||
import { composeOnEnterHooks } from 'utils/router';
|
import { composeOnEnterHooks } from 'utils/router';
|
||||||
|
|
||||||
import { loadMigrationStatus } from './components/Main/Actions';
|
import { loadMigrationStatus } from './components/Main/Actions';
|
||||||
@ -103,7 +105,14 @@ const routes = store => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route path="/" component={App} onEnter={validateLogin(store)}>
|
<Route
|
||||||
|
path="/"
|
||||||
|
component={App}
|
||||||
|
onEnter={composeOnEnterHooks([
|
||||||
|
validateLogin(store),
|
||||||
|
requireAsyncGlobals(store),
|
||||||
|
])}
|
||||||
|
>
|
||||||
<Route path="login" component={generatedLoginConnector(connect)} />
|
<Route path="login" component={generatedLoginConnector(connect)} />
|
||||||
<Route
|
<Route
|
||||||
path=""
|
path=""
|
||||||
|
@ -2,11 +2,15 @@ import Endpoints, { globalCookiePolicy } from '../Endpoints';
|
|||||||
import requestAction from '../utils/requestAction';
|
import requestAction from '../utils/requestAction';
|
||||||
import dataHeaders from '../components/Services/Data/Common/Headers';
|
import dataHeaders from '../components/Services/Data/Common/Headers';
|
||||||
import defaultTelemetryState from './State';
|
import defaultTelemetryState from './State';
|
||||||
import { getRunSqlQuery } from '../components/Common/utils/v1QueryUtils';
|
import {
|
||||||
|
getRunSqlQuery,
|
||||||
|
getConsoleOptsQuery,
|
||||||
|
} from '../components/Common/utils/v1QueryUtils';
|
||||||
import {
|
import {
|
||||||
showErrorNotification,
|
showErrorNotification,
|
||||||
showSuccessNotification,
|
showSuccessNotification,
|
||||||
} from '../components/Services/Common/Notification';
|
} from '../components/Services/Common/Notification';
|
||||||
|
import globals from '../Globals';
|
||||||
|
|
||||||
const SET_CONSOLE_OPTS = 'Telemetry/SET_CONSOLE_OPTS';
|
const SET_CONSOLE_OPTS = 'Telemetry/SET_CONSOLE_OPTS';
|
||||||
const SET_NOTIFICATION_SHOWN = 'Telemetry/SET_NOTIFICATION_SHOWN';
|
const SET_NOTIFICATION_SHOWN = 'Telemetry/SET_NOTIFICATION_SHOWN';
|
||||||
@ -98,23 +102,14 @@ const setPreReleaseNotificationOptOutInDB = () => dispatch => {
|
|||||||
return dispatch(setConsoleOptsInDB(opts, successCb, errorCb));
|
return dispatch(setConsoleOptsInDB(opts, successCb, errorCb));
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadConsoleOpts = () => {
|
export const loadConsoleOpts = () => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const url = Endpoints.getSchema;
|
const url = Endpoints.getSchema;
|
||||||
const options = {
|
const options = {
|
||||||
credentials: globalCookiePolicy,
|
credentials: globalCookiePolicy,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: dataHeaders(getState),
|
headers: dataHeaders(getState),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(getConsoleOptsQuery()),
|
||||||
type: 'select',
|
|
||||||
args: {
|
|
||||||
table: {
|
|
||||||
name: 'hdb_version',
|
|
||||||
schema: 'hdb_catalog',
|
|
||||||
},
|
|
||||||
columns: ['hasura_uuid', 'console_state'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return dispatch(requestAction(url, options)).then(
|
return dispatch(requestAction(url, options)).then(
|
||||||
@ -124,21 +119,34 @@ const loadConsoleOpts = () => {
|
|||||||
type: SET_HASURA_UUID,
|
type: SET_HASURA_UUID,
|
||||||
data: data[0].hasura_uuid,
|
data: data[0].hasura_uuid,
|
||||||
});
|
});
|
||||||
|
globals.hasuraUUID = data[0].hasura_uuid;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_CONSOLE_OPTS,
|
type: SET_CONSOLE_OPTS,
|
||||||
data: data[0].console_state,
|
data: data[0].console_state,
|
||||||
});
|
});
|
||||||
|
globals.telemetryNotificationShown =
|
||||||
|
data[0].console_state.telemetryNotificationShown;
|
||||||
}
|
}
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error(
|
console.error(
|
||||||
'Failed to load console options: ' + JSON.stringify(error)
|
'Failed to load console options: ' + JSON.stringify(error)
|
||||||
);
|
);
|
||||||
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requireConsoleOpts = ({ dispatch }) => (
|
||||||
|
nextState,
|
||||||
|
replaceState,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
dispatch(loadConsoleOpts()).finally(callback);
|
||||||
|
};
|
||||||
|
|
||||||
const telemetryReducer = (state = defaultTelemetryState, action) => {
|
const telemetryReducer = (state = defaultTelemetryState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SET_CONSOLE_OPTS:
|
case SET_CONSOLE_OPTS:
|
||||||
@ -168,7 +176,6 @@ const telemetryReducer = (state = defaultTelemetryState, action) => {
|
|||||||
|
|
||||||
export default telemetryReducer;
|
export default telemetryReducer;
|
||||||
export {
|
export {
|
||||||
loadConsoleOpts,
|
|
||||||
telemetryNotificationShown,
|
telemetryNotificationShown,
|
||||||
setPreReleaseNotificationOptOutInDB,
|
setPreReleaseNotificationOptOutInDB,
|
||||||
setTelemetryNotificationShownInDB,
|
setTelemetryNotificationShownInDB,
|
||||||
|
70
console/src/telemetry/filters.ts
Normal file
70
console/src/telemetry/filters.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import globals from '../Globals';
|
||||||
|
|
||||||
|
const filterEventsBlockList = [
|
||||||
|
'App/ONGOING_REQUEST',
|
||||||
|
'App/DONE_REQUEST',
|
||||||
|
'App/FAILED_REQUEST',
|
||||||
|
'App/ERROR_REQUEST',
|
||||||
|
'RNS_SHOW_NOTIFICATION',
|
||||||
|
'RNS_HIDE_NOTIFICATION',
|
||||||
|
'RNS_REMOVE_ALL_NOTIFICATIONS',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DATA_PATH = '/data';
|
||||||
|
const API_EXPLORER_PATH = '/api-explorer';
|
||||||
|
const REMOTE_SCHEMAS_PATH = '/remote-schemas';
|
||||||
|
const EVENTS_PATH = '/events';
|
||||||
|
|
||||||
|
const dataHandler = (path: string) => {
|
||||||
|
return (
|
||||||
|
DATA_PATH +
|
||||||
|
path
|
||||||
|
.replace(/\/schema\/([^/]*)(\/)?/, '/schema/SCHEMA_NAME$2')
|
||||||
|
.replace(
|
||||||
|
/(\/schema\/.*)\/tables\/([^/]*)(\/.*)?/,
|
||||||
|
'$1/tables/TABLE_NAME$3'
|
||||||
|
)
|
||||||
|
.replace(/(\/schema\/.*)\/views\/([^/]*)(\/.*)?/, '$1/views/VIEW_NAME$3')
|
||||||
|
.replace(
|
||||||
|
/(\/schema\/.*)\/functions\/([^/]*)(\/.*)?/,
|
||||||
|
'$1/functions/FUNCTION_NAME$3'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiExplorerHandler = () => {
|
||||||
|
return API_EXPLORER_PATH;
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteSchemasHandler = (path: string) => {
|
||||||
|
return (
|
||||||
|
REMOTE_SCHEMAS_PATH +
|
||||||
|
path.replace(/(\/manage\/)[^/]*(\/\w+.*)$/, '$1REMOTE_SCHEMA_NAME$2')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventsHandler = (path: string) => {
|
||||||
|
return (
|
||||||
|
EVENTS_PATH +
|
||||||
|
path.replace(/(\/manage\/triggers\/)[^/]*(\/\w+.*)$/, '$1TRIGGER_NAME$2')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitiseUrl = (rawPath: string) => {
|
||||||
|
const path = rawPath.replace(new RegExp(globals.urlPrefix, 'g'), '');
|
||||||
|
if (path.indexOf(DATA_PATH) === 0) {
|
||||||
|
return dataHandler(path.slice(DATA_PATH.length));
|
||||||
|
}
|
||||||
|
if (path.indexOf(API_EXPLORER_PATH) === 0) {
|
||||||
|
return apiExplorerHandler();
|
||||||
|
}
|
||||||
|
if (path.indexOf(REMOTE_SCHEMAS_PATH) === 0) {
|
||||||
|
return remoteSchemasHandler(path.slice(REMOTE_SCHEMAS_PATH.length));
|
||||||
|
}
|
||||||
|
if (path.indexOf(EVENTS_PATH) === 0) {
|
||||||
|
return eventsHandler(path.slice(EVENTS_PATH.length));
|
||||||
|
}
|
||||||
|
return '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
export { filterEventsBlockList, sanitiseUrl };
|
105
console/src/telemetry/index.ts
Normal file
105
console/src/telemetry/index.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import endpoints from '../Endpoints';
|
||||||
|
import globals from '../Globals';
|
||||||
|
import { filterEventsBlockList, sanitiseUrl } from './filters';
|
||||||
|
import { RUN_TIME_ERROR } from '../components/Main/Actions';
|
||||||
|
import { REDUX_LOCATION_CHANGE_ACTION_TYPE } from '../constants';
|
||||||
|
import { GetReduxState, ReduxAction } from '../types';
|
||||||
|
|
||||||
|
interface TelemetryGlobals {
|
||||||
|
serverVersion: string;
|
||||||
|
consoleMode: string;
|
||||||
|
cliUUID: string;
|
||||||
|
hasuraUUID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createClient = () => {
|
||||||
|
if (globals.enableTelemetry) {
|
||||||
|
try {
|
||||||
|
const client = new WebSocket(endpoints.telemetryServer);
|
||||||
|
client.onerror = e => {
|
||||||
|
console.error(`WebSocket Error for Events${e}`);
|
||||||
|
};
|
||||||
|
return client;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unable to initialise telemetry client', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = createClient();
|
||||||
|
if (client) {
|
||||||
|
const onClose = () => {
|
||||||
|
client = createClient();
|
||||||
|
if (client) {
|
||||||
|
client.onclose = onClose;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.onclose = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTelemetryConnectionReady = () => {
|
||||||
|
return !!(client && client.readyState === client.OPEN);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendEvent = (payload: any) => {
|
||||||
|
if (client && isTelemetryConnectionReady()) {
|
||||||
|
client.send(
|
||||||
|
JSON.stringify({ data: payload, topic: globals.telemetryTopic })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackReduxAction = (
|
||||||
|
action: ReduxAction,
|
||||||
|
getState: GetReduxState
|
||||||
|
) => {
|
||||||
|
const actionType = action.type;
|
||||||
|
// filter events
|
||||||
|
if (!filterEventsBlockList.includes(actionType)) {
|
||||||
|
const serverVersion = getState().main.serverVersion;
|
||||||
|
const url = sanitiseUrl(window.location.pathname);
|
||||||
|
|
||||||
|
const reqBody = {
|
||||||
|
server_version: serverVersion,
|
||||||
|
event_type: actionType,
|
||||||
|
url,
|
||||||
|
console_mode: globals.consoleMode,
|
||||||
|
cli_uuid: globals.cliUUID,
|
||||||
|
server_uuid: getState().telemetry.hasura_uuid,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLocationType = actionType === REDUX_LOCATION_CHANGE_ACTION_TYPE;
|
||||||
|
if (isLocationType) {
|
||||||
|
// capture page views
|
||||||
|
const payload = action.payload;
|
||||||
|
reqBody.url = sanitiseUrl(payload.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isErrorType = actionType === RUN_TIME_ERROR;
|
||||||
|
if (isErrorType) {
|
||||||
|
reqBody.data = action.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the data
|
||||||
|
sendEvent(reqBody);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackRuntimeError = (
|
||||||
|
telemeteryGlobals: TelemetryGlobals,
|
||||||
|
error: Error
|
||||||
|
) => {
|
||||||
|
const reqBody = {
|
||||||
|
server_version: telemeteryGlobals.serverVersion,
|
||||||
|
event_type: RUN_TIME_ERROR,
|
||||||
|
url: sanitiseUrl(window.location.pathname),
|
||||||
|
console_mode: telemeteryGlobals.consoleMode,
|
||||||
|
cli_uuid: telemeteryGlobals.cliUUID,
|
||||||
|
server_uuid: telemeteryGlobals.hasuraUUID,
|
||||||
|
data: { message: error.message, stack: error.stack },
|
||||||
|
};
|
||||||
|
sendEvent(reqBody);
|
||||||
|
};
|
9
console/src/types.ts
Normal file
9
console/src/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type ReduxAction = {
|
||||||
|
type: string;
|
||||||
|
payload: {
|
||||||
|
pathname: string;
|
||||||
|
};
|
||||||
|
data: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetReduxState = () => Record<string, any>;
|
@ -7,6 +7,7 @@ import {
|
|||||||
DONE_REQUEST,
|
DONE_REQUEST,
|
||||||
FAILED_REQUEST,
|
FAILED_REQUEST,
|
||||||
ERROR_REQUEST,
|
ERROR_REQUEST,
|
||||||
|
CONNECTION_FAILED,
|
||||||
} from '../components/App/Actions';
|
} from '../components/App/Actions';
|
||||||
import { globalCookiePolicy } from '../Endpoints';
|
import { globalCookiePolicy } from '../Endpoints';
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ const requestAction = (
|
|||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error('Request error: ', error);
|
console.error('Request error: ', error);
|
||||||
dispatch({ type: FAILED_REQUEST });
|
dispatch({ type: CONNECTION_FAILED });
|
||||||
if (ERROR) {
|
if (ERROR) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ERROR,
|
type: ERROR,
|
||||||
|
Loading…
Reference in New Issue
Block a user