Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add frontend context logging #1115

Merged
merged 5 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querybook",
"version": "3.15.5",
"version": "3.15.6",
"description": "A Big Data Webapp",
"private": true,
"scripts": {
Expand Down
15 changes: 11 additions & 4 deletions querybook/server/const/event_log.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from enum import Enum
from typing import TypedDict


class EventType(Enum):
# an api request
API = "api"
API = "API"
# websocket event
WEBSOCKET = "websocket"
WEBSOCKET = "WEBSOCKET"
# a UI element gets viewed
VIEW = "view"
VIEW = "VIEW"
# a UI element gets clicked
CLICK = "click"
CLICK = "CLICK"


class FrontendEvent(TypedDict):
timestamp: int
event_data: dict
event_type: str # value of EventType
2 changes: 2 additions & 0 deletions querybook/server/datasources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import utils
from . import table_upload
from . import tag
from . import event_log

# Flake8 :(
admin
Expand All @@ -30,3 +31,4 @@
utils
table_upload
tag
event_log
18 changes: 18 additions & 0 deletions querybook/server/datasources/event_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from app.datasource import register
from const.event_log import EventType, FrontendEvent
from lib.event_logger import event_logger


@register("/event_log/", methods=["POST"], api_logging=False)
def log_frontend_event(events: list[FrontendEvent]):
"""Log a list of frontend events.
Args:
events (list[FrontendEvent]): a list of frontend events
"""
for event in events:
event_logger.log(
event_type=EventType(event["type"]),
event_data=event["data"],
timestamp=event["timestamp"],
)
11 changes: 5 additions & 6 deletions querybook/server/lib/event_logger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ def __init__(self):
logger_name = QuerybookSettings.EVENT_LOGGER_NAME
self.logger = get_event_logger_class(logger_name)

def log(
self,
event_type: EventType,
event_data: dict,
):
def log(self, event_type: EventType, event_data: dict, timestamp: int = None):
try:
self.logger.log(
uid=current_user.id, event_type=event_type, event_data=event_data
uid=current_user.id,
event_type=event_type,
event_data=event_data,
timestamp=timestamp,
)
except Exception as e:
# catch any potential exceptions to avoid event logging
Expand Down
5 changes: 4 additions & 1 deletion querybook/server/lib/event_logger/base_event_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@ def _should_log_api_request(self, route: str, method: str) -> bool:
return True

@abstractmethod
def log(self, uid: int, event_type: EventType, event_data: dict) -> None:
def log(
self, uid: int, event_type: EventType, event_data: dict, timestamp: int = None
) -> None:
"""Log an event to some data store
Args:
uid (int): id of the user who performed the action
event_type (EventType): action event type, e.g. CLICK, VIEW
event_data (dict): addtional info of the event in JSON format.
timestamp (int): timestamp in milliseconds
"""
raise NotImplementedError()

Expand Down
12 changes: 10 additions & 2 deletions querybook/server/lib/event_logger/loggers/console_event_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ class ConsoleEventLogger(BaseEventLogger):
def logger_name(self) -> str:
return "console"

def log(self, uid: int, event_type: EventType, event_data: dict):
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
def log(
self, uid: int, event_type: EventType, event_data: dict, timestamp: int = None
):
now = (
datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
if timestamp is None
else datetime.utcfromtimestamp(timestamp / 1000).strftime(
"%Y-%m-%d %H:%M:%S"
)
)
event = f"created_at: {now}, uid={uid}, event_type={event_type}, event_data={event_data}"

LOG.info(f"{COLOR_YELLOW}{self.__class__.__name__} - {event}{COLOR_RESET}")
18 changes: 16 additions & 2 deletions querybook/server/lib/event_logger/loggers/db_event_logger.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from const.event_log import EventType
from lib.event_logger.base_event_logger import BaseEventLogger
from models.event_log import EventLog
Expand Down Expand Up @@ -62,7 +64,19 @@ def _api_deny_list(self) -> list:
},
]

def log(self, uid: int, event_type: EventType, event_data: dict):
def log(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use created_at field of EventLog?

self, uid: int, event_type: EventType, event_data: dict, timestamp: int = None
):
created_at = (
datetime.utcfromtimestamp(timestamp / 1000)
if timestamp is not None
else None
)
EventLog.create(
{"uid": uid, "event_type": event_type, "event_data": event_data}
{
"uid": uid,
"event_type": event_type,
"event_data": event_data,
"created_at": created_at,
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ class NullEventLogger(BaseEventLogger):
def logger_name(self) -> str:
return "null"

def log(self, uid: int, event_type: EventType, event_data: dict) -> None:
pass

def log_api_request(self, uid: int, route: str, method: str, params: dict) -> None:
def log(
self, uid: int, event_type: EventType, event_data: dict, timestamp: int
) -> None:
pass
3 changes: 3 additions & 0 deletions querybook/webapp/components/ChangeLog/ChangeLog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';

import { ComponentType } from 'const/analytics';
import { IChangeLogItem } from 'const/changeLog';
import { useTrackView } from 'hooks/useTrackView';
import localStore from 'lib/local-store';
import { CHANGE_LOG_KEY, ChangeLogValue } from 'lib/local-store/const';
import { sanitizeAndExtraMarkdown } from 'lib/markdown';
Expand Down Expand Up @@ -29,6 +31,7 @@ const ChangeLogMarkdown: React.FC<{ markdown: string }> = ({ markdown }) => {
};

export const ChangeLog: React.FunctionComponent = () => {
useTrackView(ComponentType.CHANGE_LOG);
const { date: changeLogDate } = useParams();
const [changeLogContent, setChangeLogContent] = React.useState<string[]>(
[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { QueryEngineStatusButton } from 'components/QueryEngineStatusButton/Quer
import { QueryExecutionButton } from 'components/QueryExecutionButton/QueryExecutionButton';
import { SearchContainer } from 'components/Search/SearchContainer';
import { UserMenu } from 'components/UserMenu/UserMenu';
import { ComponentType, ElementType } from 'const/analytics';
import { trackClick } from 'lib/analytics';
import { queryMetastoresSelector } from 'redux/dataSources/selector';
import { currentEnvironmentSelector } from 'redux/environment/selector';
import { IconButton } from 'ui/Button/IconButton';
Expand Down Expand Up @@ -41,6 +43,14 @@ export const EntitySidebar: React.FunctionComponent<IEntitySidebarProps> =
location.pathname ===
`/${environment.name}/`
}
onClick={() =>
trackClick({
component:
ComponentType.LEFT_SIDEBAR,
element:
ElementType.HOME_BUTTON,
})
}
/>
</Link>
<SearchContainer />
Expand All @@ -53,6 +63,14 @@ export const EntitySidebar: React.FunctionComponent<IEntitySidebarProps> =
`/${environment.name}/adhoc/`
)}
title="Adhoc"
onClick={() =>
trackClick({
component:
ComponentType.LEFT_SIDEBAR,
element:
ElementType.ADHOC_BUTTON,
})
}
/>
</Link>
<Link
Expand All @@ -66,6 +84,14 @@ export const EntitySidebar: React.FunctionComponent<IEntitySidebarProps> =
`/${environment.name}/doc_schedules/`
)}
title="Scheds"
onClick={() =>
trackClick({
component:
ComponentType.LEFT_SIDEBAR,
element:
ElementType.SCHEDS_BUTTON,
})
}
/>
</Link>
</>
Expand All @@ -79,6 +105,10 @@ export const EntitySidebar: React.FunctionComponent<IEntitySidebarProps> =
tooltipPos="right"
active={selectedEntity === 'datadoc'}
onClick={() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.DOCS_BUTTON,
});
onSelectEntity('datadoc');
}}
title="Docs"
Expand All @@ -89,7 +119,13 @@ export const EntitySidebar: React.FunctionComponent<IEntitySidebarProps> =
tooltip="Tables"
tooltipPos="right"
active={selectedEntity === 'table'}
onClick={() => onSelectEntity('table')}
onClick={() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.TABLES_BUTTON,
});
onSelectEntity('table');
}}
title="Tables"
/>
) : null}
Expand All @@ -98,11 +134,23 @@ export const EntitySidebar: React.FunctionComponent<IEntitySidebarProps> =
tooltip="Snippets"
tooltipPos="right"
active={selectedEntity === 'snippet'}
onClick={() => onSelectEntity('snippet')}
onClick={() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.SNIPS_BUTTON,
});
onSelectEntity('snippet');
}}
title="Snips"
/>
<QueryExecutionButton
onClick={() => onSelectEntity('execution')}
onClick={() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.EXECS_BUTTON,
});
onSelectEntity('execution');
}}
active={selectedEntity === 'execution'}
/>
</div>
Expand Down
10 changes: 9 additions & 1 deletion querybook/webapp/components/InfoMenuButton/InfoMenuButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';

import { ComponentType, ElementType } from 'const/analytics';
import { trackClick } from 'lib/analytics';
import localStore from 'lib/local-store';
import { CHANGE_LOG_KEY, ChangeLogValue } from 'lib/local-store/const';
import { navigateWithinEnv } from 'lib/utils/query-string';
Expand Down Expand Up @@ -105,7 +107,13 @@ export const InfoMenuButton: React.FunctionComponent = () => {
<div className="InfoMenuButton">
<IconButton
className="InfoMenuButton-button"
onClick={() => setShowPanel(true)}
onClick={() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.HELP_BUTTON,
});
setShowPanel(true);
}}
ref={buttonRef}
icon={'HelpCircle'}
tooltip={'Logs, Tips, Shortcuts, & FAQs'}
Expand Down
3 changes: 3 additions & 0 deletions querybook/webapp/components/Landing/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import React from 'react';
import { useDispatch } from 'react-redux';

import { QuerybookSidebarUIGuide } from 'components/UIGuide/QuerybookSidebarUIGuide';
import { ComponentType } from 'const/analytics';
import { useShallowSelector } from 'hooks/redux/useShallowSelector';
import { useBrowserTitle } from 'hooks/useBrowserTitle';
import { useTrackView } from 'hooks/useTrackView';
import { titleize } from 'lib/utils';
import { navigateWithinEnv } from 'lib/utils/query-string';
import { fetchDataDocs } from 'redux/dataDoc/action';
Expand Down Expand Up @@ -115,6 +117,7 @@ const DefaultLanding: React.FC = ({ children }) => {
};

const Landing: React.FC = () => {
useTrackView(ComponentType.LANDING_PAGE);
useBrowserTitle();

const customLandingConfig = window.CUSTOM_LANDING_PAGE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';

import { QueryEngineStatusViewer } from 'components/QueryEngineStatusViewer/QueryEngineStatusViewer';
import { ComponentType, ElementType } from 'const/analytics';
import { IQueryEngine, QueryEngineStatus } from 'const/queryEngine';
import {
queryEngineStatusToIconStatus,
queryEngineStatusToMessage,
} from 'const/queryStatusIcon';
import { TooltipDirection } from 'const/tooltip';
import { trackClick } from 'lib/analytics';
import { capitalize, titleize } from 'lib/utils';
import { fetchAllSystemStatus } from 'redux/queryEngine/action';
import {
Expand Down Expand Up @@ -221,7 +223,13 @@ export const QueryEngineStatusButton: React.FC<IProps> = ({
>
<IconButton
className="query-engine-status-button"
onClick={() => setShowPanel(true)}
onClick={() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.STATUS_BUTTON,
});
setShowPanel(true);
}}
ref={buttonRef}
icon={'Activity'}
tooltip={`Summary: ${queryEngineStatusToMessage[overallWorstQueryEngineStatus]}. Click to see details.`}
Expand Down
6 changes: 6 additions & 0 deletions querybook/webapp/components/Search/SearchContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React from 'react';

import { ComponentType, ElementType } from 'const/analytics';
import { trackClick } from 'lib/analytics';
import { navigateWithinEnv } from 'lib/utils/query-string';
import { IconButton } from 'ui/Button/IconButton';

import './SearchContainer.scss';

export const SearchContainer: React.FC = () => {
const navigateToSearch = React.useCallback(() => {
trackClick({
component: ComponentType.LEFT_SIDEBAR,
element: ElementType.ADHOC_BUTTON,
});
navigateWithinEnv('/search/', { isModal: true });
}, []);

Expand Down
Loading