From f360ce3d2c350ed4b0235e4fe3715db2dc6d734b Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Thu, 16 Mar 2023 17:11:13 +0100 Subject: [PATCH] change(ui): add visual display for frustrations --- .../components/Session_/EventsBlock/Event.js | 47 +++++++++++----- .../Session_/EventsBlock/event.module.css | 7 +++ .../Session_/OverviewPanel/OverviewPanel.tsx | 4 +- .../FeatureSelection/FeatureSelection.tsx | 8 +-- .../TimelinePointer/TimelinePointer.tsx | 20 ++++--- frontend/app/components/ui/SVG.tsx | 9 +++- .../app/components/ui/Tooltip/Tooltip.tsx | 3 +- frontend/app/player/web/Lists.ts | 2 +- frontend/app/player/web/WebPlayer.ts | 1 + frontend/app/svg/icons/click-hesitation.svg | 15 ++++++ frontend/app/svg/icons/click-rage.svg | 9 ++++ frontend/app/svg/icons/cursor-trash.svg | 15 ++++++ .../app/svg/icons/event/click_hesitation.svg | 15 ++++++ .../app/svg/icons/event/input_hesitation.svg | 17 ++++++ .../app/svg/icons/event/mouse_thrashing.svg | 15 ++++++ frontend/app/svg/icons/input-hesitation.svg | 17 ++++++ frontend/app/types/session/event.ts | 20 ++++--- frontend/app/types/session/issue.ts | 2 +- frontend/app/types/session/session.ts | 53 +++++++++++++++++-- frontend/scripts/icons.ts | 13 ++--- 20 files changed, 245 insertions(+), 47 deletions(-) create mode 100644 frontend/app/svg/icons/click-hesitation.svg create mode 100644 frontend/app/svg/icons/click-rage.svg create mode 100644 frontend/app/svg/icons/cursor-trash.svg create mode 100644 frontend/app/svg/icons/event/click_hesitation.svg create mode 100644 frontend/app/svg/icons/event/input_hesitation.svg create mode 100644 frontend/app/svg/icons/event/mouse_thrashing.svg create mode 100644 frontend/app/svg/icons/input-hesitation.svg diff --git a/frontend/app/components/Session_/EventsBlock/Event.js b/frontend/app/components/Session_/EventsBlock/Event.js index e8f985aa0..a464fcc28 100644 --- a/frontend/app/components/Session_/EventsBlock/Event.js +++ b/frontend/app/components/Session_/EventsBlock/Event.js @@ -1,7 +1,7 @@ import React from 'react'; import copy from 'copy-to-clipboard'; import cn from 'classnames'; -import { Icon, TextEllipsis } from 'UI'; +import { Icon, TextEllipsis, Tooltip } from 'UI'; import { TYPES } from 'Types/session/event'; import { prorata } from 'App/utils'; import withOverlay from 'Components/hocs/withOverlay'; @@ -9,6 +9,16 @@ import LoadInfo from './LoadInfo'; import cls from './event.module.css'; import { numberWithCommas } from 'App/utils'; +function isFrustrationEvent(evt) { + if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE) { + return true; + } + if (evt.type === TYPES.CLICK || evt.type === TYPES.INPUT) { + return evt.hesitation > 1000 + } + return false +} + @withOverlay() export default class Event extends React.PureComponent { state = { @@ -44,35 +54,50 @@ export default class Event extends React.PureComponent { const { event } = this.props; let title = event.type; let body; + let icon; + const isFrustration = isFrustrationEvent(event); + const tooltip = { disabled: true, text: '' } + switch (event.type) { case TYPES.LOCATION: title = 'Visited'; body = event.url; + icon = 'location'; break; case TYPES.CLICK: title = 'Clicked'; body = event.label; + icon = isFrustration ? 'click_hesitation' : 'click'; + isFrustration ? Object.assign(tooltip, { disabled: false, text: `User hesitated to click for ${Math.round(event.hesitation/1000)}s`, }) : null; break; case TYPES.INPUT: title = 'Input'; body = event.value; + icon = isFrustration ? 'input_hesitation' : 'input'; + isFrustration ? Object.assign(tooltip, { disabled: false, text: `User hesitated to enter a value for ${Math.round(event.hesitation/1000)}s`, }) : null; break; case TYPES.CLICKRAGE: title = `${ event.count } Clicks`; body = event.label; + icon = 'clickrage' break; case TYPES.IOS_VIEW: title = 'View'; body = event.name; + icon = 'ios_view' + break; + case 'mouse_thrashing': + title = 'Mouse Thrashing'; + icon = 'mouse_thrashing' break; } const isLocation = event.type === TYPES.LOCATION; - const isClickrage = event.type === TYPES.CLICKRAGE; return ( +
- { event.type && } + { event.type && }
@@ -100,6 +125,7 @@ export default class Event extends React.PureComponent {
}
+ ); }; @@ -110,17 +136,15 @@ export default class Event extends React.PureComponent { isCurrent, onClick, showSelection, - onCheckboxClick, showLoadInfo, toggleLoadInfo, isRed, - extended, - highlight = false, presentInSearch = false, - isLastInGroup, whiteBg, } = this.props; const { menuOpen } = this.state; + + const isFrustration = isFrustrationEvent(event); return (
{ this.wrapper = ref } } @@ -135,7 +159,7 @@ export default class Event extends React.PureComponent { [ cls.red ]: isRed, [ cls.clickType ]: event.type === TYPES.CLICK, [ cls.inputType ]: event.type === TYPES.INPUT, - [ cls.clickrageType ]: event.type === TYPES.CLICKRAGE, + [ cls.frustration ]: isFrustration, [ cls.highlight ] : presentInSearch, [ cls.lastInGroup ]: whiteBg, }) } @@ -146,13 +170,10 @@ export default class Event extends React.PureComponent { { event.target ? 'Copy CSS' : 'Copy URL' } } -
-
+
+
{ this.renderBody() }
- {/* { event.type === TYPES.LOCATION && -
{event.url}
- } */}
{ event.type === TYPES.LOCATION && (event.fcpTime || event.visuallyComplete || event.timeToInteractive) && [] }) { performanceChartData, stackList: stackEventList, eventList: eventsList, + frustrationsList, exceptionsList, resourceList: resourceListUnmap, fetchList, @@ -46,8 +46,8 @@ function OverviewPanel({ issuesList }: { issuesList: Record[] }) { NETWORK: resourceList, ERRORS: exceptionsList, EVENTS: stackEventList, - CLICKRAGE: eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE), PERFORMANCE: performanceChartData, + FRUSTRATIONS: frustrationsList, }; }, [dataLoaded]); diff --git a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx index 8d76a3070..3a841d97c 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx @@ -4,15 +4,15 @@ import { Checkbox, Tooltip } from 'UI'; const NETWORK = 'NETWORK'; const ERRORS = 'ERRORS'; const EVENTS = 'EVENTS'; -const CLICKRAGE = 'CLICKRAGE'; +const FRUSTRATIONS = 'FRUSTRATIONS'; const PERFORMANCE = 'PERFORMANCE'; export const HELP_MESSAGE: any = { NETWORK: 'Network requests made in this session', EVENTS: 'Visualizes the events that takes place in the DOM', ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.', - CLICKRAGE: 'Indicates user frustration when repeated clicks are recorded', PERFORMANCE: 'Summary of this session’s memory, and CPU consumption on the timeline', + FRUSTRATIONS: 'Indicates user frustrations in the session', }; interface Props { @@ -21,7 +21,7 @@ interface Props { } function FeatureSelection(props: Props) { const { list } = props; - const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE]; + const features = [NETWORK, ERRORS, EVENTS, PERFORMANCE, FRUSTRATIONS]; const disabled = list.length >= 5; return ( @@ -30,7 +30,7 @@ function FeatureSelection(props: Props) { const checked = list.includes(feature); const _disabled = disabled && !checked; return ( - + { ); }; - const renderClickRageElement = (item: any) => { + const renderFrustrationElement = (item: any) => { + const elData = { name: '', icon: ''} + if (item.type === TYPES.CLICK) Object.assign(elData, { name: `User hesitated to click for ${Math.round(item.hesitation/1000)}s`, icon: 'click-hesitation' }) + if (item.type === TYPES.INPUT) Object.assign(elData, { name: `User hesitated to enter a value for ${Math.round(item.hesitation/1000)}s`, icon: 'input-hesitation' }) + if (item.type === TYPES.CLICKRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' }) + if (item.type === issueTypes.MOUSE_THRASHING) Object.assign(elData, { name: 'Mouse Thrashing', icon: 'cursor-trash' }) + return ( - {'Click Rage'} + {elData.name}
} delay={0} placement="top" >
- +
); @@ -158,8 +166,8 @@ const TimelinePointer = React.memo((props: Props) => { if (type === 'NETWORK') { return renderNetworkElement(pointer); } - if (type === 'CLICKRAGE') { - return renderClickRageElement(pointer); + if (type === 'FRUSTRATIONS') { + return renderFrustrationElement(pointer); } if (type === 'ERRORS') { return renderExceptionElement(pointer); diff --git a/frontend/app/components/ui/SVG.tsx b/frontend/app/components/ui/SVG.tsx index 66af7be76..eeaeae5dd 100644 --- a/frontend/app/components/ui/SVG.tsx +++ b/frontend/app/components/ui/SVG.tsx @@ -1,7 +1,7 @@ import React from 'react'; -export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_bear' | 'avatar/icn_beaver' | 'avatar/icn_bird' | 'avatar/icn_bison' | 'avatar/icn_camel' | 'avatar/icn_chameleon' | 'avatar/icn_deer' | 'avatar/icn_dog' | 'avatar/icn_dolphin' | 'avatar/icn_elephant' | 'avatar/icn_fish' | 'avatar/icn_fox' | 'avatar/icn_gorilla' | 'avatar/icn_hippo' | 'avatar/icn_horse' | 'avatar/icn_hyena' | 'avatar/icn_kangaroo' | 'avatar/icn_lemur' | 'avatar/icn_mammel' | 'avatar/icn_monkey' | 'avatar/icn_moose' | 'avatar/icn_panda' | 'avatar/icn_penguin' | 'avatar/icn_porcupine' | 'avatar/icn_quail' | 'avatar/icn_rabbit' | 'avatar/icn_rhino' | 'avatar/icn_sea_horse' | 'avatar/icn_sheep' | 'avatar/icn_snake' | 'avatar/icn_squirrel' | 'avatar/icn_tapir' | 'avatar/icn_turtle' | 'avatar/icn_vulture' | 'avatar/icn_wild1' | 'avatar/icn_wild_bore' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'dash' | 'dashboard-icn' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope' | 'errors-icon' | 'event/click' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/link' | 'event/location' | 'event/resize' | 'event/view' | 'exclamation-circle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'journal-code' | 'layer-group' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'percent' | 'performance-icon' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-circle' | 'redo-back' | 'redo' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'text-paragraph' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; +export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_bear' | 'avatar/icn_beaver' | 'avatar/icn_bird' | 'avatar/icn_bison' | 'avatar/icn_camel' | 'avatar/icn_chameleon' | 'avatar/icn_deer' | 'avatar/icn_dog' | 'avatar/icn_dolphin' | 'avatar/icn_elephant' | 'avatar/icn_fish' | 'avatar/icn_fox' | 'avatar/icn_gorilla' | 'avatar/icn_hippo' | 'avatar/icn_horse' | 'avatar/icn_hyena' | 'avatar/icn_kangaroo' | 'avatar/icn_lemur' | 'avatar/icn_mammel' | 'avatar/icn_monkey' | 'avatar/icn_moose' | 'avatar/icn_panda' | 'avatar/icn_penguin' | 'avatar/icn_porcupine' | 'avatar/icn_quail' | 'avatar/icn_rabbit' | 'avatar/icn_rhino' | 'avatar/icn_sea_horse' | 'avatar/icn_sheep' | 'avatar/icn_snake' | 'avatar/icn_squirrel' | 'avatar/icn_tapir' | 'avatar/icn_turtle' | 'avatar/icn_vulture' | 'avatar/icn_wild1' | 'avatar/icn_wild_bore' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'journal-code' | 'layer-group' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'percent' | 'performance-icon' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-circle' | 'redo-back' | 'redo' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'text-paragraph' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; interface Props { name: IconNames; @@ -119,6 +119,8 @@ const SVG = (props: Props) => { case 'chevron-up': return ; case 'circle-fill': return ; case 'circle': return ; + case 'click-hesitation': return ; + case 'click-rage': return ; case 'clipboard-list-check': return ; case 'clock': return ; case 'close': return ; @@ -140,6 +142,7 @@ const SVG = (props: Props) => { case 'credit-card-front': return ; case 'cross': return ; case 'cubes': return ; + case 'cursor-trash': return ; case 'dash': return ; case 'dashboard-icn': return ; case 'desktop': return ; @@ -156,12 +159,15 @@ const SVG = (props: Props) => { case 'envelope': return ; case 'errors-icon': return ; case 'event/click': return ; + case 'event/click_hesitation': return ; case 'event/clickrage': return ; case 'event/code': return ; case 'event/i-cursor': return ; case 'event/input': return ; + case 'event/input_hesitation': return ; case 'event/link': return ; case 'event/location': return ; + case 'event/mouse_thrashing': return ; case 'event/resize': return ; case 'event/view': return ; case 'exclamation-circle': return ; @@ -271,6 +277,7 @@ const SVG = (props: Props) => { case 'info-circle': return ; case 'info-square': return ; case 'info': return ; + case 'input-hesitation': return ; case 'inspect': return ; case 'integrations/assist': return ; case 'integrations/bugsnag-text': return ; diff --git a/frontend/app/components/ui/Tooltip/Tooltip.tsx b/frontend/app/components/ui/Tooltip/Tooltip.tsx index fcb5e1687..ee68b71e7 100644 --- a/frontend/app/components/ui/Tooltip/Tooltip.tsx +++ b/frontend/app/components/ui/Tooltip/Tooltip.tsx @@ -23,6 +23,7 @@ function Tooltip(props: Props) { placement, className = '', anchorClassName = '', + containerClassName = '', delay = 500, style = {}, offset = 5, @@ -39,7 +40,7 @@ function Tooltip(props: Props) { }); return ( -
+
{props.children} Log({ level: LogLevel.ERROR, diff --git a/frontend/app/svg/icons/click-hesitation.svg b/frontend/app/svg/icons/click-hesitation.svg new file mode 100644 index 000000000..144b82cd5 --- /dev/null +++ b/frontend/app/svg/icons/click-hesitation.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/app/svg/icons/click-rage.svg b/frontend/app/svg/icons/click-rage.svg new file mode 100644 index 000000000..54ccb06a6 --- /dev/null +++ b/frontend/app/svg/icons/click-rage.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/app/svg/icons/cursor-trash.svg b/frontend/app/svg/icons/cursor-trash.svg new file mode 100644 index 000000000..bdf687c91 --- /dev/null +++ b/frontend/app/svg/icons/cursor-trash.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/app/svg/icons/event/click_hesitation.svg b/frontend/app/svg/icons/event/click_hesitation.svg new file mode 100644 index 000000000..144b82cd5 --- /dev/null +++ b/frontend/app/svg/icons/event/click_hesitation.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/app/svg/icons/event/input_hesitation.svg b/frontend/app/svg/icons/event/input_hesitation.svg new file mode 100644 index 000000000..a2f79bfb6 --- /dev/null +++ b/frontend/app/svg/icons/event/input_hesitation.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/app/svg/icons/event/mouse_thrashing.svg b/frontend/app/svg/icons/event/mouse_thrashing.svg new file mode 100644 index 000000000..af00c02cf --- /dev/null +++ b/frontend/app/svg/icons/event/mouse_thrashing.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/app/svg/icons/input-hesitation.svg b/frontend/app/svg/icons/input-hesitation.svg new file mode 100644 index 000000000..439606a52 --- /dev/null +++ b/frontend/app/svg/icons/input-hesitation.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts index 99693d756..53924ef0c 100644 --- a/frontend/app/types/session/event.ts +++ b/frontend/app/types/session/event.ts @@ -7,16 +7,18 @@ const CLICKRAGE = 'CLICKRAGE'; const IOS_VIEW = 'VIEW'; export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW }; +export type EventType = + | typeof CONSOLE + | typeof CLICK + | typeof INPUT + | typeof LOCATION + | typeof CUSTOM + | typeof CLICKRAGE; + interface IEvent { time: number; timestamp: number; - type: - | typeof CONSOLE - | typeof CLICK - | typeof INPUT - | typeof LOCATION - | typeof CUSTOM - | typeof CLICKRAGE; + type: EventType name: string; key: number; label: string; @@ -35,6 +37,7 @@ interface ConsoleEvent extends IEvent { interface ClickEvent extends IEvent { targetContent: string; count: number; + hesitation: number; } interface InputEvent extends IEvent { @@ -98,12 +101,13 @@ export class Click extends Event { readonly name = 'Click'; targetContent = ''; count: number; + hesitation: number = 0; constructor(evt: ClickEvent, isClickRage?: boolean) { - console.log(evt); super(evt); this.targetContent = evt.targetContent; this.count = evt.count; + this.hesitation = evt.hesitation; if (isClickRage) { this.type = CLICKRAGE; } diff --git a/frontend/app/types/session/issue.ts b/frontend/app/types/session/issue.ts index 2abedc5ea..8f5f415e3 100644 --- a/frontend/app/types/session/issue.ts +++ b/frontend/app/types/session/issue.ts @@ -1,6 +1,6 @@ import Record from 'Types/Record'; -const types = { +export const types = { ALL: 'all', JS_EXCEPTION: 'js_exception', BAD_REQUEST: 'bad_request', diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index 3b254ae4b..a473ed623 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -2,12 +2,37 @@ import { Duration } from 'luxon'; import SessionEvent, { TYPES, EventData, InjectedEvent } from './event'; import StackEvent from './stackEvent'; import SessionError, { IError } from './error'; -import Issue, { IIssue } from './issue'; +import Issue, { IIssue, types as issueTypes } from './issue'; import { Note } from 'App/services/NotesService' const HASH_MOD = 1610612741; const HASH_P = 53; +function mergeEventLists(arr1: any[], arr2: any[]) { + let merged = []; + let index1 = 0; + let index2 = 0; + let current = 0; + + while (current < (arr1.length + arr2.length)) { + + let isArr1Depleted = index1 >= arr1.length; + let isArr2Depleted = index2 >= arr2.length; + + if (!isArr1Depleted && (isArr2Depleted || (arr1[index1].timestamp < arr2[index2].timestamp))) { + merged[current] = arr1[index1]; + index1++; + } else { + merged[current] = arr2[index2]; + index2++; + } + + current++; + } + + return merged; +} + function hashString(s: string): number { let mul = 1; let hash = 0; @@ -158,6 +183,8 @@ export default class Session { agentToken: ISession["agentToken"] notes: ISession["notes"] notesWithEvents: ISession["notesWithEvents"] + frustrations: Array + fileKey: ISession["fileKey"] constructor(plainSession?: ISession) { @@ -217,8 +244,20 @@ export default class Session { (i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })) || []; const rawNotes = notes; - const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => { - // @ts-ignore just in case + + + const frustrationEvents = events.filter(ev => { + if (ev.type === TYPES.CLICK || ev.type === TYPES.INPUT) { + // @ts-ignore + return ev.hesitation > 1000 + } + return ev.type === TYPES.CLICKRAGE + } + ) + const frustrationIssues = issuesList.filter(i => i.type === issueTypes.MOUSE_THRASHING) + + const frustrationList = [...frustrationEvents, ...frustrationIssues].sort((a, b) => { + // @ts-ignore const aTs = a.timestamp || a.time; // @ts-ignore const bTs = b.timestamp || b.time; @@ -226,6 +265,11 @@ export default class Session { return aTs - bTs; }) || []; + const mixedEventsWithIssues = mergeEventLists( + mergeEventLists(rawEvents, rawNotes), + frustrationIssues + ) + Object.assign(this, { ...session, isIOS: session.platform === 'ios', @@ -255,7 +299,8 @@ export default class Session { domURL, devtoolsURL, notes, - notesWithEvents: notesWithEvents, + notesWithEvents: mixedEventsWithIssues, + frustrations: frustrationList, }) } } \ No newline at end of file diff --git a/frontend/scripts/icons.ts b/frontend/scripts/icons.ts index dab11b224..0d7f67a31 100644 --- a/frontend/scripts/icons.ts +++ b/frontend/scripts/icons.ts @@ -32,6 +32,8 @@ const plugins = (removeFill = true) => { name: 'preset-default', params: { overrides: { + cleanupIds: false, + prefixIds: false, inlineStyles: { onlyMatchedOnce: false, }, @@ -63,12 +65,10 @@ const plugins = (removeFill = true) => { ] } } -// fs.promises.mkdir('/tmp/a/apple', { recursive: true }) -// .then(() => { - fs.writeFileSync(`${UI_DIRNAME}/SVG.tsx`, ` +fs.writeFileSync(`${UI_DIRNAME}/SVG.tsx`, ` import React from 'react'; -export type IconNames = ${icons.map(icon => "'"+ icon.slice(0, -4) + "'").join(' | ')}; +export type IconNames = ${icons.map((icon) => "'"+ icon.slice(0, -4) + "'").join(' | ')}; interface Props { name: IconNames; @@ -88,10 +88,11 @@ ${icons.map(icon => { const { data } = optimize(svg, plugins(canOptimize)); return ` case '${icon.slice(0, -4)}': return ${data.replace(/xlink\:href/g, 'xlinkHref') .replace(/xmlns\:xlink/g, 'xmlnsXlink') - .replace(/clip-path/g, 'clipPath') - .replace(/clip-rule/g, 'clipRule') + .replace(/clip\-path/g, 'clipPath') + .replace(/clip\-rule/g, 'clipRule') // hack to keep fill rule for some icons like stop recording square .replace(/clipRule="evenoddCustomFill"/g, 'clipRule="evenodd" fillRule="evenodd"') + .replace(/fill-rule/g, 'fillRule') .replace(/fill-opacity/g, 'fillOpacity') .replace(/stop-color/g, 'stopColor')