From 684f1598bc85e34856fc2c9dc3d9c788f69d0778 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 3 Jun 2022 17:17:23 +0200 Subject: [PATCH 001/144] feat(tracker): add option to hide dom nodes --- .../tracker/src/main/app/observer/observer.ts | 33 ++++++++++++++----- tracker/tracker/src/main/app/sanitizer.ts | 16 +++++++-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index dcbba6365..79c92cc47 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -1,5 +1,5 @@ -import { - RemoveNodeAttribute, +import { + RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, @@ -220,9 +220,10 @@ export default abstract class Observer { } const parent = node.parentNode; let parentID: number | undefined; + // Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before) // TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though) - // TODO: Clean the logic (though now it workd fine) + // TODO: Clean the logic (though now it workd fine) if (!isInstance(node, HTMLHtmlElement) || !this.isTopContext) { if (parent === null) { this.unbindNode(node); @@ -239,6 +240,11 @@ export default abstract class Observer { } this.app.sanitizer.handleNode(id, parentID, node); } + if (parentID) { + if (this.app.sanitizer.isMaskedContainer(parentID)) { + return false; + } + } let sibling = node.previousSibling; while (sibling !== null) { const siblingID = this.app.nodes.getID(sibling); @@ -259,20 +265,31 @@ export default abstract class Observer { } if (isNew === true) { if (isInstance(node, Element)) { + let el: Element = node if (parentID !== undefined) { - this.app.send(new + if (this.app.sanitizer.isMaskedContainer(id)) { + const width = el.clientWidth; + const height = el.clientHeight; + console.log(width, height); + el = node.cloneNode() as Element; + (el as HTMLElement | SVGElement).style.width = width + 'px'; + (el as HTMLElement | SVGElement).style.height = height + 'px'; + console.log(el) + } + + this.app.send(new CreateElementNode( id, parentID, index, - node.tagName, + el.tagName, isSVGElement(node), ), ); } - for (let i = 0; i < node.attributes.length; i++) { - const attr = node.attributes[i]; - this.sendNodeAttribute(id, node, attr.nodeName, attr.value); + for (let i = 0; i < el.attributes.length; i++) { + const attr = el.attributes[i]; + this.sendNodeAttribute(id, el, attr.nodeName, attr.value); } } else if (isInstance(node, Text)) { // for text node id != 0, hence parentID !== undefined and parent is Element diff --git a/tracker/tracker/src/main/app/sanitizer.ts b/tracker/tracker/src/main/app/sanitizer.ts index 3e2e275ac..3b783041a 100644 --- a/tracker/tracker/src/main/app/sanitizer.ts +++ b/tracker/tracker/src/main/app/sanitizer.ts @@ -9,6 +9,7 @@ export interface Options { export default class Sanitizer { private readonly masked: Set = new Set(); + private readonly maskedContainers: Set = new Set(); private readonly options: Options; constructor(private readonly app: App, options: Partial) { @@ -21,10 +22,15 @@ export default class Sanitizer { handleNode(id: number, parentID: number, node: Node) { if ( this.masked.has(parentID) || - (isElementNode(node) && hasOpenreplayAttribute(node, 'masked')) + (isElementNode(node) && + (hasOpenreplayAttribute(node, 'masked'))) ) { this.masked.add(id); } + if (this.maskedContainers.has(parentID) || + (isElementNode(node) && hasOpenreplayAttribute(node, 'htmlmasked'))) { + this.maskedContainers.add(id); + } } sanitize(id: number, data: string): string { @@ -34,7 +40,7 @@ export default class Sanitizer { /[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█', ); - } + } if (this.options.obscureTextNumbers) { data = data.replace(/\d/g, '0'); } @@ -51,6 +57,9 @@ export default class Sanitizer { isMasked(id: number): boolean { return this.masked.has(id); } + isMaskedContainer(id: number) { + return this.maskedContainers.has(id); + } getInnerTextSecure(el: HTMLElement): string { const id = this.app.nodes.getID(el) @@ -61,6 +70,7 @@ export default class Sanitizer { clear(): void { this.masked.clear(); + this.maskedContainers.clear(); } -} \ No newline at end of file +} From a08ac6101aa6a77ab6fe4e1803a6c1223aa235f1 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Fri, 17 Jun 2022 10:45:41 +0200 Subject: [PATCH 002/144] chore(helm): change nginx-ingress default lb to ewma Signed-off-by: rjshrjndrn --- scripts/helmcharts/vars.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/helmcharts/vars.yaml b/scripts/helmcharts/vars.yaml index 9cb7b5b82..5517d0375 100644 --- a/scripts/helmcharts/vars.yaml +++ b/scripts/helmcharts/vars.yaml @@ -60,6 +60,7 @@ ingress-nginx: &ingress-nginx default-ssl-certificate: "app/openreplay-ssl" config: use-gzip: true + load-balace: ewma enable-real-ip: true # Enable LB forwarded protocol # Ref: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#use-forwarded-headers From 215d8897825343f6e19685aceb0b403b18e006f1 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Fri, 17 Jun 2022 11:01:13 +0200 Subject: [PATCH 003/144] ci(workers): build both ee and oss for deployment changes Signed-off-by: rjshrjndrn --- .github/workflows/api-ee.yaml | 1 + .github/workflows/workers-ee.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/api-ee.yaml b/.github/workflows/api-ee.yaml index 7c48fc7d9..9de1c53d3 100644 --- a/.github/workflows/api-ee.yaml +++ b/.github/workflows/api-ee.yaml @@ -6,6 +6,7 @@ on: - dev paths: - ee/api/** + - api/** name: Build and Deploy Chalice EE diff --git a/.github/workflows/workers-ee.yaml b/.github/workflows/workers-ee.yaml index 170f0761a..3d063c5b5 100644 --- a/.github/workflows/workers-ee.yaml +++ b/.github/workflows/workers-ee.yaml @@ -7,6 +7,7 @@ on: - dev paths: - ee/backend/** + - backend/** name: Build and deploy workers EE From 951ffa0320f9bb428cafa212d99658fd2702b78e Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Fri, 17 Jun 2022 14:48:06 +0200 Subject: [PATCH 004/144] fix(backend/db): fixed bug (index row size exceeds maximum) by adding left() func in sql requests --- backend/pkg/db/postgres/messages-common.go | 6 +++--- backend/pkg/db/postgres/messages-web-stats.go | 2 +- backend/pkg/db/postgres/messages-web.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/pkg/db/postgres/messages-common.go b/backend/pkg/db/postgres/messages-common.go index c21221ebf..0b3039a45 100644 --- a/backend/pkg/db/postgres/messages-common.go +++ b/backend/pkg/db/postgres/messages-common.go @@ -115,7 +115,7 @@ func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64 INSERT INTO events_common.requests ( session_id, timestamp, seq_index, url, duration, success ) VALUES ( - $1, $2, $3, $4, $5, $6 + $1, $2, $3, left($4, 2700), $5, $6 )` conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), url, duration, success) @@ -129,7 +129,7 @@ func (conn *Conn) InsertCustomEvent(sessionID uint64, timestamp uint64, index ui INSERT INTO events_common.customs ( session_id, timestamp, seq_index, name, payload ) VALUES ( - $1, $2, $3, $4, $5 + $1, $2, $3, left($4, 2700), $5 )` conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), name, payload) @@ -222,7 +222,7 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag INSERT INTO events_common.customs (session_id, seq_index, timestamp, name, payload, level) VALUES - ($1, $2, $3, $4, $5, 'error') + ($1, $2, $3, left($4, 2700), $5, 'error') `, sessionID, getSqIdx(e.MessageID), e.Timestamp, e.ContextString, e.Payload, ); err != nil { diff --git a/backend/pkg/db/postgres/messages-web-stats.go b/backend/pkg/db/postgres/messages-web-stats.go index 5f0b11e87..396f2e74d 100644 --- a/backend/pkg/db/postgres/messages-web-stats.go +++ b/backend/pkg/db/postgres/messages-web-stats.go @@ -57,7 +57,7 @@ func (conn *Conn) InsertWebStatsResourceEvent(sessionID uint64, e *ResourceEvent ) VALUES ( $1, $2, $3, $4, - $5, $6, $7, + left($5, 2700), $6, $7, $8, $9, NULLIF($10, '')::events.resource_method, NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), NULLIF($14, 0), NULLIF($15, 0) diff --git a/backend/pkg/db/postgres/messages-web.go b/backend/pkg/db/postgres/messages-web.go index 495ca53e4..db5b9d80c 100644 --- a/backend/pkg/db/postgres/messages-web.go +++ b/backend/pkg/db/postgres/messages-web.go @@ -229,7 +229,7 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet duration, success ) VALUES ( $1, $2, $3, - $4, $5, $6, $7, + left($4, 2700), $5, $6, $7, $8, $9, $10::smallint, NULLIF($11, '')::http_method, $12, $13 ) ON CONFLICT DO NOTHING` @@ -261,7 +261,7 @@ func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *G request_body, response_body ) VALUES ( $1, $2, $3, - $4, + left($4, 2700), $5, $6 ) ON CONFLICT DO NOTHING` conn.batchQueue(sessionID, sqlRequest, sessionID, e.Timestamp, e.MessageID, From 10c064c99cdffcfe3f50c3f716237fc56fe8d8b3 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 16 Jun 2022 19:27:01 +0200 Subject: [PATCH 005/144] feat(ui) - sessions - widget - pagination --- .../CustomMetricTableSessions.tsx | 8 +++- .../components/WidgetChart/WidgetChart.tsx | 27 ++++++----- .../components/WidgetForm/WidgetForm.tsx | 17 +++---- .../WidgetPreview/WidgetPreview.tsx | 8 +++- .../components/WidgetView/WidgetView.tsx | 3 +- .../Session_/Player/Controls/Controls.js | 1 - .../SelectDateRange/SelectDateRange.tsx | 1 + .../ui/SegmentSelection/SegmentSelection.js | 48 +++++++++++-------- .../segmentSelection.module.css | 7 +++ frontend/app/mstore/dashboardStore.ts | 9 +++- frontend/app/mstore/types/widget.ts | 12 +++-- frontend/app/services/MetricService.ts | 5 +- frontend/app/types/filter/filterType.ts | 2 +- frontend/app/utils.ts | 9 +++- 14 files changed, 103 insertions(+), 54 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx index 45cbd198a..8e2a56004 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx @@ -1,6 +1,8 @@ +import { useObserver } from 'mobx-react-lite'; import React from 'react'; import SessionItem from 'Shared/SessionItem'; import { Pagination } from 'UI'; +import { useStore } from 'App/mstore'; const PER_PAGE = 10; interface Props { @@ -11,8 +13,10 @@ interface Props { } function CustomMetricTableSessions(props: Props) { - const { data = { sessions: [], total: 0 }, isEdit = false, metric = {}, isTemplate } = props; + const { data = { sessions: [], total: 0 }, isEdit = false } = props; const currentPage = 1; + const { metricStore } = useStore(); + const metric: any = useObserver(() => metricStore.instance); return (
@@ -25,7 +29,7 @@ function CustomMetricTableSessions(props: Props) { this.props.updateCurrentPage(page)} + onPageChange={(page: any) => metric.updateKey('page', page)} limit={PER_PAGE} debounceRequest={500} /> diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 83894d299..5df1873fe 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -84,7 +84,7 @@ function WidgetChart(props: Props) { prevMetricRef.current = metric; const payload = isWidget ? { ...params } : { ...metricParams, ...metric.toJson() }; debounceRequest(metric, payload, isWidget); - }, [period, depsString]); + }, [period, depsString, _metric.page]); const renderChart = () => { const { metricType, viewType, metricOf } = metric; @@ -131,19 +131,24 @@ function WidgetChart(props: Props) { if (metricType === 'table') { if (metricOf === 'SESSIONS') { - return + ) } if (viewType === 'table') { - return ; + return ( + + ) } else if (viewType === 'pieChart') { return ( { const wasCreating = !metric.exists() - metricStore.save(metric, dashboardId).then((metric: any) => { - if (wasCreating) { - if (parseInt(dashboardId) > 0) { - history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId)); - } else { - history.replace(withSiteId(metricDetails(metric.metricId), siteId)); + metricStore.save(metric, dashboardId) + .then((metric: any) => { + if (wasCreating) { + if (parseInt(dashboardId) > 0) { + history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId)); + } else { + history.replace(withSiteId(metricDetails(metric.metricId), siteId)); + } } - } - }); + }); } const onDelete = async () => { diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index 679da3944..75e6eb270 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -5,6 +5,7 @@ import { useStore } from 'App/mstore'; import { SegmentSelection } from 'UI'; import { useObserver } from 'mobx-react-lite'; import SelectDateRange from 'Shared/SelectDateRange'; +import { FilterKey } from 'Types/filter/filterType'; interface Props { className?: string; @@ -16,6 +17,7 @@ function WidgetPreview(props: Props) { const metric: any = useObserver(() => metricStore.instance); const isTimeSeries = metric.metricType === 'timeseries'; const isTable = metric.metricType === 'table'; + const disableVisualization = useObserver(() => metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS); const chagneViewType = (e, { name, value }: any) => { metric.update({ [ name ]: value }); @@ -55,9 +57,11 @@ function WidgetPreview(props: Props) { onSelect={ chagneViewType } value={{ value: metric.viewType }} list={[ - { value: 'table', name: 'Table', icon: 'table' }, - { value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' }, + { value: 'table', name: 'Table', icon: 'table' }, + { value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' }, ]} + disabled={disableVisualization} + disabledMessage="Chart view is not supported" /> )} diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index 27d3b292c..338204f8a 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -10,6 +10,7 @@ import WidgetName from '../WidgetName'; import { withSiteId } from 'App/routes'; import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues'; import Breadcrumb from 'Shared/Breadcrumb'; +import { FilterKey } from 'Types/filter/filterType'; interface Props { history: any; match: any @@ -80,7 +81,7 @@ function WidgetView(props: Props) {
- { widget.metricOf !== 'SESSIONS' && widget.metricOf !== 'ERRORS' && ( + { widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && ( <> { (widget.metricType === 'table' || widget.metricType === 'timeseries') && } { widget.metricType === 'funnel' && } diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 48f738247..d42cff222 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -164,7 +164,6 @@ export default class Controls extends React.Component { } onKeyDown = (e) => { - console.log(e.key, e.target) if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { return; } diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index 13573b4d5..107c87337 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -5,6 +5,7 @@ import Period, { LAST_7_DAYS } from 'Types/app/period'; import { components } from 'react-select'; import DateRangePopup from 'Shared/DateRangeDropdown/DateRangePopup'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; +import cn from 'classnames'; interface Props { period: any, diff --git a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js index feebd694a..dde4d6b7c 100644 --- a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js +++ b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon } from 'UI'; +import { Icon, Popup } from 'UI'; import cn from 'classnames'; import styles from './segmentSelection.module.css'; @@ -9,29 +9,35 @@ class SegmentSelection extends React.Component { } render() { - const { className, list, small = false, extraSmall = false, primary = false, size = "normal", icons = false } = this.props; + const { className, list, small = false, extraSmall = false, primary = false, size = "normal", icons = false, disabled = false, disabledMessage = 'Not Allowed' } = this.props; return ( -
- { list.map(item => ( -
!item.disabled && this.setActiveItem(item) } - > - { item.icon && } -
{ item.name }
-
- )) - } -
+
+ { list.map(item => ( +
!item.disabled && this.setActiveItem(item) } + > + { item.icon && } +
{ item.name }
+
+ )) + } +
+ ); } } diff --git a/frontend/app/components/ui/SegmentSelection/segmentSelection.module.css b/frontend/app/components/ui/SegmentSelection/segmentSelection.module.css index 907f81e37..082e675c9 100644 --- a/frontend/app/components/ui/SegmentSelection/segmentSelection.module.css +++ b/frontend/app/components/ui/SegmentSelection/segmentSelection.module.css @@ -5,6 +5,7 @@ border: solid thin $gray-light; border-radius: 3px; overflow: hidden; + user-select: none; & .item { color: $gray-medium; @@ -79,4 +80,10 @@ .icons .item { padding: 4px !important; font-size: 12px; +} + +.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; } \ No newline at end of file diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 6ea803c82..98e0c5e78 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -434,8 +434,15 @@ export default class DashboardStore implements IDashboardSotre { fetchMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise { const period = this.period.toTimestamps() + const params = { ...period, ...data, key: metric.predefinedKey } + + if (metric.page && metric.limit) { + params['page'] = metric.page + params['limit'] = metric.limit + } + return new Promise((resolve, reject) => { - return metricService.getMetricChartData(metric, { ...period, ...data, key: metric.predefinedKey, page: 1, limit: 10 }, isWidget) + return metricService.getMetricChartData(metric, params, isWidget) .then((data: any) => { if (metric.metricType === 'predefined' && metric.viewType === 'overview') { const _data = { ...data, chart: getChartFormatter(this.period)(data.chart) } diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index b343b529f..de01ad834 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -33,10 +33,13 @@ export interface IWidget { dashboardId: any colSpan: number predefinedKey: string + + page: number + limit: number params: any - udpateKey(key: string, value: any): void + updateKey(key: string, value: any): void removeSeries(index: number): void addSeries(): void fromJson(json: any): void @@ -68,6 +71,8 @@ export default class Widget implements IWidget { dashboards: any[] = [] dashboardIds: any[] = [] config: any = {} + page: number = 1 + limit: number = 5 params: any = { density: 70 } sessionsLoading: boolean = false @@ -100,6 +105,7 @@ export default class Widget implements IWidget { dashboardId: observable, colSpan: observable, series: observable, + page: observable, addSeries: action, removeSeries: action, @@ -107,14 +113,14 @@ export default class Widget implements IWidget { toJson: action, validate: action, update: action, - udpateKey: action, + updateKey: action, }) const filterSeries = new FilterSeries() this.series.push(filterSeries) } - udpateKey(key: string, value: any) { + updateKey(key: string, value: any) { this[key] = value } diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index 3bc4d4ef9..cfab5ae39 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -1,6 +1,7 @@ import Widget, { IWidget } from "App/mstore/types/widget"; import APIClient from 'App/api_client'; import { IFilter } from "App/mstore/types/filter"; +import { fetchErrorCheck } from "App/utils"; export interface IMetricService { initClient(client?: APIClient): void; @@ -59,8 +60,8 @@ export default class MetricService implements IMetricService { const method = isCreating ? 'post' : 'put'; const url = isCreating ? '/metrics' : '/metrics/' + data[Widget.ID_KEY]; return this.client[method](url, data) - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data || {}); + .then(fetchErrorCheck) + .then((response: { data: any; }) => response.data || {}) } /** diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 4194b759b..772bea55e 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -93,5 +93,5 @@ export enum FilterKey { GRAPHQL_RESPONSE_BODY = "GRAPHQL_RESPONSE_BODY", SESSIONS = 'SESSIONS', - ERRORS = 'ERRORS' + ERRORS = 'js_exception' } \ No newline at end of file diff --git a/frontend/app/utils.ts b/frontend/app/utils.ts index 39b9cd3f7..4a2b57a56 100644 --- a/frontend/app/utils.ts +++ b/frontend/app/utils.ts @@ -308,4 +308,11 @@ export const exportCSVFile = (headers, items, fileTitle) => { document.body.removeChild(link); } } -} \ No newline at end of file +} + +export const fetchErrorCheck = (response: any) => { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); +} From 93455cd746d3aabd86f24437ba180ce64d3de425 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 17 Jun 2022 15:38:16 +0200 Subject: [PATCH 006/144] feat(ui) - sessions - widget - pagination --- .../CustomMetricTableErrors.tsx | 44 ++++++++++-- .../CustomMetricTableSessions.tsx | 34 ++++----- .../DashboardWidgetGrid.tsx | 6 +- .../Errors/ErrorLabel/ErrorLabel.tsx | 23 ++++++ .../components/Errors/ErrorLabel/index.ts | 1 + .../Errors/ErrorListItem/ErrorListItem.tsx | 71 +++++++++++++++++-- .../components/Errors/ErrorName/ErrorName.tsx | 15 ++++ .../components/Errors/ErrorName/index.ts | 1 + .../components/WidgetChart/WidgetChart.tsx | 21 ++++-- .../components/WidgetView/WidgetView.tsx | 4 +- .../Errors/List/ListItem/ListItem.js | 3 +- .../Funnels/FunnelWidget/FunnelBar.tsx | 2 +- frontend/app/mstore/dashboardStore.ts | 8 ++- frontend/app/mstore/types/error.ts | 10 ++- frontend/app/mstore/types/funnel.ts | 4 +- frontend/app/mstore/types/widget.ts | 13 ++-- frontend/app/services/MetricService.ts | 2 +- 17 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx create mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts create mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx create mode 100644 frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx index 3806d55f5..91b95a163 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors/CustomMetricTableErrors.tsx @@ -1,15 +1,49 @@ import React from 'react'; +import { Pagination } from 'UI'; +import ErrorListItem from '../../../components/Errors/ErrorListItem'; - +const PER_PAGE = 5; interface Props { - + metric: any; + isTemplate?: boolean; + isEdit?: boolean; } -function CustomMetricTableErrors(props) { +function CustomMetricTableErrors(props: Props) { + const { metric, isEdit = false } = props; + return (
- + {metric.data.errors && metric.data.errors.map((error: any, index: any) => ( + + ))} + + {isEdit && ( +
+ metric.updateKey('page', page)} + limit={metric.limit} + debounceRequest={500} + /> +
+ )} + + {!isEdit && ( + + )}
); } -export default CustomMetricTableErrors; \ No newline at end of file +export default CustomMetricTableErrors; + +const ViewMore = ({ total, limit }: any) => total > limit && ( +
+
+
+ All {total} errors +
+
+
+); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx index 8e2a56004..0e5ff0587 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx @@ -1,51 +1,45 @@ import { useObserver } from 'mobx-react-lite'; import React from 'react'; import SessionItem from 'Shared/SessionItem'; -import { Pagination } from 'UI'; -import { useStore } from 'App/mstore'; +import { Pagination, NoContent } from 'UI'; -const PER_PAGE = 10; interface Props { - data: any - metric?: any + metric: any; isTemplate?: boolean; isEdit?: boolean; } function CustomMetricTableSessions(props: Props) { - const { data = { sessions: [], total: 0 }, isEdit = false } = props; - const currentPage = 1; - const { metricStore } = useStore(); - const metric: any = useObserver(() => metricStore.instance); + const { isEdit = false, metric } = props; - return ( -
- {data.sessions && data.sessions.map((session: any, index: any) => ( - + return useObserver(() => ( + + {metric.data.sessions && metric.data.sessions.map((session: any, index: any) => ( + ))} {isEdit && (
metric.updateKey('page', page)} - limit={PER_PAGE} + limit={metric.data.total} debounceRequest={500} />
)} {!isEdit && ( - + )} -
- ); + + )); } export default CustomMetricTableSessions; -const ViewMore = ({ total }: any) => total > PER_PAGE && ( +const ViewMore = ({ total, limit }: any) => total > limit && (
diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index b60fa2de2..cf80137dd 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -10,7 +10,7 @@ interface Props { onEditHandler: () => void; id?: string; } -function DashboardWidgetGrid(props) { +function DashboardWidgetGrid(props: Props) { const { dashboardId, siteId } = props; const { dashboardStore } = useStore(); const loading = useObserver(() => dashboardStore.isLoading); @@ -31,12 +31,12 @@ function DashboardWidgetGrid(props) { } >
- {list && list.map((item, index) => ( + {list && list.map((item: any, index: any) => ( dashboard.swapWidgetPosition(dragIndex, hoverIndex)} + moveListItem={(dragIndex: any, hoverIndex: any) => dashboard.swapWidgetPosition(dragIndex, hoverIndex)} dashboardId={dashboardId} siteId={siteId} isWidget={true} diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx new file mode 100644 index 000000000..1899ccbb9 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/ErrorLabel.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import cn from "classnames"; + +interface Props { + className?: string; + topValue: string; + topValueSize?: string; + bottomValue: string; + topMuted?: boolean; + bottomMuted?: boolean; +} +function ErrorLabel({ className, topValue, topValueSize = 'text-base', bottomValue, topMuted = false, bottomMuted = false }: Props) { + return ( +
+
{ topValue }
+
{ bottomValue }
+
+ ) +} + +ErrorLabel.displayName = "ErrorLabel"; + +export default ErrorLabel; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts new file mode 100644 index 000000000..716e5986c --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorLabel/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorLabel' \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx index 6f230b516..95ce3f021 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx @@ -1,14 +1,77 @@ import React from 'react'; +import cn from 'classnames'; +import moment from 'moment'; +import { error as errorRoute } from 'App/routes'; +import { IGNORED, RESOLVED } from 'Types/errorInfo'; +import { Link, Label } from 'UI'; +import ErrorName from '../ErrorName'; +import ErrorLabel from '../ErrorLabel'; +import { BarChart, Bar, YAxis, Tooltip, XAxis } from 'recharts'; +import { Styles } from '../../../Widgets/common'; +import { diffFromNowString } from 'App/date'; interface Props { error: any; + className?: string; } function ErrorListItem(props: Props) { + const { error, className = '' } = props; return ( -
- Errors List Item -
+
+
+
+ +
+ { error.message } +
+
+
+ + + + } /> + + + + + +
); } -export default ErrorListItem; \ No newline at end of file +export default ErrorListItem; + +const CustomTooltip = ({ active, payload, label }: any) => { + if (active) { + const p = payload[0].payload; + return ( +
+

{`${p.timestamp ? moment(p.timestamp).format('l') : ''}`}

+

Sessions: {p.count}

+
+ ); + } + + return null; + }; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx new file mode 100644 index 000000000..646b5c68f --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorName/ErrorName.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import cn from "classnames"; + +function ErrorText({ className, icon, name, message, bold, lineThrough = false }: any) { + return ( +
+ { name } + { message } +
+ ); +} + +ErrorText.displayName = "ErrorText"; + +export default ErrorText; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts new file mode 100644 index 000000000..548c10f38 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorName/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorName'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 5df1873fe..0d9e3283a 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -12,11 +12,13 @@ import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMe import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted' +import { FilterKey } from 'Types/filter/filterType'; import FunnelWidget from 'App/components/Funnels/FunnelWidget'; import ErrorsWidget from '../Errors/ErrorsWidget'; import SessionWidget from '../Sessions/SessionWidget'; -import CustomMetricTableSessions from '../../Widgets/CustomMetricsWidgets/CustomMetricTableSessions'; +import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions'; +import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors'; interface Props { metric: any; isWidget?: boolean; @@ -130,12 +132,19 @@ function WidgetChart(props: Props) { } if (metricType === 'table') { - if (metricOf === 'SESSIONS') { + if (metricOf === FilterKey.SESSIONS) { return ( + ) + } + if (metricOf === FilterKey.ERRORS) { + return ( + @@ -165,7 +174,7 @@ function WidgetChart(props: Props) { return
Unknown
; } return useObserver(() => ( - + {renderChart()} )); diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index 338204f8a..e9acace0b 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -54,11 +54,11 @@ function WidgetView(props: Props) { className={cn( "px-6 py-4 flex justify-between items-center", { - 'cursor-pointer hover:bg-active-blue hover:shadow-border-blue': !expanded, + 'cursor-pointer hover:bg-active-blue hover:shadow-border-blue rounded': !expanded, } )} onClick={openEdit} - > + >

{ if (active) { - const p = payload[0].payload; + const p = payload[0].payload; return (

{`${moment(p.timestamp).format('l')}`}

diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx index b846ed94b..4d945b483 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelBar.tsx @@ -7,7 +7,7 @@ interface Props { } function FunnelBar(props: Props) { const { filter } = props; - const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues); + // const completedPercentage = calculatePercentage(filter.sessionsCount, filter.dropDueToIssues); return (
diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 98e0c5e78..ba0e9ee83 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -8,6 +8,8 @@ import { getChartFormatter } from 'Types/dashboard/helper'; import Filter, { IFilter } from "./types/filter"; import Funnel from "./types/funnel"; import Session from "./types/session"; +import Error from "./types/error"; +import { FilterKey } from 'Types/filter/filterType'; export interface IDashboardSotre { dashboards: IDashboard[] @@ -132,7 +134,7 @@ export default class DashboardStore implements IDashboardSotre { fetchMetricChartData: action }) - const drillDownPeriod = Period({ rangeName: LAST_24_HOURS }).toTimestamps(); + const drillDownPeriod = Period({ rangeName: LAST_30_DAYS }).toTimestamps(); this.drillDownFilter.updateKey('startTimestamp', drillDownPeriod.startTimestamp) this.drillDownFilter.updateKey('endTimestamp', drillDownPeriod.endTimestamp) } @@ -459,8 +461,10 @@ export default class DashboardStore implements IDashboardSotre { } // TODO refactor to widget class - if (metric.metricOf === 'SESSIONS') { + if (metric.metricOf === FilterKey.SESSIONS) { _data['sessions'] = data.sessions.map((s: any) => new Session().fromJson(s)) + } else if (metric.metricOf === FilterKey.ERRORS) { + _data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s)) } else { if (data.hasOwnProperty('chart')) { _data['chart'] = getChartFormatter(this.period)(data.chart) diff --git a/frontend/app/mstore/types/error.ts b/frontend/app/mstore/types/error.ts index 7542aca4b..2f8b8fbec 100644 --- a/frontend/app/mstore/types/error.ts +++ b/frontend/app/mstore/types/error.ts @@ -2,7 +2,6 @@ export default class Error { sessionId: string = '' messageId: string = '' - timestamp: string = '' errorId: string = '' projectId: string = '' source: string = '' @@ -11,6 +10,12 @@ export default class Error { time: string = '' function: string = '?' stack0InfoString: string = '' + + chart: any = [] + sessions: number = 0 + users: number = 0 + lastOccurrence: string = '' + timestamp: string = '' constructor() { } @@ -26,7 +31,10 @@ export default class Error { this.message = json.message this.time = json.time this.function = json.function + this.chart = json.chart this.stack0InfoString = getStck0InfoString(json.stack || []) + this.sessions = json.sessions + this.users = json.users return this } } diff --git a/frontend/app/mstore/types/funnel.ts b/frontend/app/mstore/types/funnel.ts index 253fdb076..52b702d55 100644 --- a/frontend/app/mstore/types/funnel.ts +++ b/frontend/app/mstore/types/funnel.ts @@ -31,9 +31,9 @@ export default class Funnel implements IFunnel { const firstStage = json.stages[0] const lastStage = json.stages[json.stages.length - 1] this.lostConversions = firstStage.sessionsCount - lastStage.sessionsCount - this.lostConversionsPercentage = this.lostConversions / firstStage.sessionsCount * 100 + this.lostConversionsPercentage = Math.round(this.lostConversions / firstStage.sessionsCount * 100) this.totalConversions = lastStage.sessionsCount - this.totalConversionsPercentage = this.totalConversions / firstStage.sessionsCount * 100 + this.totalConversionsPercentage = Math.round(this.totalConversions / firstStage.sessionsCount * 100) this.conversionImpact = this.lostConversions ? Math.round((this.lostConversions / firstStage.sessionsCount) * 100) : 0; this.stages = json.stages ? json.stages.map((stage: any, index: number) => new FunnelStage().fromJSON(stage, firstStage.sessionsCount, index > 0 ? json.stages[index - 1].sessionsCount : stage.sessionsCount)) : [] this.affectedUsers = firstStage.usersCount ? firstStage.usersCount - lastStage.usersCount : 0; diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index de01ad834..d84a18efc 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -5,6 +5,7 @@ import { metricService } from "App/services"; import Session from "App/mstore/types/session"; import Funnelissue from 'App/mstore/types/funnelIssue'; import { issueOptions } from 'App/constants/filterOptions'; +import { FilterKey } from 'Types/filter/filterType'; export interface IWidget { metricId: any @@ -58,7 +59,7 @@ export default class Widget implements IWidget { widgetId: any = undefined name: string = "New Metric" // metricType: string = "timeseries" - metricType: string = "table" + metricType: string = "timeseries" metricOf: string = "sessionCount" metricValue: string = "" viewType: string = "lineChart" @@ -79,6 +80,8 @@ export default class Widget implements IWidget { position: number = 0 data: any = { + sessions: [], + total: 0, chart: [], namesMap: {}, avg: 0, @@ -93,7 +96,7 @@ export default class Widget implements IWidget { constructor() { makeAutoObservable(this, { sessionsLoading: observable, - data: observable.ref, + data: observable, metricId: observable, widgetId: observable, name: observable, @@ -183,7 +186,7 @@ export default class Widget implements IWidget { series: this.series.map((series: any) => series.toJson()), config: { ...this.config, - col: this.metricType === 'funnel' ? 4 : this.config.col + col: this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS ? 4 : this.config.col }, } } @@ -204,7 +207,7 @@ export default class Widget implements IWidget { setData(data: any) { runInAction(() => { - Object.assign(this.data, data) + this.data = data; }) } @@ -236,8 +239,6 @@ export default class Widget implements IWidget { fetchIssue(funnelId: any, issueId: any, params: any): Promise { return new Promise((resolve, reject) => { metricService.fetchIssue(funnelId, issueId, params).then((response: any) => { - response = response[0] - console.log('response', response) resolve({ issue: new Funnelissue().fromJSON(response.issue), sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)), diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index cfab5ae39..2696eec3b 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -89,7 +89,7 @@ export default class MetricService implements IMetricService { getMetricChartData(metric: IWidget, data: any, isWidget: boolean = false): Promise { const path = isWidget ? `/metrics/${metric.metricId}/chart` : `/metrics/try`; return this.client.post(path, data) - .then((response: { json: () => any; }) => response.json()) + .then(fetchErrorCheck) .then((response: { data: any; }) => response.data || {}); } From cbc4c25a8ea81c17f13d4da8aa7a21f0c4c36a7b Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Fri, 17 Jun 2022 15:48:05 +0200 Subject: [PATCH 007/144] feat(ui) - sessions - widget - data reload --- .../Dashboard/components/WidgetChart/WidgetChart.tsx | 2 +- frontend/app/mstore/types/error.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 0d9e3283a..e6368be84 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -86,7 +86,7 @@ function WidgetChart(props: Props) { prevMetricRef.current = metric; const payload = isWidget ? { ...params } : { ...metricParams, ...metric.toJson() }; debounceRequest(metric, payload, isWidget); - }, [period, depsString, _metric.page]); + }, [period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType]); const renderChart = () => { const { metricType, viewType, metricOf } = metric; diff --git a/frontend/app/mstore/types/error.ts b/frontend/app/mstore/types/error.ts index 2f8b8fbec..54382f081 100644 --- a/frontend/app/mstore/types/error.ts +++ b/frontend/app/mstore/types/error.ts @@ -1,4 +1,3 @@ - export default class Error { sessionId: string = '' messageId: string = '' @@ -14,6 +13,7 @@ export default class Error { chart: any = [] sessions: number = 0 users: number = 0 + firstOccurrence: string = '' lastOccurrence: string = '' timestamp: string = '' @@ -23,7 +23,6 @@ export default class Error { fromJSON(json: any) { this.sessionId = json.sessionId this.messageId = json.messageId - this.timestamp = json.timestamp this.errorId = json.errorId this.projectId = json.projectId this.source = json.source @@ -31,15 +30,20 @@ export default class Error { this.message = json.message this.time = json.time this.function = json.function - this.chart = json.chart this.stack0InfoString = getStck0InfoString(json.stack || []) + + this.chart = json.chart this.sessions = json.sessions this.users = json.users + this.firstOccurrence = json.firstOccurrence + this.lastOccurrence = json.lastOccurrence + this.timestamp = json.timestamp + return this } } -function getStck0InfoString(stack) { +function getStck0InfoString(stack: any) { const stack0 = stack[0]; if (!stack0) return ""; let s = stack0.function || ""; From f34c433a42b4d9534cafdfd1941349e0a5fda540 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Fri, 17 Jun 2022 16:15:49 +0200 Subject: [PATCH 008/144] chore(backend): clean go mod Signed-off-by: rjshrjndrn --- backend/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 8de0b0279..11f0100e9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,13 +6,14 @@ WORKDIR /root COPY go.mod . COPY go.sum . -RUN go mod download +RUN go mod tidy && go mod download FROM prepare AS build COPY cmd cmd COPY pkg pkg COPY internal internal +RUN go mod tidy ARG SERVICE_NAME RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openreplay/backend/cmd/$SERVICE_NAME From 592cbd5fd5210d154771d19013284d36f07c8045 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Fri, 17 Jun 2022 16:54:39 +0200 Subject: [PATCH 009/144] feat(api): errors search ignore `Script error` on query level --- api/chalicelib/core/errors.py | 2 ++ ee/api/chalicelib/core/errors.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/api/chalicelib/core/errors.py b/api/chalicelib/core/errors.py index 2026f9232..bbdea726b 100644 --- a/api/chalicelib/core/errors.py +++ b/api/chalicelib/core/errors.py @@ -436,6 +436,8 @@ def search(data: schemas.SearchErrorsSchema, project_id, user_id, flows=False): pg_sub_query = __get_basic_constraints(platform, project_key="sessions.project_id") pg_sub_query += ["sessions.start_ts>=%(startDate)s", "sessions.start_ts<%(endDate)s", "source ='js_exception'", "pe.project_id=%(project_id)s"] + # To ignore Script error + pg_sub_query.append("pe.message!='Script error.'") pg_sub_query_chart = __get_basic_constraints(platform, time_constraint=False, chart=True, project_key=None) # pg_sub_query_chart.append("source ='js_exception'") pg_sub_query_chart.append("errors.error_id =details.error_id") diff --git a/ee/api/chalicelib/core/errors.py b/ee/api/chalicelib/core/errors.py index 9477f8ec7..07a5e10ba 100644 --- a/ee/api/chalicelib/core/errors.py +++ b/ee/api/chalicelib/core/errors.py @@ -477,6 +477,8 @@ def search(data: schemas.SearchErrorsSchema, project_id, user_id, flows=False): pg_sub_query = __get_basic_constraints_pg(platform, project_key="sessions.project_id") pg_sub_query += ["sessions.start_ts>=%(startDate)s", "sessions.start_ts<%(endDate)s", "source ='js_exception'", "pe.project_id=%(project_id)s"] + # To ignore Script error + pg_sub_query.append("pe.message!='Script error.'") pg_sub_query_chart = __get_basic_constraints_pg(platform, time_constraint=False, chart=True, project_key=None) # pg_sub_query_chart.append("source ='js_exception'") pg_sub_query_chart.append("errors.error_id =details.error_id") @@ -649,6 +651,8 @@ def search_deprecated(data: schemas.SearchErrorsSchema, project_id, user_id, flo platform = f.value[0] ch_sub_query = __get_basic_constraints(platform) ch_sub_query.append("source ='js_exception'") + # To ignore Script error + ch_sub_query.append("message!='Script error.'") statuses = [] error_ids = None # Clickhouse keeps data for the past month only, so no need to search beyond that From 3da78cfe628a3bb4b401a27b4a81fdc2f6960426 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Fri, 17 Jun 2022 17:33:52 +0200 Subject: [PATCH 010/144] feat(backend): added metadata insertion retrier (temp solution) --- backend/pkg/db/cache/messages-common.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/pkg/db/cache/messages-common.go b/backend/pkg/db/cache/messages-common.go index 8ca7b2f85..90b97efbf 100644 --- a/backend/pkg/db/cache/messages-common.go +++ b/backend/pkg/db/cache/messages-common.go @@ -1,7 +1,9 @@ package cache import ( + "log" . "openreplay/backend/pkg/messages" + "time" // . "openreplay/backend/pkg/db/types" ) @@ -45,6 +47,12 @@ func (c *PGCache) InsertMetadata(sessionID uint64, metadata *Metadata) error { return nil } if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { + // Try to insert metadata after one minute + time.AfterFunc(time.Minute, func() { + if err := c.Conn.InsertMetadata(sessionID, keyNo, metadata.Value); err != nil { + log.Printf("metadata retry err: %s", err) + } + }) return err } session.SetMetadata(keyNo, metadata.Value) From 623e241afbb18ed09d05f03035bbe290fba06d27 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 20 Jun 2022 09:26:05 +0200 Subject: [PATCH 011/144] feat(backend): moved recording sessionStart to db into http service and sessionEnd into ender service (#545) Co-authored-by: Alexander Zavorotynskiy --- backend/cmd/ender/main.go | 11 +++++- backend/internal/config/ender/config.go | 24 +++++++------ backend/internal/db/datasaver/messages.go | 4 +-- backend/internal/http/router/handlers-web.go | 10 ++++-- backend/pkg/db/cache/messages-common.go | 12 +++---- backend/pkg/db/cache/messages-ios.go | 2 +- backend/pkg/db/cache/messages-web.go | 32 +++++++++++++++-- backend/pkg/db/postgres/messages-common.go | 36 +++++++++++--------- 8 files changed, 90 insertions(+), 41 deletions(-) diff --git a/backend/cmd/ender/main.go b/backend/cmd/ender/main.go index 82f9b51d0..9751f26f3 100644 --- a/backend/cmd/ender/main.go +++ b/backend/cmd/ender/main.go @@ -4,6 +4,8 @@ import ( "log" "openreplay/backend/internal/config/ender" "openreplay/backend/internal/sessionender" + "openreplay/backend/pkg/db/cache" + "openreplay/backend/pkg/db/postgres" "openreplay/backend/pkg/monitoring" "time" @@ -30,6 +32,9 @@ func main() { // Load service configuration cfg := ender.New() + pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres), cfg.ProjectExpirationTimeoutMs) + defer pg.Close() + // Init all modules statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) sessions, err := sessionender.New(metrics, intervals.EVENTS_SESSION_END_TIMEOUT, cfg.PartitionsNumber) @@ -70,8 +75,12 @@ func main() { // Find ended sessions and send notification to other services sessions.HandleEndedSessions(func(sessionID uint64, timestamp int64) bool { msg := &messages.SessionEnd{Timestamp: uint64(timestamp)} + if err := pg.InsertSessionEnd(sessionID, msg.Timestamp); err != nil { + log.Printf("can't save sessionEnd to database, sessID: %d", sessionID) + return false + } if err := producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(msg)); err != nil { - log.Printf("can't send SessionEnd to trigger topic: %s; sessID: %d", err, sessionID) + log.Printf("can't send sessionEnd to topic: %s; sessID: %d", err, sessionID) return false } return true diff --git a/backend/internal/config/ender/config.go b/backend/internal/config/ender/config.go index 5898c69ec..7203eae33 100644 --- a/backend/internal/config/ender/config.go +++ b/backend/internal/config/ender/config.go @@ -5,19 +5,23 @@ import ( ) type Config struct { - GroupEnder string - LoggerTimeout int - TopicRawWeb string - ProducerTimeout int - PartitionsNumber int + Postgres string + ProjectExpirationTimeoutMs int64 + GroupEnder string + LoggerTimeout int + TopicRawWeb string + ProducerTimeout int + PartitionsNumber int } func New() *Config { return &Config{ - GroupEnder: env.String("GROUP_ENDER"), - LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"), - TopicRawWeb: env.String("TOPIC_RAW_WEB"), - ProducerTimeout: 2000, - PartitionsNumber: env.Int("PARTITIONS_NUMBER"), + Postgres: env.String("POSTGRES_STRING"), + ProjectExpirationTimeoutMs: 1000 * 60 * 20, + GroupEnder: env.String("GROUP_ENDER"), + LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"), + TopicRawWeb: env.String("TOPIC_RAW_WEB"), + ProducerTimeout: 2000, + PartitionsNumber: env.Int("PARTITIONS_NUMBER"), } } diff --git a/backend/internal/db/datasaver/messages.go b/backend/internal/db/datasaver/messages.go index 0f24d237e..112559fb1 100644 --- a/backend/internal/db/datasaver/messages.go +++ b/backend/internal/db/datasaver/messages.go @@ -19,9 +19,9 @@ func (mi *Saver) InsertMessage(sessionID uint64, msg Message) error { // Web case *SessionStart: - return mi.pg.InsertWebSessionStart(sessionID, m) + return mi.pg.HandleWebSessionStart(sessionID, m) case *SessionEnd: - return mi.pg.InsertWebSessionEnd(sessionID, m) + return mi.pg.HandleWebSessionEnd(sessionID, m) case *UserID: return mi.pg.InsertWebUserID(sessionID, m) case *UserAnonymousID: diff --git a/backend/internal/http/router/handlers-web.go b/backend/internal/http/router/handlers-web.go index 7514a78fc..69cbe4675 100644 --- a/backend/internal/http/router/handlers-web.go +++ b/backend/internal/http/router/handlers-web.go @@ -98,7 +98,7 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) expTime := startTime.Add(time.Duration(p.MaxSessionDuration) * time.Millisecond) tokenData = &token.TokenData{ID: sessionID, ExpTime: expTime.UnixMilli()} - e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(&SessionStart{ + sessionStart := &SessionStart{ Timestamp: req.Timestamp, ProjectID: uint64(p.ProjectID), TrackerVersion: req.TrackerVersion, @@ -115,7 +115,13 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request) UserDeviceMemorySize: req.DeviceMemory, UserDeviceHeapSize: req.JsHeapSizeLimit, UserID: req.UserID, - })) + } + + // Save sessionStart to db + e.services.Database.InsertWebSessionStart(sessionID, sessionStart) + + // Send sessionStart message to kafka + e.services.Producer.Produce(e.cfg.TopicRawWeb, tokenData.ID, Encode(sessionStart)) } ResponseWithJSON(w, &StartSessionResponse{ diff --git a/backend/pkg/db/cache/messages-common.go b/backend/pkg/db/cache/messages-common.go index 90b97efbf..eb4365a61 100644 --- a/backend/pkg/db/cache/messages-common.go +++ b/backend/pkg/db/cache/messages-common.go @@ -7,18 +7,16 @@ import ( // . "openreplay/backend/pkg/db/types" ) -func (c *PGCache) insertSessionEnd(sessionID uint64, timestamp uint64) error { - //duration, err := c.Conn.InsertSessionEnd(sessionID, timestamp) +func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) error { _, err := c.Conn.InsertSessionEnd(sessionID, timestamp) if err != nil { return err } + return nil +} + +func (c *PGCache) HandleSessionEnd(sessionID uint64) error { c.DeleteSession(sessionID) - // session, err := c.GetSession(sessionID) - // if err != nil { - // return err - // } - // session.Duration = &duration return nil } diff --git a/backend/pkg/db/cache/messages-ios.go b/backend/pkg/db/cache/messages-ios.go index 4bbc8c1f5..4195976c3 100644 --- a/backend/pkg/db/cache/messages-ios.go +++ b/backend/pkg/db/cache/messages-ios.go @@ -32,7 +32,7 @@ func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) er } func (c *PGCache) InsertIOSSessionEnd(sessionID uint64, e *IOSSessionEnd) error { - return c.insertSessionEnd(sessionID, e.Timestamp) + return c.InsertSessionEnd(sessionID, e.Timestamp) } func (c *PGCache) InsertIOSScreenEnter(sessionID uint64, screenEnter *IOSScreenEnter) error { diff --git a/backend/pkg/db/cache/messages-web.go b/backend/pkg/db/cache/messages-web.go index 71f2c38d0..7da7006af 100644 --- a/backend/pkg/db/cache/messages-web.go +++ b/backend/pkg/db/cache/messages-web.go @@ -7,6 +7,30 @@ import ( ) func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error { + return c.Conn.InsertSessionStart(sessionID, &Session{ + SessionID: sessionID, + Platform: "web", + Timestamp: s.Timestamp, + ProjectID: uint32(s.ProjectID), + TrackerVersion: s.TrackerVersion, + RevID: s.RevID, + UserUUID: s.UserUUID, + UserOS: s.UserOS, + UserOSVersion: s.UserOSVersion, + UserDevice: s.UserDevice, + UserCountry: s.UserCountry, + // web properties (TODO: unite different platform types) + UserAgent: s.UserAgent, + UserBrowser: s.UserBrowser, + UserBrowserVersion: s.UserBrowserVersion, + UserDeviceType: s.UserDeviceType, + UserDeviceMemorySize: s.UserDeviceMemorySize, + UserDeviceHeapSize: s.UserDeviceHeapSize, + UserID: &s.UserID, + }) +} + +func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error { if c.sessions[sessionID] != nil { return errors.New("This session already in cache!") } @@ -31,7 +55,7 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error UserDeviceHeapSize: s.UserDeviceHeapSize, UserID: &s.UserID, } - if err := c.Conn.InsertSessionStart(sessionID, c.sessions[sessionID]); err != nil { + if err := c.Conn.HandleSessionStart(sessionID, c.sessions[sessionID]); err != nil { c.sessions[sessionID] = nil return err } @@ -39,7 +63,11 @@ func (c *PGCache) InsertWebSessionStart(sessionID uint64, s *SessionStart) error } func (c *PGCache) InsertWebSessionEnd(sessionID uint64, e *SessionEnd) error { - return c.insertSessionEnd(sessionID, e.Timestamp) + return c.InsertSessionEnd(sessionID, e.Timestamp) +} + +func (c *PGCache) HandleWebSessionEnd(sessionID uint64, e *SessionEnd) error { + return c.HandleSessionEnd(sessionID) } func (c *PGCache) InsertWebErrorEvent(sessionID uint64, e *ErrorEvent) error { diff --git a/backend/pkg/db/postgres/messages-common.go b/backend/pkg/db/postgres/messages-common.go index 0b3039a45..b92e4fd6f 100644 --- a/backend/pkg/db/postgres/messages-common.go +++ b/backend/pkg/db/postgres/messages-common.go @@ -38,7 +38,7 @@ func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value str } func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { - if err := conn.exec(` + return conn.exec(` INSERT INTO sessions ( session_id, project_id, start_ts, user_uuid, user_device, user_device_type, user_country, @@ -66,9 +66,10 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { s.Platform, s.UserAgent, s.UserBrowser, s.UserBrowserVersion, s.UserDeviceMemorySize, s.UserDeviceHeapSize, s.UserID, - ); err != nil { - return err - } + ) +} + +func (conn *Conn) HandleSessionStart(sessionID uint64, s *types.Session) error { conn.insertAutocompleteValue(sessionID, getAutocompleteType("USEROS", s.Platform), s.UserOS) conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice) conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry) @@ -79,6 +80,20 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { } func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) { + var dur uint64 + if err := conn.queryRow(` + UPDATE sessions SET duration=$2 - start_ts + WHERE session_id=$1 + RETURNING duration + `, + sessionID, timestamp, + ).Scan(&dur); err != nil { + return 0, err + } + return dur, nil +} + +func (conn *Conn) HandleSessionEnd(sessionID uint64, timestamp uint64) error { // TODO: search acceleration? sqlRequest := ` UPDATE sessions @@ -96,18 +111,7 @@ func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, // Record approximate message size conn.updateBatchSize(sessionID, len(sqlRequest)+8) - - var dur uint64 - if err := conn.queryRow(` - UPDATE sessions SET duration=$2 - start_ts - WHERE session_id=$1 - RETURNING duration - `, - sessionID, timestamp, - ).Scan(&dur); err != nil { - return 0, err - } - return dur, nil + return nil } func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64, url string, duration uint64, success bool) error { From 1898f18d6b172f1b672d020e9e113438f4a80ed8 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Mon, 20 Jun 2022 09:44:12 +0200 Subject: [PATCH 012/144] fix(helm): efs clean cron path Signed-off-by: rjshrjndrn --- .../openreplay/charts/utilities/templates/efs-cron.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml b/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml index 5fbd041df..2407d4839 100644 --- a/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml +++ b/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml @@ -19,7 +19,7 @@ spec: args: - | # Delete all the files older than 7 days - find efs -type f -mtime +7 -delete + find /mnt/efs -type f -mtime +7 -delete volumeMounts: - mountPath: /mnt/efs name: datadir From 3ca389ff3c9986eea7e0c1aab147d70090c08c52 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Mon, 20 Jun 2022 10:11:36 +0200 Subject: [PATCH 013/144] fix(helm): nginx lb algorithm Signed-off-by: rjshrjndrn --- scripts/helmcharts/vars.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helmcharts/vars.yaml b/scripts/helmcharts/vars.yaml index 5517d0375..b03f6e444 100644 --- a/scripts/helmcharts/vars.yaml +++ b/scripts/helmcharts/vars.yaml @@ -60,7 +60,7 @@ ingress-nginx: &ingress-nginx default-ssl-certificate: "app/openreplay-ssl" config: use-gzip: true - load-balace: ewma + load-balance: ewma enable-real-ip: true # Enable LB forwarded protocol # Ref: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#use-forwarded-headers From 1a66daa3a2ff06ad549840165d60b2fd7bc82e9f Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Mon, 20 Jun 2022 10:29:05 +0200 Subject: [PATCH 014/144] chore(helm): details of cleaning. Signed-off-by: rjshrjndrn --- .../openreplay/charts/utilities/templates/efs-cron.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml b/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml index 2407d4839..7a9a383b4 100644 --- a/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml +++ b/scripts/helmcharts/openreplay/charts/utilities/templates/efs-cron.yaml @@ -19,7 +19,13 @@ spec: args: - | # Delete all the files older than 7 days + echo "Cleaning NFS strorage for data older than 7 days" + stroage=`du -sh /mnt/efs` find /mnt/efs -type f -mtime +7 -delete + echo "Storage before cleaning" + echo ${storage} + echo "Storage after cleaning" + du -sh /mnt/efs volumeMounts: - mountPath: /mnt/efs name: datadir From 94fc4d693e3a3f0c243c912119faed416aaedd47 Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Mon, 20 Jun 2022 11:40:07 +0200 Subject: [PATCH 015/144] fix(actions): image override Signed-off-by: rjshrjndrn --- .github/workflows/workers-ee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/workers-ee.yaml b/.github/workflows/workers-ee.yaml index 3d063c5b5..e42089602 100644 --- a/.github/workflows/workers-ee.yaml +++ b/.github/workflows/workers-ee.yaml @@ -119,7 +119,7 @@ jobs: ## Update images for image in $(cat /tmp/images_to_build.txt); do - sed -i "/${image}/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml + sed -i "/${image}/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml done cat /tmp/image_override.yaml From fb164af465d81593668d638f1f89a7c4f22bf40b Mon Sep 17 00:00:00 2001 From: rjshrjndrn Date: Mon, 20 Jun 2022 11:58:06 +0200 Subject: [PATCH 016/144] chore(helm): Adding postgres string in ender Signed-off-by: rjshrjndrn --- .../openreplay/charts/ender/templates/deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/helmcharts/openreplay/charts/ender/templates/deployment.yaml b/scripts/helmcharts/openreplay/charts/ender/templates/deployment.yaml index 7d307ca4b..739a2b36f 100644 --- a/scripts/helmcharts/openreplay/charts/ender/templates/deployment.yaml +++ b/scripts/helmcharts/openreplay/charts/ender/templates/deployment.yaml @@ -49,6 +49,8 @@ spec: value: '{{ .Values.global.kafka.kafkaHost }}:{{ .Values.global.kafka.kafkaPort }}' - name: KAFKA_USE_SSL value: '{{ .Values.global.kafka.kafkaUseSsl }}' + - name: POSTGRES_STRING + value: 'postgres://{{ .Values.global.postgresql.postgresqlUser }}:{{ .Values.global.postgresql.postgresqlPassword }}@{{ .Values.global.postgresql.postgresqlHost }}:{{ .Values.global.postgresql.postgresqlPort }}/{{ .Values.global.postgresql.postgresqlDatabase }}' {{- range $key, $val := .Values.env }} - name: {{ $key }} value: '{{ $val }}' From 1bcb0dfc0198b903805d14c1e1483718e0e2ff71 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 20 Jun 2022 12:02:28 +0200 Subject: [PATCH 017/144] feat(ui) - funnels more steps --- .../Errors/ErrorListItem/ErrorListItem.tsx | 21 +++-- .../Funnels/FunnelIssues/FunnelIssues.tsx | 20 +---- .../FunnelIssuesDropdown.tsx | 35 ++++++-- .../FunnelIssuesList/FunnelIssuesList.tsx | 19 ++++- .../components/WidgetChart/WidgetChart.tsx | 5 +- .../components/WidgetForm/WidgetForm.tsx | 2 +- .../WidgetPreview/WidgetPreview.tsx | 15 +++- .../WidgetSessions/WidgetSessions.tsx | 62 +++++++------- .../components/WidgetView/WidgetView.tsx | 4 +- .../Funnels/FunnelWidget/FunnelWidget.tsx | 83 +++++++++++++++---- frontend/app/mstore/dashboardStore.ts | 15 +++- frontend/app/mstore/metricStore.ts | 15 ++-- frontend/app/mstore/types/widget.ts | 19 ++++- frontend/app/types/app/period.js | 8 ++ 14 files changed, 212 insertions(+), 111 deletions(-) diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx index 95ce3f021..8ce3e8fad 100644 --- a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx +++ b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/ErrorListItem.tsx @@ -18,10 +18,10 @@ function ErrorListItem(props: Props) { const { error, className = '' } = props; return (
-
+
- - - - } /> - - +
+ + + + } /> + + +
); diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx index e053f2562..bbc2aab7e 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx @@ -1,24 +1,19 @@ import React, { useEffect, useState } from 'react'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; -import { NoContent, Loader } from 'UI'; +import { Loader } from 'UI'; import FunnelIssuesDropdown from '../FunnelIssuesDropdown'; import FunnelIssuesSort from '../FunnelIssuesSort'; import FunnelIssuesList from '../FunnelIssuesList'; import { DateTime } from 'luxon'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted'; -import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; function FunnelIssues() { const { metricStore, dashboardStore } = useStore(); const [data, setData] = useState({ issues: [] }); const [loading, setLoading] = useState(false); const isMounted = useIsMounted() - // const funnel = useObserver(() => funnelStore.instance); - // const funnel = useObserver(() => metricStore.instance); - // const issues = useObserver(() => funnelStore.issues); - // const loading = useObserver(() => funnelStore.isLoadingIssues); const fetchIssues = (filter: any) => { if (!isMounted()) return; @@ -35,7 +30,6 @@ function FunnelIssues() { const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a'); const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a'); const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []); - const depsString = JSON.stringify(widget.series); useEffect(() => { @@ -54,17 +48,7 @@ function FunnelIssues() {
- - -
No issues found
-
- } - > - - +

)); diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx index 547e896ed..361337443 100644 --- a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx @@ -1,9 +1,10 @@ -import React, { Component, ReactNode, FunctionComponent, useEffect } from 'react'; +import React, { useEffect } from 'react'; import Select from 'Shared/Select' import { components } from 'react-select'; import { Icon } from 'UI'; import FunnelIssuesSelectedFilters from '../FunnelIssuesSelectedFilters'; import { useStore } from 'App/mstore'; +import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; const options = [ { value: "click_rage", label: "Click Rage" }, @@ -20,7 +21,7 @@ const options = [ { value: "js_error", label: "Error" } ] -function FunnelIssuesDropdown(props) { +function FunnelIssuesDropdown() { const { funnelStore } = useStore(); const [isOpen, setIsOpen] = React.useState(false); const [selectedValues, setSelectedValues] = React.useState([]); @@ -48,12 +49,20 @@ function FunnelIssuesDropdown(props) { } } + const onClickOutside = () => { + if (isOpen) { + setTimeout(() => { + setIsOpen(false); + }, 0); + } + } + return (
dashboardStore.period); const metric: any = useObserver(() => metricStore.instance); const isTimeSeries = metric.metricType === 'timeseries'; const isTable = metric.metricType === 'table'; + // const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter); const disableVisualization = useObserver(() => metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS); + const period = useObserver(() => dashboardStore.drillDownPeriod); const chagneViewType = (e, { name, value }: any) => { metric.update({ [ name ]: value }); } + const onChangePeriod = (period: any) => { + dashboardStore.setDrillDownPeriod(period); + } + return useObserver(() => (
@@ -39,8 +45,8 @@ function WidgetPreview(props: Props) { onSelect={ chagneViewType } value={{ value: metric.viewType }} list={ [ - { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' }, - { value: 'progress', name: 'Progress', icon: 'hash' }, + { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' }, + { value: 'progress', name: 'Progress', icon: 'hash' }, ]} /> @@ -69,7 +75,8 @@ function WidgetPreview(props: Props) { Time Range dashboardStore.setPeriod(period)} + // onChange={(period: any) => metric.setPeriod(period)} + onChange={onChangePeriod} right={true} />
diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx index 09adadc36..411bdab4a 100644 --- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { NoContent, Dropdown, Icon, Loader, Pagination } from 'UI'; +import { NoContent, Loader, Pagination } from 'UI'; import Select from 'Shared/Select'; import cn from 'classnames'; import { useStore } from 'App/mstore'; @@ -15,19 +15,29 @@ interface Props { } function WidgetSessions(props: Props) { const { className = '' } = props; + const [activeSeries, setActiveSeries] = useState('all'); const [data, setData] = useState([]); - const isMounted = useIsMounted() + const isMounted = useIsMounted(); const [loading, setLoading] = useState(false); + const filteredSessions = getListSessionsBySeries(data, activeSeries); + const { dashboardStore, metricStore } = useStore(); + const filter = useObserver(() => dashboardStore.drillDownFilter); + const widget: any = useObserver(() => metricStore.instance); + const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod); + const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a'); + const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a'); + const [timestamps, setTimestamps] = useState({ + startTimestamp: 0, + endTimestamp: 0, + }); const [seriesOptions, setSeriesOptions] = useState([ { label: 'All', value: 'all' }, ]); - const [activeSeries, setActiveSeries] = useState('all'); - - const writeOption = (e, { name, value }) => setActiveSeries(value.value); + const writeOption = ({ value }: any) => setActiveSeries(value.value); useEffect(() => { if (!data) return; - const seriesOptions = data.map(item => ({ + const seriesOptions = data.map((item: any) => ({ label: item.seriesName, value: item.seriesId, })); @@ -37,22 +47,15 @@ function WidgetSessions(props: Props) { ]); }, [data]); - const fetchSessions = (metricId, filter) => { + const fetchSessions = (metricId: any, filter: any) => { if (!isMounted()) return; setLoading(true) - widget.fetchSessions(metricId, filter).then(res => { + widget.fetchSessions(metricId, filter).then((res: any) => { setData(res) }).finally(() => { setLoading(false) }); } - - const filteredSessions = getListSessionsBySeries(data, activeSeries); - const { dashboardStore, metricStore } = useStore(); - const filter = useObserver(() => dashboardStore.drillDownFilter); - const widget: any = useObserver(() => metricStore.instance); - const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a'); - const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a'); const debounceRequest: any = React.useCallback(debounce(fetchSessions, 1000), []); const depsString = JSON.stringify(widget.series); @@ -60,6 +63,12 @@ function WidgetSessions(props: Props) { debounceRequest(widget.metricId, { ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize }); }, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]); + useEffect(() => { + const timestamps = drillDownPeriod.toTimestamps(); + console.log('timestamps', timestamps); + debounceRequest(widget.metricId, { startTime: timestamps.startTimestamp, endTime: timestamps.endTimestamp, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize }); + }, [drillDownPeriod]); + return useObserver(() => (
@@ -71,18 +80,6 @@ function WidgetSessions(props: Props) { { widget.metricType !== 'table' && (
Series - {/* } - /> */} onClick(category)} >
{category.name}
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index e16b19a1d..a8c1d96c4 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -18,7 +18,7 @@ function MetricsView(props: Props) { metricStore.fetchList(); }, []); return useObserver(() => ( -
+
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index cc7574f63..af32c1318 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -101,7 +101,7 @@ function WidgetChart(props: Props) { } if (metricType === 'funnel') { - return + return } if (metricType === 'predefined') { diff --git a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx index b52a91939..137891866 100644 --- a/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx +++ b/frontend/app/components/shared/Filters/FilterOperator/FilterOperator.tsx @@ -21,7 +21,9 @@ const dropdownStyles = { ...provided, paddingRight: '0px', width: 'fit-content', - // height: '26px' + '& input': { + marginTop: '-3px', + }, }), placeholder: (provided: any) => ({ ...provided, @@ -40,6 +42,7 @@ const dropdownStyles = { top: 20, left: 0, minWidth: 'fit-content', + overflow: 'hidden', }), container: (provided: any) => ({ ...provided, @@ -49,7 +52,12 @@ const dropdownStyles = { const opacity = state.isDisabled ? 0.5 : 1; const transition = 'opacity 300ms'; - return { ...provided, opacity, transition }; + return { + ...provided, + opacity, + transition, + marginTop: '-3px', + }; } } interface Props { diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index df36a1046..7087926ff 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -88,7 +88,7 @@ function FilterValue(props: Props) { case FilterType.DROPDOWN: return ( onChange(null, { value }, valueIndex)} onAddValue={onAddValue} diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx index 6123f931d..13c40cbd8 100644 --- a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx @@ -43,6 +43,7 @@ const dropdownStyles = { top: 20, left: 0, minWidth: 'fit-content', + overflow: 'hidden', }), container: (provided: any) => ({ ...provided, @@ -50,9 +51,10 @@ const dropdownStyles = { }), input: (provided: any) => ({ ...provided, - // padding: '0px', - // margin: '0px', height: '22px', + '& input:focus': { + border: 'none !important', + } }), singleValue: (provided: any, state: { isDisabled: any; }) => { const opacity = state.isDisabled ? 0.5 : 1; @@ -67,14 +69,14 @@ const dropdownStyles = { } } interface Props { - filter: any; // event/filter + // filter: any; // event/filter // options: any[]; value: string; onChange: (value: any) => void; className?: string; options: any[]; search?: boolean; - multiple?: boolean; + // multiple?: boolean; showCloseButton?: boolean; showOrButton?: boolean; onRemoveValue?: () => void; @@ -82,7 +84,7 @@ interface Props { isMultilple?: boolean; } function FilterValueDropdown(props: Props) { - const { filter, multiple = false, isMultilple = true, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props; + const { isMultilple = true, search = false, options, onChange, value, className = '', showCloseButton = true, showOrButton = true } = props; // const options = [] return ( diff --git a/frontend/app/components/ui/Input/Input.tsx b/frontend/app/components/ui/Input/Input.tsx index 502142508..7851fec7f 100644 --- a/frontend/app/components/ui/Input/Input.tsx +++ b/frontend/app/components/ui/Input/Input.tsx @@ -7,14 +7,21 @@ interface Props { className: string; icon?: string; leadingButton?: React.ReactNode; + type?: string; + rows?: number; [x:string]: any; } function Input(props: Props) { - const { className, leadingButton = "", wrapperClassName = "", icon = "", ...rest } = props; + const { className, leadingButton = "", wrapperClassName = "", icon = "", type="text", rows=4, ...rest } = props; return (
{icon && } - + { type === 'textarea' ? ( +