feat: tag and watch (UI/Tracker) (#1822)

* feat(ui/tracker): start tag n watch

* fix(tracker): test coverage, fix some watcher api

* fix(tracker): add intersectionobserver, adjust tests

* feat(tracker): relay + apollo plugins

* feat(ui): tags search

* feat(ui): tags name edit

* feat(ui): tags search icon

* feat(ui): icons for tabs in player

* feat(ui): save and find button

* feat(tracker): save tags in session storage (just in case)

* feat(ui): improve loading

* feat(ui): fix icon names gen

* feat(ui): fix typo
This commit is contained in:
Delirium 2024-01-19 11:11:27 +01:00 committed by GitHub
parent 0c2dd6f9f1
commit 309a9fd970
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2087 additions and 1178 deletions

View file

@ -86,6 +86,7 @@ const (
MsgTabChange = 117
MsgTabData = 118
MsgCanvasNode = 119
MsgTagTrigger = 120
MsgIssueEvent = 125
MsgSessionEnd = 126
MsgSessionSearch = 127
@ -2301,6 +2302,27 @@ func (msg *CanvasNode) TypeID() int {
return 119
}
type TagTrigger struct {
message
TagId int64
}
func (msg *TagTrigger) Encode() []byte {
buf := make([]byte, 11)
buf[0] = 120
p := 1
p = WriteInt(msg.TagId, buf, p)
return buf[:p]
}
func (msg *TagTrigger) Decode() Message {
return msg
}
func (msg *TagTrigger) TypeID() int {
return 120
}
type IssueEvent struct {
message
MessageID uint64

View file

@ -1401,6 +1401,15 @@ func DecodeCanvasNode(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeTagTrigger(reader BytesReader) (Message, error) {
var err error = nil
msg := &TagTrigger{}
if msg.TagId, err = reader.ReadInt(); err != nil {
return nil, err
}
return msg, err
}
func DecodeIssueEvent(reader BytesReader) (Message, error) {
var err error = nil
msg := &IssueEvent{}
@ -2033,6 +2042,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeTabData(reader)
case 119:
return DecodeCanvasNode(reader)
case 120:
return DecodeTagTrigger(reader)
case 125:
return DecodeIssueEvent(reader)
case 126:

View file

@ -808,6 +808,13 @@ class CanvasNode(Message):
self.timestamp = timestamp
class TagTrigger(Message):
__id__ = 120
def __init__(self, tag_id):
self.tag_id = tag_id
class IssueEvent(Message):
__id__ = 125

View file

@ -1194,6 +1194,15 @@ cdef class CanvasNode(PyMessage):
self.timestamp = timestamp
cdef class TagTrigger(PyMessage):
cdef public int __id__
cdef public long tag_id
def __init__(self, long tag_id):
self.__id__ = 120
self.tag_id = tag_id
cdef class IssueEvent(PyMessage):
cdef public int __id__
cdef public unsigned long message_id

View file

@ -727,6 +727,11 @@ class MessageCodec(Codec):
timestamp=self.read_uint(reader)
)
if message_id == 120:
return TagTrigger(
tag_id=self.read_int(reader)
)
if message_id == 125:
return IssueEvent(
message_id=self.read_uint(reader),

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,8 @@ const siteIdRequiredPaths: string[] = [
'/notes',
'/feature-flags',
'/check-recording-status',
'/usability-tests'
'/usability-tests',
'/tags'
];
export const clean = (obj: any, forbiddenValues: any[] = [undefined, '']): any => {

View file

@ -3,7 +3,6 @@ import { Button, PageTitle } from 'UI'
import FFlagsSearch from "Components/FFlags/FFlagsSearch";
import { useHistory } from "react-router";
import { newFFlag, withSiteId } from 'App/routes';
import { observer } from 'mobx-react-lite';
function FFlagsListHeader({ siteId }: { siteId: string }) {
const history = useHistory();

View file

@ -24,7 +24,7 @@ function Player() {
>
<div className={cn("relative flex-1", 'overflow-visible')}>
<Overlay isClickmap />
<div className={cn(stl.screenWrapper, '!overflow-y-scroll')} style={{ maxHeight: 800 }} ref={screenWrapper} />
<div className={cn(stl.screenWrapper, stl.checkers, '!overflow-y-scroll')} style={{ maxHeight: 800 }} ref={screenWrapper} />
</div>
</div>
);

View file

@ -52,7 +52,7 @@ function Player(props: IProps) {
>
<div className="relative flex-1 overflow-hidden">
<Overlay closedLive={closedLive} />
<div className={cn(stl.screenWrapper)} ref={screenWrapper} />
<div className={cn(stl.screenWrapper, stl.checkers)} ref={screenWrapper} />
</div>
{bottomBlock === CONSOLE ? (
<div style={{ maxWidth, width: '100%' }}>

View file

@ -33,6 +33,7 @@ import ConsolePanel from 'Shared/DevTools/ConsolePanel';
import ProfilerPanel from 'Shared/DevTools/ProfilerPanel';
import { PlayerContext } from 'App/components/Session/playerContext';
import { debounce } from 'App/utils';
import { observer } from 'mobx-react-lite';
interface IProps {
fullView: boolean;
@ -109,6 +110,7 @@ function Player(props: IProps) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
const isInspMode = playerContext.store.get().inspectorMode;
return (
<div
@ -118,7 +120,7 @@ function Player(props: IProps) {
{fullscreen && <EscapeButton onClose={fullscreenOff} />}
<div className={cn('relative flex-1', 'overflow-hidden')}>
<Overlay nextId={nextId} />
<div className={cn(stl.screenWrapper)} ref={screenWrapper} />
<div className={cn(stl.screenWrapper, isInspMode ? stl.solidBg : stl.checkers)} ref={screenWrapper} />
</div>
{!fullscreen && !!bottomBlock && (
<div
@ -143,7 +145,6 @@ function Player(props: IProps) {
{bottomBlock === PERFORMANCE && <ConnectedPerformance />}
{bottomBlock === GRAPHQL && <GraphQL />}
{bottomBlock === EXCEPTIONS && <Exceptions />}
{bottomBlock === INSPECTOR && <Inspector />}
</div>
)}
{!fullView ? (
@ -168,4 +169,4 @@ export default connect(
fullscreenOff,
updateLastPlayedSession,
}
)(Player);
)(observer(Player));

View file

@ -0,0 +1,69 @@
import React from 'react';
import { Button, Checkbox, Input } from 'antd';
import { useHistory } from 'react-router-dom';
import { withSiteId, sessions } from 'App/routes';
import store from 'App/store';
interface Props {
onSave: (name: string, ignoreClRage: boolean, ignoreDeadCl: boolean) => Promise<any>;
hideModal: () => void;
}
function SaveModal({ onSave, hideModal }: Props) {
const history = useHistory();
const [name, setName] = React.useState('');
const [ignoreClRage, setIgnoreClRage] = React.useState(false);
const [ignoreDeadCl, setIgnoreDeadCl] = React.useState(false);
const save = () => {
void onSave(name, ignoreClRage, ignoreDeadCl);
hideModal();
};
const saveAndOpen = () => {
onSave(name, ignoreClRage, ignoreDeadCl).then((tagId) => {
hideModal();
const siteId = store.getState().getIn(['site', 'siteId']);
history.push(withSiteId(sessions({ tnw: `is|${tagId}`, range: 'LAST_24_HOURS' }), siteId));
});
};
return (
<div className={'h-screen bg-white p-4 flex flex-col gap-4'}>
<div className={'font-semibold text-xl'}>Tag Element</div>
<div className={'w-full border border-b-light-gray'} />
<div>
<div className={'font-semibold'}>Name</div>
<Input
placeholder={'E.g Buy Now Button'}
className={'w-full'}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<div className={'font-semibold'}>Ignore following actions on this element</div>
<div className={'flex gap-2'}>
<Checkbox checked={ignoreClRage} onChange={(e) => setIgnoreClRage(e.target.checked)}>
Click Rage
</Checkbox>
<Checkbox checked={ignoreDeadCl} onChange={(e) => setIgnoreDeadCl(e.target.checked)}>
Dead Click
</Checkbox>
</div>
</div>
<div className={'w-full border border-b-light-gray'} />
<div className={'flex gap-2'}>
<Button type={'primary'} disabled={name === ''} onClick={save}>
Tag
</Button>
<Button type={'default'} disabled={name === ''} onClick={saveAndOpen}>
Tag & Find Element
</Button>
<Button type={'primary'} ghost onClick={hideModal}>
Cancel
</Button>
</div>
</div>
);
}
export default SaveModal;

View file

@ -0,0 +1,93 @@
import { useStore } from 'App/mstore';
import SaveModal from 'Components/Session/Player/TagWatch/SaveModal';
import React from 'react';
import { PlayerContext } from 'Components/Session/playerContext';
import { Button, Input } from 'antd';
import { CopyButton } from 'UI';
import { SearchOutlined, ZoomInOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import { useModal } from 'App/components/Modal';
import { toast } from 'react-toastify';
function TagWatch() {
const { tagWatchStore } = useStore();
const [selector, setSelector] = React.useState('');
const { store, player } = React.useContext(PlayerContext);
const { showModal, hideModal } = useModal();
const tagSelector = store.get().tagSelector;
React.useEffect(() => {
player.pause();
player.toggleInspectorMode(true);
player.scale();
return () => {
player.toggleInspectorMode(false);
player.scale();
};
}, []);
React.useEffect(() => {
if (tagSelector !== '' && tagSelector !== selector) {
setSelector(tagSelector);
}
}, [tagSelector]);
React.useEffect(() => {
if (selector !== tagSelector) {
player.markBySelector(selector);
}
}, [selector]);
const onSave = async (name: string, ignoreClRage: boolean, ignoreDeadCl: boolean) => {
try {
const tag = await tagWatchStore.createTag({
name,
selector,
ignoreClickRage: ignoreClRage,
ignoreDeadClick: ignoreDeadCl,
});
// @ts-ignore
toast.success('Tag created');
setSelector('');
return tag
} catch {
// @ts-ignore
toast.error('Failed to create tag');
}
};
const openSaveModal = () => {
if (selector === '') {
return;
}
showModal(<SaveModal onSave={onSave} hideModal={hideModal} />, { right: true, width: 400 });
};
return (
<div className={'w-full h-full p-2 flex flex-col gap-2'}>
<div className={'flex items-center justify-between'}>
<div className={'font-semibold text-xl'}>Element Selector</div>
<CopyButton content={selector} />
</div>
<Input.TextArea value={selector} onChange={(e) => setSelector(e.target.value)} />
<Button
onClick={openSaveModal}
type={'primary'}
ghost
icon={<ZoomInOutlined />}
disabled={selector === ''}
>
Tag Element
</Button>
<div className={'text-disabled-text text-sm'}>
Create and filter sessions by watch elements to determine if they rendered or not.
</div>
<div className={'w-full border border-b-light-gray'} />
<Button type={'link'} icon={<SearchOutlined />}>
Find session with selector
</Button>
</div>
);
}
export default observer(TagWatch);

View file

@ -0,0 +1 @@
export { default } from './TagWatch';

View file

@ -1,30 +1,40 @@
import React from 'react'
import React from 'react';
import EventsBlock from '../Session_/EventsBlock';
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel'
import PageInsightsPanel from '../Session_/PageInsightsPanel/PageInsightsPanel';
import TagWatch from "Components/Session/Player/TagWatch";
import cn from 'classnames';
import stl from './rightblock.module.css';
function RightBlock(props: any) {
const { activeTab } = props;
if (activeTab === 'EVENTS') {
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
<EventsBlock
setActiveTab={props.setActiveTab}
/>
</div>
)
function RightBlock({
activeTab,
setActiveTab,
}: {
activeTab: string;
setActiveTab: (tab: string) => void;
}) {
switch (activeTab) {
case 'EVENTS':
return (
<div className={cn('flex flex-col bg-white border-l', stl.panel)}>
<EventsBlock setActiveTab={setActiveTab} />
</div>
);
case 'CLICKMAP':
return (
<div className={cn('flex flex-col bg-white border-l', stl.panel)}>
<PageInsightsPanel setActiveTab={setActiveTab} />
</div>
);
case 'INSPECTOR':
return (
<div className={cn('bg-white border-l', stl.panel)}>
<TagWatch />
</div>
);
default:
return null;
}
if (activeTab === 'CLICKMAP') {
return (
<div className={cn("flex flex-col bg-white border-l", stl.panel)}>
<PageInsightsPanel setActiveTab={props.setActiveTab} />
</div>
)
}
return null
}
export default RightBlock
export default RightBlock;

View file

@ -1,31 +1,48 @@
import React from 'react';
import cn from 'classnames';
import stl from './tabs.module.css';
import { Segmented } from 'antd';
import { Icon } from 'UI'
interface Props {
tabs: Array<any>;
active: string;
onClick: (key: any) => void;
border?: boolean;
className?: string;
tabs: Array<any>;
active: string;
onClick: (key: any) => void;
border?: boolean;
className?: string;
}
const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => (
<div className={ cn(stl.tabs, className, { [ stl.bordered ]: border }) } role="tablist" >
{ tabs.map(({ key, text, hidden = false, disabled = false }) => (
<div
key={ key }
className={ cn(stl.tab, { [ stl.active ]: active === key, [ stl.disabled ]: disabled }) }
data-hidden={ hidden }
onClick={ onClick && (() => onClick(key)) }
role="tab"
data-openreplay-label={text}
>
{ text }
</div>
))}
</div>
);
const iconMap = {
"INSPECTOR": 'filters/tag-element',
"CLICKMAP": 'mouse-pointer-click',
'EVENTS': 'user-switch'
} as const
const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
console.log(tabs)
return (
<div className={cn(stl.tabs, className, { [stl.bordered]: border })} role="tablist">
<Segmented
value={active}
options={tabs.map(({ key, text, hidden = false, disabled = false }) => ({
label: (
<div
onClick={() => {
onClick(key);
}}
className={'font-semibold flex gap-1 items-center'}
>
<Icon size={16} color={'black'} name={iconMap[key as keyof typeof iconMap]} />
<span>{text}</span>
</div>
),
value: key,
disabled: disabled,
}))}
/>
</div>
);
};
Tabs.displayName = 'Tabs';

View file

@ -1,14 +1,17 @@
.tabs {
display: flex;
height: 100%;
width: 100%;
justify-content: space-around;
align-items: center;
font-size: 16px;
&.bordered {
border-bottom: solid thin $gray-light;
border-bottom: solid thin $gray-light;
}
}
}
.tab {
padding: 14px 15px;
.tab {
padding: 14px 0;
text-align: center;
text-transform: uppercase;
flex: 1;
@ -19,16 +22,16 @@
white-space: nowrap;
&:hover {
color: $teal;
color: $teal;
}
&.active {
color: $teal;
border-bottom: solid thin $teal;
color: $teal;
border-bottom: solid thin $teal;
}
}
}
.disabled {
.disabled {
pointer-events: none;
opacity: 0.5;
}
}

View file

@ -17,8 +17,9 @@ import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
const TABS = {
EVENTS: 'User Events',
EVENTS: 'Activity',
CLICKMAP: 'Click Map',
INSPECTOR: 'Tag',
};
const UXTTABS = {
EVENTS: TABS.EVENTS

View file

@ -2,7 +2,7 @@ import { useStore } from "App/mstore";
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import {MarkedTarget, selectStorageType, STORAGE_TYPES, StorageType} from 'Player';
import { selectStorageType, STORAGE_TYPES, StorageType } from 'Player';
import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui'
import { Tooltip } from 'UI';
@ -131,13 +131,8 @@ function Controls(props: any) {
};
const toggleBottomTools = (blockName: number) => {
if (blockName === INSPECTOR) {
// player.toggleInspectorMode(false);
bottomBlock && toggleBottomBlock();
} else {
// player.toggleInspectorMode(false);
player.toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
const state = completed ? PlayingState.Completed : playing ? PlayingState.Playing : PlayingState.Paused

View file

@ -15,8 +15,14 @@
/* border: solid thin $gray-light; */
/* border-radius: 3px; */
overflow: hidden;
}
.checkers {
background: repeating-conic-gradient($gray-lightest 0% 25%, transparent 0% 50%)
50% / 10px 10px;
50% / 10px 10px;
}
.solidBg {
background: $gray-lightest;
}
.mobileScreenWrapper {

View file

@ -4,12 +4,14 @@ import SavedSearch from 'Shared/SavedSearch';
import { Button } from 'UI';
import { connect } from 'react-redux';
import { clearSearch } from 'Duck/search';
import TagList from './components/TagList';
interface Props {
clearSearch: () => void;
appliedFilter: any;
savedSearch: any;
}
const MainSearchBar = (props: Props) => {
const { appliedFilter } = props;
const hasFilters = appliedFilter && appliedFilter.filters && appliedFilter.filters.size > 0;
@ -20,7 +22,8 @@ const MainSearchBar = (props: Props) => {
<div style={{ width: '60%', marginRight: '10px' }}>
<SessionSearchField />
</div>
<div className="flex items-center" style={{ width: '40%' }}>
<div className="flex items-center gap-2" style={{ width: '40%' }}>
<TagList />
<SavedSearch />
<Button
variant={hasSearch ? 'text-primary' : 'text'}
@ -37,7 +40,7 @@ const MainSearchBar = (props: Props) => {
export default connect(
(state: any) => ({
appliedFilter: state.getIn(['search', 'instance']),
savedSearch: state.getIn([ 'search', 'savedSearch' ])
savedSearch: state.getIn(['search', 'savedSearch']),
}),
{
clearSearch,

View file

@ -0,0 +1,156 @@
import { Tag } from 'App/services/TagWatchService';
import { useModal } from 'Components/Modal';
import { refreshFilterOptions, addFilterByKeyAndValue } from 'Duck/search';
import { connect } from 'react-redux';
import React from 'react';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { FilterKey } from 'Types/filter/filterType';
import { addOptionsToFilter } from 'Types/filter/newFilter';
import { Button, Icon, confirm } from 'UI';
import { Typography } from 'antd';
import { toast } from 'react-toastify';
function TagList(props: {
refreshFilterOptions: typeof refreshFilterOptions;
addFilterByKeyAndValue: typeof addFilterByKeyAndValue;
}) {
const { refreshFilterOptions, addFilterByKeyAndValue } = props;
const { tagWatchStore } = useStore();
const { showModal, hideModal } = useModal();
React.useEffect(() => {
if (!tagWatchStore.isLoading) {
tagWatchStore.getTags().then((tags) => {
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({ label: tag.name, value: tag.tagId.toString() }))
);
refreshFilterOptions();
}
});
}
}, []);
const addTag = (tagId: number) => {
addFilterByKeyAndValue(FilterKey.TAGGED_ELEMENT, tagId.toString());
hideModal();
};
const openModal = () => {
showModal(<TagListModal onTagClick={addTag} />, {
right: true,
width: 400,
});
};
return (
<Button variant={'outline'} disabled={!tagWatchStore.tags.length} onClick={openModal}>
<span>Tags</span>
<span className={'font-bold ml-1'}>{tagWatchStore.tags.length}</span>
</Button>
);
}
const TagListModal = observer(({ onTagClick }: { onTagClick: (tagId: number) => void }) => {
const { tagWatchStore } = useStore();
const updateTagName = (id: number, name: string) => {
void tagWatchStore.updateTagName(id, name);
// very annoying
// @ts-ignore
toast.success('Tag name updated');
};
const onRemove = async (id: number) => {
if (
await confirm({
header: 'Remove Tag',
confirmButton: 'Remove',
confirmation: 'Are you sure you want to remove this tag?',
})
) {
void tagWatchStore.deleteTag(id);
}
};
return (
<div className={'h-screen flex flex-col gap-2 p-4'}>
<div className={'text-2xl font-semibold'}>Tagged Elements</div>
{tagWatchStore.tags.map((tag) => (
<TagRow
key={tag.tagId}
tag={tag}
onEdit={updateTagName}
onDelete={onRemove}
onTagClick={onTagClick}
/>
))}
</div>
);
});
const TagRow = (props: {
tag: Tag;
onEdit: (id: number, name: string) => void;
onDelete: (id: number) => void;
onTagClick: (tagId: number) => void;
}) => {
const { tag, onEdit, onDelete, onTagClick } = props;
const [isEditing, setIsEditing] = React.useState(false);
const [name, setName] = React.useState(tag.name);
return (
<div
className={
'w-full border-b border-b-gray-light p-2 hover:bg-active-blue flex items-center gap-2 cursor-pointer'
}
onClick={() => onTagClick(tag.tagId)}
key={tag.tagId}
>
<Icon name={'search'} />
<Typography.Text
editable={{
onChange: (e) => {
if (e !== tag.name) {
onEdit(tag.tagId, e);
setName(e);
}
setIsEditing(false);
},
text: name,
editing: isEditing,
onCancel: () => {
setIsEditing(false);
setName(tag.name);
},
triggerType: [],
maxLength: 90,
}}
>
{tag.name}
</Typography.Text>
<div
className={'cursor-pointer ml-auto p-2 hover:bg-gray-light rounded'}
onClick={(e) => {
e.stopPropagation();
setIsEditing(true);
}}
>
<Icon name={'edit'} />
</div>
<div
className={'cursor-pointer p-2 hover:bg-gray-light rounded'}
onClick={(e) => {
e.stopPropagation();
void onDelete(tag.tagId);
}}
>
<Icon name={'trash'} />
</div>
</div>
);
};
export default connect(() => ({}), { refreshFilterOptions, addFilterByKeyAndValue })(
observer(TagList)
);

View file

@ -3,33 +3,57 @@ import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { connect } from 'react-redux';
import { FilterKey } from 'Types/filter/filterType';
import { addOptionsToFilter } from 'Types/filter/newFilter';
import { Button } from 'UI';
import { edit, addFilter, fetchSessions, updateFilter } from 'Duck/search';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { debounce } from 'App/utils';
import useSessionSearchQueryHandler from 'App/hooks/useSessionSearchQueryHandler';
import { refreshFilterOptions } from 'Duck/search';
let debounceFetch: any = () => {}
let debounceFetch: any = () => {};
interface Props {
appliedFilter: any;
edit: typeof edit;
addFilter: typeof addFilter;
saveRequestPayloads: boolean;
metaLoading?: boolean
metaLoading?: boolean;
fetchSessions: typeof fetchSessions;
updateFilter: typeof updateFilter;
refreshFilterOptions: typeof refreshFilterOptions;
}
function SessionSearch(props: Props) {
const { tagWatchStore } = useStore();
const { appliedFilter, saveRequestPayloads = false, metaLoading = false } = props;
const hasEvents = appliedFilter.filters.filter((i: any) => i.isEvent).size > 0;
const hasFilters = appliedFilter.filters.filter((i: any) => !i.isEvent).size > 0;
useSessionSearchQueryHandler({ appliedFilter, applyFilter: props.updateFilter, loading: metaLoading });
useSessionSearchQueryHandler({
appliedFilter,
applyFilter: props.updateFilter,
loading: metaLoading,
onBeforeLoad: async () => {
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
tags.map((tag) => ({
label: tag.name,
value: tag.tagId.toString()
}))
);
props.refreshFilterOptions();
}
},
});
useEffect(() => {
debounceFetch = debounce(() => props.fetchSessions(), 500);
}, [])
}, []);
const onAddFilter = (filter: any) => {
props.addFilter(filter);
@ -49,7 +73,7 @@ function SessionSearch(props: Props) {
filters: newFilters,
});
debounceFetch()
debounceFetch();
};
const onRemoveFilter = (filterIndex: any) => {
@ -61,7 +85,7 @@ function SessionSearch(props: Props) {
filters: newFilters,
});
debounceFetch()
debounceFetch();
};
const onChangeEventsOrder = (e: any, { value }: any) => {
@ -69,7 +93,7 @@ function SessionSearch(props: Props) {
eventsOrder: value,
});
debounceFetch()
debounceFetch();
};
return !metaLoading ? (
@ -89,11 +113,7 @@ function SessionSearch(props: Props) {
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
<Button
variant="text-primary"
className="mr-2"
icon="plus"
>
<Button variant="text-primary" className="mr-2" icon="plus">
ADD STEP
</Button>
</FilterSelection>
@ -114,7 +134,7 @@ export default connect(
(state: any) => ({
saveRequestPayloads: state.getIn(['site', 'instance', 'saveRequestPayloads']),
appliedFilter: state.getIn(['search', 'instance']),
metaLoading: state.getIn(['customFields', 'fetchRequestActive', 'loading'])
metaLoading: state.getIn(['customFields', 'fetchRequestActive', 'loading']),
}),
{ edit, addFilter, fetchSessions, updateFilter }
)(SessionSearch);
{ edit, addFilter, fetchSessions, updateFilter, refreshFilterOptions }
)(observer(SessionSearch));

View file

@ -0,0 +1,19 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Filters_tag_element(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><g><path clipRule="evenodd" d="M1.958 6.664a3.908 3.908 0 0 0 .785 2.318c.046.036.088.08.123.13.29.408.837.959 1.531 1.404.695.446 1.484.752 2.263.752a.625.625 0 1 1 0 1.25c-1.096 0-2.119-.424-2.937-.95-.82-.525-1.487-1.184-1.876-1.734l-.007-.01a.626.626 0 0 1-.115-.118A5.158 5.158 0 0 1 .708 6.672v-.005C.708 3.252 4.112.708 8 .708c3.887 0 7.291 2.544 7.291 5.959v.013a5.416 5.416 0 0 1-.248 1.508.625.625 0 1 1-1.193-.376c.118-.373.182-.761.191-1.152C14.037 4.184 11.47 1.958 8 1.958c-3.472 0-6.04 2.229-6.042 4.706Zm8 6.457V8.778l3.01 3.076-1.317-.44a.625.625 0 0 0-.718.246l-.975 1.46Zm.135-5.988a.84.84 0 0 0-.54-.198h-.018a.852.852 0 0 0-.827.852v6.666a.858.858 0 0 0 .589.807c.063.021.13.032.196.032h.06a.858.858 0 0 0 .706-.372l1.449-2.17 2.54.85c.06.02.122.03.185.032a.845.845 0 0 0 .58-1.476l-4.873-4.98a.618.618 0 0 0-.047-.043Z"/></g><defs><clipPath id="a"><path fill="none" d="M0 0h16v16H0z"/></clipPath></defs></svg>
);
}
export default Filters_tag_element;

View file

@ -225,6 +225,7 @@ export { default as Filters_referrer } from './filters_referrer';
export { default as Filters_resize } from './filters_resize';
export { default as Filters_rev_id } from './filters_rev_id';
export { default as Filters_state_action } from './filters_state_action';
export { default as Filters_tag_element } from './filters_tag_element';
export { default as Filters_ttfb } from './filters_ttfb';
export { default as Filters_user_alt } from './filters_user_alt';
export { default as Filters_userid } from './filters_userid';
@ -344,6 +345,7 @@ export { default as Mic } from './mic';
export { default as Minus } from './minus';
export { default as Mobile } from './mobile';
export { default as Mouse_alt } from './mouse_alt';
export { default as Mouse_pointer_click } from './mouse_pointer_click';
export { default as Network } from './network';
export { default as Next1 } from './next1';
export { default as No_dashboard } from './no_dashboard';
@ -454,6 +456,7 @@ export { default as Turtle } from './turtle';
export { default as User_alt } from './user_alt';
export { default as User_circle } from './user_circle';
export { default as User_friends } from './user_friends';
export { default as User_switch } from './user_switch';
export { default as Users } from './users';
export { default as Vendors_graphql } from './vendors_graphql';
export { default as Vendors_mobx } from './vendors_mobx';

View file

@ -0,0 +1,19 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function Mouse_pointer_click(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg fill="none" width={ `${ width }px` } height={ `${ height }px` } ><g stroke="#000" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="m5.25 5.25 2.917 7L9.2 9.201l3.049-1.034-7-2.917Z" fill="#000" fillOpacity=".85"/><path d="m9.375 9.375 2.475 2.475M4.193 1.306l.453 1.69m-1.65 1.65-1.69-.453m6.832-1.83L6.9 3.6M3.6 6.9 2.363 8.137"/></g></svg>
);
}
export default Mouse_pointer_click;

View file

@ -0,0 +1,19 @@
/* Auto-generated, do not edit */
import React from 'react';
interface Props {
size?: number | string;
width?: number | string;
height?: number | string;
fill?: string;
}
function User_switch(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg fill="none" width={ `${ width }px` } height={ `${ height }px` } ><path d="M11.526 4.23a3.874 3.874 0 1 0-6.14 3.144l-.015.007A5.822 5.822 0 0 0 3.507 8.64a5.835 5.835 0 0 0-1.256 1.867 5.838 5.838 0 0 0-.46 2.158.125.125 0 0 0 .126.128h.935a.126.126 0 0 0 .125-.122 4.662 4.662 0 0 1 1.37-3.192A4.631 4.631 0 0 1 7.65 8.106a3.874 3.874 0 0 0 3.875-3.875ZM7.65 6.919a2.687 2.687 0 1 1 0-5.375 2.687 2.687 0 0 1 0 5.375Zm1.64 3.453h4.125a.125.125 0 0 0 .125-.125v-.875a.125.125 0 0 0-.125-.125H10.66l.737-.939a.126.126 0 0 0 .027-.076.125.125 0 0 0-.125-.125h-1.135a.255.255 0 0 0-.196.095l-1.07 1.36a.501.501 0 0 0 .395.81Zm3.75 1H8.918a.125.125 0 0 0-.125.125v.875c0 .069.056.125.125.125h2.757l-.737.94a.125.125 0 0 0 .098.201h1.135a.255.255 0 0 0 .197-.096l1.07-1.36a.502.502 0 0 0-.396-.81Z" fill="#000" fillOpacity=".85"/></svg>
);
}
export default User_switch;

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
export default {
warning: 'Warnings',
alert: 'Alerts',
all: 'Log Entires',
all: 'Log Entries',
};

View file

@ -8,7 +8,12 @@ import { errors as errorsRoute, isRoute } from 'App/routes';
import { fetchList as fetchSessionList, fetchAutoplayList } from './sessions';
import { fetchList as fetchErrorsList } from './errors';
import { FilterCategory, FilterKey } from 'Types/filter/filterType';
import { filtersMap, liveFiltersMap, conditionalFiltersMap, generateFilterOptions } from 'Types/filter/newFilter';
import {
filtersMap,
liveFiltersMap,
conditionalFiltersMap,
generateFilterOptions
} from "Types/filter/newFilter";
import { DURATION_FILTER } from 'App/constants/storageKeys';
import Period, { CUSTOM_RANGE } from 'Types/app/period';

View file

@ -1,38 +1,44 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import { createUrlQuery, getFiltersFromQuery } from 'App/utils/search';
interface Props {
onBeforeLoad?: () => Promise<any>;
appliedFilter: any;
applyFilter: any;
loading: boolean;
}
const useSessionSearchQueryHandler = (props: Props) => {
const [beforeHookLoaded, setBeforeHookLoaded] = useState(!props.onBeforeLoad);
const { appliedFilter, applyFilter, loading } = props;
const history = useHistory();
useEffect(() => {
const applyFilterFromQuery = () => {
const applyFilterFromQuery = async () => {
if (!loading) {
if (props.onBeforeLoad) {
await props.onBeforeLoad();
setBeforeHookLoaded(true);
}
const filter = getFiltersFromQuery(history.location.search, appliedFilter);
applyFilter(filter, true, false);
}
};
applyFilterFromQuery();
void applyFilterFromQuery();
}, [loading]);
useEffect(() => {
const generateUrlQuery = () => {
if (!loading) {
if (!loading && beforeHookLoaded) {
const search: any = createUrlQuery(appliedFilter);
history.replace({ search });
}
};
generateUrlQuery();
}, [appliedFilter, loading]);
}, [appliedFilter, loading, beforeHookLoaded]);
return null;
};

View file

@ -5,22 +5,21 @@ import UserStore from './userStore';
import RoleStore from './roleStore';
import APIClient from 'App/api_client';
import FunnelStore from './funnelStore';
import {
services
} from 'App/services';
import { services } from 'App/services';
import SettingsStore from './settingsStore';
import AuditStore from './auditStore';
import NotificationStore from './notificationStore';
import ErrorStore from './errorStore';
import SessionStore from './sessionStore';
import NotesStore from './notesStore';
import BugReportStore from './bugReportStore'
import RecordingsStore from './recordingsStore'
import BugReportStore from './bugReportStore';
import RecordingsStore from './recordingsStore';
import AssistMultiviewStore from './assistMultiviewStore';
import WeeklyReportStore from './weeklyReportConfigStore'
import AlertStore from './alertsStore'
import FeatureFlagsStore from "./featureFlagsStore";
import WeeklyReportStore from './weeklyReportConfigStore';
import AlertStore from './alertsStore';
import FeatureFlagsStore from './featureFlagsStore';
import UxtestingStore from './uxtestingStore';
import TagWatchStore from './tagWatchStore';
export class RootStore {
dashboardStore: DashboardStore;
@ -37,10 +36,11 @@ export class RootStore {
bugReportStore: BugReportStore;
recordingsStore: RecordingsStore;
assistMultiviewStore: AssistMultiviewStore;
weeklyReportStore: WeeklyReportStore
alertsStore: AlertStore
featureFlagsStore: FeatureFlagsStore
uxtestingStore: UxtestingStore
weeklyReportStore: WeeklyReportStore;
alertsStore: AlertStore;
featureFlagsStore: FeatureFlagsStore;
uxtestingStore: UxtestingStore;
tagWatchStore: TagWatchStore;
constructor() {
this.dashboardStore = new DashboardStore();
@ -61,13 +61,14 @@ export class RootStore {
this.alertsStore = new AlertStore();
this.featureFlagsStore = new FeatureFlagsStore();
this.uxtestingStore = new UxtestingStore();
this.tagWatchStore = new TagWatchStore();
}
initClient() {
const client = new APIClient();
services.forEach(service => {
services.forEach((service) => {
service.initClient(client);
})
});
}
}

View file

@ -0,0 +1,66 @@
import { makeAutoObservable } from 'mobx';
import { tagWatchService } from 'App/services';
import { CreateTag, Tag } from 'App/services/TagWatchService';
export default class TagWatchStore {
tags: Tag[] = [];
isLoading = false;
constructor() {
makeAutoObservable(this);
}
setTags = (tags: Tag[]) => {
this.tags = tags;
};
setLoading = (loading: boolean) => {
this.isLoading = loading;
};
getTags = async () => {
if (this.isLoading) {
return;
}
this.setLoading(true);
try {
const tags: Tag[] = await tagWatchService.getTags();
this.setTags(tags);
return tags;
} catch (e) {
console.error(e);
} finally {
this.setLoading(false);
}
};
createTag = async (data: CreateTag) => {
try {
const tagId: number = await tagWatchService.createTag(data);
return tagId;
} catch (e) {
console.error(e);
}
};
deleteTag = async (id: number) => {
try {
await tagWatchService.deleteTag(id);
this.setTags(this.tags.filter((t) => t.tagId !== id));
} catch (e) {
console.error(e);
}
};
updateTagName = async (id: number, name: string) => {
try {
await tagWatchService.updateTagName(id, name);
const updatedTag = this.tags.find((t) => t.tagId === id)
if (updatedTag) {
this.setTags(this.tags.map((t) => t.tagId === id ? { ...updatedTag, name } : t));
}
} catch (e) {
console.error(e);
}
};
}

View file

@ -1,19 +1,10 @@
import type Screen from './Screen'
import type Marker from './Marker'
//import { select } from 'optimal-select';
export default class Inspector {
// private captureCallbacks = [];
// private bubblingCallbacks = [];
constructor(private screen: Screen, private marker: Marker) {}
private onMouseMove = (e: MouseEvent) => {
// const { overlay } = this.screen;
// if (!overlay.contains(e.target)) {
// return;
// }
e.stopPropagation();
const target = this.screen.getElementFromPoint(e);
@ -34,30 +25,15 @@ export default class Inspector {
return
}
this.clickCallback && this.clickCallback({ target });
// const targets = [ target ];
// while (target.parentElement !== null) {
// target = target.parentElement;
// targets.push(target);
// }
// for (let i = targets.length - 1; i >= 0; i--) {
// for (let j = 0; j < this.captureCallbacks.length; j++) {
// this.captureCallbacks[j]({ target: targets[i] });
// }
// }
// onTargetClick(select(markedTarget, { root: this.screen.document }));
}
// addClickListener(callback, useCapture = false) {
// if (useCapture) {
// this.captureCallbacks.push(callback);
// } else {
// //this.bubblingCallbacks.push(callback);
// }
// }
addClickListener(callback: (el: { target: Element }) => void) {
this.clickCallback = callback
}
private clickCallback: (e: { target: Element }) => void | null = null
enable(clickCallback?: Inspector['clickCallback']) {
private clickCallback: (e: { target: Element }) => void = () => {}
enable() {
this.screen.overlay.addEventListener('mousemove', this.onMouseMove)
this.screen.overlay.addEventListener('mouseleave', this.onOverlayLeave)
this.screen.overlay.addEventListener('click', this.onMarkClick)

View file

@ -1,5 +1,6 @@
import type Screen from './Screen'
import type Screen from './Screen';
import styles from './marker.module.css';
import { finder } from '@medv/finder';
const metaCharsMap = {
'&': '&amp;',
@ -9,7 +10,7 @@ const metaCharsMap = {
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
'=': '&#x3D;',
};
function escapeHtml(str: string) {
@ -19,29 +20,30 @@ function escapeHtml(str: string) {
});
}
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function safeString(string: string) {
return (escapeHtml(escapeRegExp(string)))
return escapeHtml(escapeRegExp(string));
}
export default class Marker {
private _target: Element | null = null;
private selector: string | null = null;
private tooltip: HTMLDivElement
private marker: HTMLDivElement
private readonly tooltip: HTMLDivElement;
private readonly tooltipSelector: HTMLDivElement;
private readonly tooltipHint: HTMLDivElement;
private marker: HTMLDivElement;
constructor(overlay: HTMLElement, private readonly screen: Screen) {
constructor(private readonly overlay: HTMLElement, private readonly screen: Screen) {
this.tooltip = document.createElement('div');
this.tooltip.className = styles.tooltip;
this.tooltip.appendChild(document.createElement('div'));
const htmlStr = document.createElement('div');
htmlStr.innerHTML = '<b>Right-click > Inspect</b> for more details.';
this.tooltip.appendChild(htmlStr);
this.tooltipSelector = document.createElement('div');
this.tooltipHint = document.createElement('div');
this.tooltipHint.innerText = '(click to tag element)'
this.tooltipHint.className = styles.tooltipHint;
this.tooltip.append(this.tooltipSelector, this.tooltipHint);
const marker = document.createElement('div');
marker.className = styles.marker;
@ -78,19 +80,17 @@ export default class Marker {
}
unmark() {
this.mark(null)
this.mark(null);
}
private autodefineTarget() {
// TODO: put to Screen
if (this.selector) {
if (this.selector && this.screen.document) {
try {
const fitTargets = this.screen.document.querySelectorAll(this.selector);
if (fitTargets.length === 0) {
this._target = null;
} else {
// TODO: fix getCursorTarget()?
// this._target = fitTargets[0];
this._target = fitTargets[0];
// const cursorTarget = this.screen.getCursorTarget();
// fitTargets.forEach((target) => {
// if (target.contains(cursorTarget)) {
@ -108,27 +108,23 @@ export default class Marker {
markBySelector(selector: string) {
this.selector = selector;
this.lastSelector = selector;
this.autodefineTarget();
this.redraw();
}
lastSelector = '';
private getTagString(el: Element) {
const attrs = el.attributes;
let str = `<span style="color:#9BBBDC">${el.tagName.toLowerCase()}</span>`;
for (let i = 0; i < attrs.length; i++) {
let k = attrs[i];
const attribute = k.name;
if (attribute === 'class') {
str += `<span style="color:#F29766">${'.' + safeString(k.value).split(' ').join('.')}</span>`;
}
if (attribute === 'id') {
str += `<span style="color:#F29766">${'#' + safeString(k.value).split(' ').join('#')}</span>`;
}
}
return str;
if (!this.screen.document) return '';
const selector = finder(el, {
root: this.screen.document.body,
seedMinLength: 3,
optimizedMinLength: 2,
threshold: 1000,
maxNumberOfTries: 10_000,
});
this.lastSelector = selector;
return selector
}
redraw() {
@ -146,6 +142,17 @@ export default class Marker {
this.marker.style.width = rect.width + 'px';
this.marker.style.height = rect.height + 'px';
this.tooltip.firstChild.innerHTML = this.getTagString(this._target);
const replayScale = this.screen.getScale()
if (replayScale < 1) {
const upscale = (1 / replayScale).toFixed(3);
const yShift = ((1 - replayScale)/2) * 100;
this.tooltip.style.transform = `scale(${upscale}) translateY(-${yShift + 0.5}%)`
}
this.tooltipSelector.textContent = this.getTagString(this._target);
}
clean() {
this.marker.remove();
}
}

View file

@ -39,24 +39,21 @@
position: absolute;
left: 0;
bottom: 100%;
padding: 15px;
padding: 8px;
max-width: 600px;
box-shadow: 2px 2px 1px rgba(40, 40, 100, .3);
z-index: 999;
border-radius: 3px;
background-color: #202124;
min-width: 400px;
font-size: 20px !important;
& div:first-child {
max-width: 600px;
height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
& div:last-child {
font-size: 18px;
margin-top: 10px;
color: $tealx;
}
min-width: 300px;
font-size: 16px !important;
color:#9BBBDC;
}
.tooltipHint {
margin-top: 4px;
font-size: 14px;
width: 100%;
text-align: center;
color: rgba(138, 170, 201, 0.8);
}

View file

@ -16,6 +16,8 @@ export default class WebPlayer extends Player {
...TargetMarker.INITIAL_STATE,
...MessageManager.INITIAL_STATE,
...MessageLoader.INITIAL_STATE,
...InspectorController.INITIAL_STATE,
liveTimeTravel: false,
inspectorMode: false,
}
@ -65,7 +67,7 @@ export default class WebPlayer extends Player {
}
this.targetMarker = new TargetMarker(this.screen, wpState)
this.inspectorController = new InspectorController(screen)
this.inspectorController = new InspectorController(screen, wpState)
const endTime = session.duration?.valueOf() || 0
@ -126,7 +128,7 @@ export default class WebPlayer extends Player {
this.inspectorController.marker?.mark(e)
}
toggleInspectorMode = (flag: boolean, clickCallback?: Parameters<InspectorController['enableInspector']>[0]) => {
toggleInspectorMode = (flag: boolean) => {
if (typeof flag !== 'boolean') {
const { inspectorMode } = this.wpState.get()
flag = !inspectorMode
@ -135,13 +137,17 @@ export default class WebPlayer extends Player {
if (flag) {
this.pause()
this.wpState.update({ inspectorMode: true })
return this.inspectorController.enableInspector(clickCallback)
return this.inspectorController.enableInspector()
} else {
this.inspectorController.disableInspector()
this.wpState.update({ inspectorMode: false })
}
}
markBySelector = (selector: string) => {
this.inspectorController.markBySelector(selector)
}
// Target Marker
setActiveTarget = (...args: Parameters<TargetMarker['setActiveTarget']>) => {
this.targetMarker.setActiveTarget(...args)

View file

@ -1,72 +1,58 @@
import Marker from '../Screen/Marker'
import Inspector from '../Screen/Inspector'
import Screen from '../Screen/Screen'
import type { Dimensions } from '../Screen/types'
import type { Store } from 'Player';
import { State } from 'Player/web/addons/TargetMarker';
import Marker from '../Screen/Marker';
import Inspector from '../Screen/Inspector';
import Screen, { ScaleMode } from '../Screen/Screen';
import type { Dimensions } from '../Screen/types';
export default class InspectorController {
private substitutor: Screen | null = null
private inspector: Inspector | null = null
marker: Marker | null = null
constructor(private screen: Screen) {
screen.overlay.addEventListener('contextmenu', () => {
screen.overlay.style.display = 'none'
const doc = screen.document
if (!doc) { return }
const returnOverlay = () => {
screen.overlay.style.display = 'block'
doc.removeEventListener('mousemove', returnOverlay)
doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection
}
doc.addEventListener('mousemove', returnOverlay)
doc.addEventListener('mouseclick', returnOverlay)
})
static INITIAL_STATE = {
tagSelector: '',
}
private substitutor: Screen | null = null;
private inspector: Inspector | null = null;
marker: Marker | null = null;
constructor(private screen: Screen, private readonly store: Store<{ tagSelector: string }>) {
screen.overlay.addEventListener('contextmenu', () => {
screen.overlay.style.display = 'none';
const doc = screen.document;
if (!doc) {
return;
}
const returnOverlay = () => {
screen.overlay.style.display = 'block';
doc.removeEventListener('mousemove', returnOverlay);
doc.removeEventListener('mouseclick', returnOverlay); // TODO: prevent default in case of input selection
};
doc.addEventListener('mousemove', returnOverlay);
doc.addEventListener('mouseclick', returnOverlay);
});
}
scale(dims: Dimensions) {
if (this.substitutor) {
this.substitutor.scale(dims)
}
this.screen.scale(dims);
}
enableInspector(clickCallback?: (e: { target: Element }) => void): Document | null {
const parent = this.screen.getParentElement()
if (!parent) return null;
if (!this.substitutor) {
this.substitutor = new Screen()
this.marker = new Marker(this.substitutor.overlay, this.substitutor)
this.inspector = new Inspector(this.substitutor, this.marker)
//this.inspector.addClickListener(clickCallback, true)
this.substitutor.attach(parent)
}
enableInspector(): Document | null {
this.marker = new Marker(this.screen.overlay, this.screen);
this.inspector = new Inspector(this.screen, this.marker);
this.inspector.addClickListener(() => {
this.store.update({ tagSelector: this.marker?.lastSelector ?? '' })
});
this.substitutor.display(false)
this.inspector?.enable();
return this.screen.document;
}
const docElement = this.screen.document?.documentElement // this.substitutor.document?.importNode(
const doc = this.substitutor.document
if (doc && docElement) {
doc.open()
doc.write(docElement.outerHTML)
doc.close()
// TODO! : copy stylesheets & cssRules?
}
this.screen.display(false);
this.inspector.enable(clickCallback);
this.substitutor.display(true);
return doc;
markBySelector(selector: string) {
this.marker?.markBySelector(selector);
}
disableInspector() {
if (this.substitutor) {
const doc = this.substitutor.document;
if (doc) {
doc.documentElement.innerHTML = "";
}
this.inspector.clean();
this.substitutor.display(false);
}
this.screen.display(true);
this.inspector?.clean();
this.inspector = null;
this.marker?.clean();
this.marker = null;
}
}

View file

@ -741,6 +741,14 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 120: {
const tagId = this.readInt(); if (tagId === null) { return resetPointer() }
return {
tp: MType.TagTrigger,
tagId,
};
}
case 93: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const length = this.readUint(); if (length === null) { return resetPointer() }

View file

@ -63,6 +63,7 @@ import type {
RawTabChange,
RawTabData,
RawCanvasNode,
RawTagTrigger,
RawIosEvent,
RawIosScreenChanges,
RawIosClickEvent,
@ -196,6 +197,8 @@ export type TabData = RawTabData & Timed
export type CanvasNode = RawCanvasNode & Timed
export type TagTrigger = RawTagTrigger & Timed
export type IosEvent = RawIosEvent & Timed
export type IosScreenChanges = RawIosScreenChanges & Timed

View file

@ -61,6 +61,7 @@ export const enum MType {
TabChange = 117,
TabData = 118,
CanvasNode = 119,
TagTrigger = 120,
IosEvent = 93,
IosScreenChanges = 96,
IosClickEvent = 100,
@ -494,6 +495,11 @@ export interface RawCanvasNode {
timestamp: number,
}
export interface RawTagTrigger {
tp: MType.TagTrigger,
tagId: number,
}
export interface RawIosEvent {
tp: MType.IosEvent,
timestamp: number,
@ -586,4 +592,4 @@ export interface RawIosIssueEvent {
}
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;

View file

@ -62,6 +62,7 @@ export const TP_MAP = {
117: MType.TabChange,
118: MType.TabData,
119: MType.CanvasNode,
120: MType.TagTrigger,
93: MType.IosEvent,
96: MType.IosScreenChanges,
100: MType.IosClickEvent,

View file

@ -509,8 +509,13 @@ type TrCanvasNode = [
timestamp: number,
]
type TrTagTrigger = [
type: 120,
tagId: number,
]
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
@ -1028,6 +1033,13 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 120: {
return {
tp: MType.TagTrigger,
tagId: tMsg[1],
}
}
default:
return null
}

View file

@ -0,0 +1,38 @@
import BaseService from "App/services/BaseService";
export interface CreateTag {
name: string;
selector: string;
ignoreClickRage: boolean;
ignoreDeadClick: boolean;
}
export interface Tag extends CreateTag {
tagId: number;
}
export default class TagWatchService extends BaseService {
createTag(data: CreateTag) {
return this.client.post('/tags', data)
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
}
getTags() {
return this.client.get('/tags')
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
}
deleteTag(id: number) {
return this.client.delete(`/tags/${id}`)
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
}
updateTagName(id: number, name: string) {
return this.client.put(`/tags/${id}`, { name })
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
}
}

View file

@ -1,23 +1,24 @@
import DashboardService from "./DashboardService";
import MetricService from "./MetricService";
import FunnelService from "./FunnelService";
import SessionSerivce from "./SessionService";
import UserService from "./UserService";
import DashboardService from './DashboardService';
import MetricService from './MetricService';
import FunnelService from './FunnelService';
import SessionService from './SessionService';
import UserService from './UserService';
import AuditService from './AuditService';
import ErrorService from "./ErrorService";
import NotesService from "./NotesService";
import RecordingsService from "./RecordingsService";
import ConfigService from './ConfigService'
import AlertsService from './AlertsService'
import WebhookService from './WebhookService'
import HealthService from "./HealthService";
import FFlagsService from "App/services/FFlagsService";
import AssistStatsService from './AssistStatsService'
import UxtestingService from './UxtestingService'
import ErrorService from './ErrorService';
import NotesService from './NotesService';
import RecordingsService from './RecordingsService';
import ConfigService from './ConfigService';
import AlertsService from './AlertsService';
import WebhookService from './WebhookService';
import HealthService from './HealthService';
import FFlagsService from 'App/services/FFlagsService';
import AssistStatsService from './AssistStatsService';
import UxtestingService from './UxtestingService';
import TagWatchService from 'App/services/TagWatchService';
export const dashboardService = new DashboardService();
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
export const sessionService = new SessionSerivce();
export const sessionService = new SessionService();
export const userService = new UserService();
export const funnelService = new FunnelService();
export const auditService = new AuditService();
@ -36,6 +37,8 @@ export const assistStatsService = new AssistStatsService();
export const uxtestingService = new UxtestingService();
export const tagWatchService = new TagWatchService();
export const services = [
dashboardService,
metricService,
@ -52,5 +55,6 @@ export const services = [
healthService,
fflagsService,
assistStatsService,
uxtestingService
]
uxtestingService,
tagWatchService,
];

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g clip-path="url(#clip0_322_2510)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.95801 6.66413C1.96504 7.49187 2.2347 8.29603 2.72816 8.96069C2.73328 8.96759 2.73825 8.97457 2.74304 8.98161C2.78908 9.01786 2.83075 9.06126 2.86636 9.1115C3.15601 9.52018 3.70303 10.0709 4.39747 10.5164C5.09214 10.9619 5.88124 11.2684 6.65956 11.2684C7.00474 11.2684 7.28456 11.5482 7.28456 11.8934C7.28456 12.2386 7.00474 12.5184 6.65956 12.5184C5.56421 12.5184 4.54129 12.0937 3.72259 11.5685C2.90365 11.0432 2.23594 10.3837 1.84653 9.8343L1.83975 9.82454C1.79721 9.79103 1.75836 9.75139 1.72452 9.70581C1.07259 8.82771 0.716628 7.76512 0.708027 6.6715L0.708008 6.66659C0.708008 3.25163 4.11207 0.708252 7.99967 0.708252C11.8873 0.708252 15.2913 3.25163 15.2913 6.66659V6.6804L15.2912 6.68039C15.2799 7.19201 15.1962 7.69945 15.0425 8.18758C14.9389 8.51683 14.5879 8.69972 14.2587 8.59608C13.9294 8.49244 13.7465 8.14151 13.8502 7.81226C13.9677 7.43895 14.032 7.05096 14.0413 6.65972C14.0366 4.18409 11.4689 1.95825 7.99967 1.95825C4.52843 1.95825 1.9597 4.18674 1.95801 6.66413ZM9.95801 13.1206V8.77774L12.9688 11.8542L11.651 11.4139C11.3837 11.3246 11.0897 11.4253 10.9332 11.6597L9.95801 13.1206ZM10.0931 7.13325C9.95984 7.02219 9.77556 6.93506 9.55301 6.93506L9.53463 6.93533C9.31312 6.94185 9.10288 7.03444 8.94853 7.19346C8.79426 7.35242 8.70798 7.56522 8.70801 7.78673V7.787V14.4534C8.70801 14.4607 8.70814 14.4681 8.7084 14.4754C8.71458 14.6507 8.7743 14.8199 8.87953 14.9602C8.98476 15.1005 9.13044 15.2052 9.29697 15.2602C9.36022 15.2811 9.4264 15.2917 9.49301 15.2917H9.55301C9.68626 15.2917 9.81768 15.2607 9.93687 15.2011C10.056 15.1415 10.1597 15.055 10.2397 14.9484C10.2465 14.9392 10.2531 14.9299 10.2595 14.9204L11.7078 12.7508L14.2483 13.5995C14.308 13.6195 14.3704 13.6303 14.4333 13.6316C14.6107 13.6353 14.7848 13.583 14.9308 13.4823C15.0769 13.3815 15.1875 13.2373 15.247 13.0701C15.3065 12.903 15.3119 12.7213 15.2623 12.5509C15.2179 12.3982 15.1315 12.2615 15.0134 12.1561L10.1397 7.17624C10.1249 7.16113 10.1094 7.14679 10.0931 7.13325Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_322_2510">
<rect width="16" height="16" fill="none"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,7 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="mouse-pointer-click">
<path id="Vector" d="M5.25 5.25L8.16667 12.25L9.2015 9.2015L12.25 8.16667L5.25 5.25Z" fill="black" fill-opacity="0.85" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M9.37451 9.37475L11.8496 11.8498" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M4.19316 1.30608L4.64641 2.996M2.99616 4.64625L1.30566 4.193M8.13766 2.3625L6.89983 3.60033M3.59991 6.89967L2.36325 8.1375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 709 B

View file

@ -0,0 +1,5 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="UserSwitch">
<path id="Vector" d="M11.5259 4.23058C11.5259 2.08995 9.79154 0.355577 7.65092 0.355577C5.51029 0.355577 3.77592 2.08995 3.77592 4.23058C3.77592 5.52433 4.41029 6.6712 5.38529 7.37433C5.37904 7.37745 5.37436 7.37901 5.37123 7.38058C4.67279 7.67589 4.04623 8.09933 3.50717 8.63995C2.97122 9.17493 2.54453 9.80912 2.25092 10.5071C1.96203 11.1907 1.80612 11.923 1.79154 12.665C1.79113 12.6816 1.79405 12.6982 1.80015 12.7137C1.80624 12.7293 1.81538 12.7434 1.82703 12.7554C1.83868 12.7673 1.8526 12.7768 1.86797 12.7833C1.88335 12.7897 1.89986 12.7931 1.91654 12.7931H2.85248C2.91967 12.7931 2.97592 12.7384 2.97748 12.6712C3.00873 11.465 3.49154 10.3353 4.34623 9.47902C5.22904 8.59308 6.40092 8.10558 7.65092 8.10558C9.79154 8.10558 11.5259 6.3712 11.5259 4.23058ZM7.65092 6.91808C6.16654 6.91808 4.96342 5.71495 4.96342 4.23058C4.96342 2.7462 6.16654 1.54308 7.65092 1.54308C9.13529 1.54308 10.3384 2.7462 10.3384 4.23058C10.3384 5.71495 9.13529 6.91808 7.65092 6.91808ZM9.29154 10.3712H13.4165C13.4853 10.3712 13.5415 10.315 13.5415 10.2462V9.3712C13.5415 9.30245 13.4853 9.2462 13.4165 9.2462H10.6587L11.3962 8.30714C11.4132 8.28522 11.4226 8.25832 11.4228 8.23058C11.4228 8.16183 11.3665 8.10558 11.2978 8.10558H10.1634C10.0869 8.10558 10.015 8.14151 9.96654 8.20089L8.89623 9.56183C8.82748 9.64933 8.78998 9.7587 8.78998 9.8712C8.79154 10.1478 9.01498 10.3712 9.29154 10.3712ZM13.0415 11.3712H8.91654C8.84779 11.3712 8.79154 11.4275 8.79154 11.4962V12.3712C8.79154 12.44 8.84779 12.4962 8.91654 12.4962H11.6744L10.9369 13.4353C10.9198 13.4572 10.9105 13.4841 10.9103 13.5118C10.9103 13.5806 10.9665 13.6368 11.0353 13.6368H12.1697C12.2462 13.6368 12.3181 13.6009 12.3665 13.5415L13.4369 12.1806C13.5056 12.0931 13.5431 11.9837 13.5431 11.8712C13.5415 11.5946 13.3181 11.3712 13.0415 11.3712Z" fill="black" fill-opacity="0.85"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -64,6 +64,8 @@ export const setQueryParamKeyFromFilterkey = (filterKey: string) => {
return 'duration';
case FilterKey.FEATURE_FLAG:
return 'feature_flag';
case FilterKey.TAGGED_ELEMENT:
return 'tnw'
}
};
@ -149,6 +151,8 @@ export const getFilterKeyTypeByKey = (key: string) => {
return FilterKey.DURATION;
case FilterKey.FEATURE_FLAG:
return 'feature_flag';
case 'tnw':
return FilterKey.TAGGED_ELEMENT
}
};
@ -306,4 +310,5 @@ export enum FilterKey {
CLICKMAP_URL = 'clickMapUrl',
FEATURE_FLAG = 'featureFlag',
TAGGED_ELEMENT = 'tag',
}

View file

@ -262,6 +262,17 @@ export const filters = [
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
icon: 'filters/duration'
},
{
key: FilterKey.TAGGED_ELEMENT,
type: FilterType.MULTIPLE_DROPDOWN,
category: FilterCategory.RECORDING_ATTRIBUTES,
label: 'Tagged Element',
operator: 'is',
isEvent: true,
icon: 'filters/tag-element',
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
options: [],
},
{
key: FilterKey.USER_COUNTRY,
type: FilterType.MULTIPLE_DROPDOWN,
@ -721,6 +732,15 @@ export const addElementToFiltersMap = (
};
};
export const addOptionsToFilter = (
key,
options,
) => {
if (filtersMap[key] && filtersMap[key].options) {
filtersMap[key].options = options
}
}
export const addElementToFlagConditionsMap = (
category = FilterCategory.METADATA,
key,
@ -830,7 +850,11 @@ export default Record({
if (type === FilterKey.METADATA) {
_filter = filtersMap[filter.source];
} else {
_filter = filtersMap[type];
if (filtersMap[filter.key]) {
_filter = filtersMap[filter.key]
} else {
_filter = filtersMap[type];
}
}
}

View file

@ -48,18 +48,18 @@ export const getFiltersFromQuery = (search: string, filter: any) => {
return;
}
const entires = getQueryObject(search);
const period: any = getPeriodFromEntries(entires);
const filters = getFiltersFromEntries(entires);
const entries = getQueryObject(search);
const period: any = getPeriodFromEntries(entries);
const filters = getFiltersFromEntries(entries);
return Filter({ filters, rangeValue: period.rangeName, startDate: period.start, endDate: period.end });
};
const getFiltersFromEntries = (entires: any) => {
const getFiltersFromEntries = (entries: any) => {
const _filters: any = { ...filtersMap };
const filters: any = [];
if (entires.length > 0) {
entires.forEach((item: any) => {
if (entries.length > 0) {
entries.forEach((item: any) => {
if (!item.key || !item.value) {
return;
}
@ -72,29 +72,33 @@ const getFiltersFromEntries = (entires: any) => {
const sourceArr = tmp[1] ? tmp[1].split('|') : [];
const sourceOperator = sourceArr.shift();
if (filterKey) {
filter.type = filterKey;
filter.key = filterKey;
if (filterKey && _filters[filterKey]) {
filter = _filters[filterKey];
filter.value = valueArr;
} else {
filter = _filters[item.key];
if (!!filter) {
filter.type = filter.key;
filter.key = filter.key;
if (filterKey) {
filter.type = filterKey;
filter.key = filterKey;
} else {
filter = _filters[item.key];
if (!!filter) {
filter.type = filter.key;
}
}
}
if (!filter) {
return;
}
if (!filter) {
return;
}
filter.value = valueArr;
filter.operator = operator;
if (filter.icon === 'filters/metadata') {
filter.source = filter.type;
filter.type = 'MULTIPLE';
} else {
filter.source = sourceArr && sourceArr.length > 0 ? sourceArr : null;
filter.sourceOperator = !!sourceOperator ? decodeURI(sourceOperator) : null;
filter.value = valueArr;
filter.operator = operator;
if (filter.icon === 'filters/metadata') {
filter.source = filter.type;
filter.type = 'MULTIPLE';
} else {
filter.source = sourceArr && sourceArr.length > 0 ? sourceArr : null;
filter.sourceOperator = !!sourceOperator ? decodeURI(sourceOperator) : null;
}
}
if (!filter.filters || filter.filters.size === 0) {
@ -106,15 +110,15 @@ const getFiltersFromEntries = (entires: any) => {
return filters;
};
const getPeriodFromEntries = (entires: any) => {
const rangeFilter = entires.find(({ key }: any) => key === 'range');
const getPeriodFromEntries = (entries: any) => {
const rangeFilter = entries.find(({ key }: any) => key === 'range');
if (!rangeFilter) {
return Period();
}
if (rangeFilter.value === CUSTOM_RANGE) {
const start = entires.find(({ key }: any) => key === 'rStart').value;
const end = entires.find(({ key }: any) => key === 'rEnd').value;
const start = entries.find(({ key }: any) => key === 'rStart').value;
const end = entries.find(({ key }: any) => key === 'rEnd').value;
return Period({ rangeName: rangeFilter.value, start, end });
}

View file

@ -27,6 +27,7 @@
"@ant-design/icons": "^5.2.5",
"@babel/plugin-transform-private-methods": "^7.23.3",
"@floating-ui/react-dom-interactions": "^0.10.3",
"@medv/finder": "^3.1.0",
"@sentry/browser": "^5.21.1",
"@svg-maps/world": "^1.0.1",
"@svgr/webpack": "^6.2.1",

View file

@ -112,11 +112,11 @@ ${iconPaths.map(icon => ` ${titleCase(icon.fileName)}`).join(',\n')}
} from './Icons'
// export type IconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4).replaceAll('-', '_') + '\'').join(' | ')};
export type OldIconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4) + '\'').join(' | ')};
// export type NewIconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4).replaceAll('-', '_') + '\'').join(' | ')};
export type IconNames = ${icons.map((icon) => '\'' + icon.slice(0, -4) + '\'').join(' | ')};
interface Props {
name: OldIconNames;
name: IconNames;
size?: number | string;
width?: number | string;
height?: number | string;

View file

@ -2762,6 +2762,13 @@ __metadata:
languageName: node
linkType: hard
"@medv/finder@npm:^3.1.0":
version: 3.1.0
resolution: "@medv/finder@npm:3.1.0"
checksum: 43f8f5af50d51e2609a26b7f1306cf0b170967df9aa0169d70319215957d47066c35d888d8d049d47384639e28c1bba96bc9c09558bb7368ea43c8b0b6e03836
languageName: node
linkType: hard
"@mrmlnc/readdir-enhanced@npm:^2.2.1":
version: 2.2.1
resolution: "@mrmlnc/readdir-enhanced@npm:2.2.1"
@ -18091,6 +18098,7 @@ __metadata:
"@babel/runtime": ^7.23.2
"@floating-ui/react-dom-interactions": ^0.10.3
"@jest/globals": ^29.7.0
"@medv/finder": ^3.1.0
"@openreplay/sourcemap-uploader": ^3.0.8
"@sentry/browser": ^5.21.1
"@storybook/addon-actions": ^6.5.12

View file

@ -518,6 +518,10 @@ message 119, 'CanvasNode' do
uint 'Timestamp'
end
message 120, 'TagTrigger', :replayer => :devtools do
int 'TagId'
end
## Backend-only
message 125, 'IssueEvent', :replayer => false, :tracker => false do
uint 'MessageID'

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "12.0.0",
"version": "12.0.1-2",
"keywords": [
"logging",
"replay"

View file

@ -73,6 +73,7 @@ export declare const enum Type {
TabChange = 117,
TabData = 118,
CanvasNode = 119,
TagTrigger = 120,
}
@ -580,6 +581,11 @@ export type CanvasNode = [
/*timestamp:*/ number,
]
export type TagTrigger = [
/*type:*/ Type.TagTrigger,
/*tagId:*/ number,
]
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger
export default Message

View file

@ -1,6 +1,6 @@
import ConditionsManager from '../modules/conditionsManager.js'
import FeatureFlags from '../modules/featureFlags.js'
import type Message from './messages.gen.js'
import Message, { TagTrigger } from './messages.gen.js'
import {
Timestamp,
Metadata,
@ -34,6 +34,7 @@ import type { Options as SessOptions } from './session.js'
import type { Options as NetworkOptions } from '../modules/network.js'
import CanvasRecorder from './canvas.js'
import UserTestManager from '../modules/userTesting/index.js'
import TagWatcher from '../modules/tagWatcher.js'
import type {
Options as WebworkerOptions,
@ -179,6 +180,7 @@ export default class App {
private uxtManager: UserTestManager
private conditionsManager: ConditionsManager | null = null
public featureFlags: FeatureFlags
private tagWatcher: TagWatcher
constructor(
projectKey: string,
@ -230,6 +232,9 @@ export default class App {
this.session = new Session(this, this.options)
this.attributeSender = new AttributeSender(this, Boolean(this.options.disableStringDict))
this.featureFlags = new FeatureFlags(this)
this.tagWatcher = new TagWatcher(this.sessionStorage, this.debug.error, (tag) => {
this.send(TagTrigger(tag) as Message)
})
this.session.attachUpdateCallback(({ userID, metadata }) => {
if (userID != null) {
// TODO: nullable userID
@ -687,6 +692,7 @@ export default class App {
this.startCallbacks.forEach((cb) => cb(onStartInfo))
await this.conditionsManager?.fetchConditions(projectID as string, token as string)
await this.featureFlags.reloadFlags(token as string)
await this.tagWatcher.fetchTags(this.options.ingestPoint, token as string)
this.conditionsManager?.processFlags(this.featureFlags.flags)
}
const cycle = () => {
@ -896,7 +902,6 @@ export default class App {
) {
const reason =
'OpenReplay: trying to call `start()` on the instance that has been started already.'
this.signalError(reason, [])
return Promise.resolve(UnsuccessfulStart(reason))
}
this.activityState = ActivityState.Starting
@ -1049,8 +1054,10 @@ export default class App {
this.compressionThreshold = compressionThreshold
const onStartInfo = { sessionToken: token, userUUID, sessionID }
// TODO: start as early as possible (before receiving the token)
/** after start */
this.startCallbacks.forEach((cb) => cb(onStartInfo)) // MBTODO: callbacks after DOM "mounted" (observed)
void this.featureFlags.reloadFlags()
await this.tagWatcher.fetchTags(this.options.ingestPoint, token)
this.activityState = ActivityState.Active
if (canvasEnabled) {
@ -1234,6 +1241,7 @@ export default class App {
this.ticker.stop()
this.stopCallbacks.forEach((cb) => cb())
this.debug.log('OpenReplay tracking stopped.')
this.tagWatcher.clear()
if (this.worker && stopWorker) {
this.worker.postMessage('stop')
}

View file

@ -942,3 +942,12 @@ export function CanvasNode(
]
}
export function TagTrigger(
tagId: number,
): Messages.TagTrigger {
return [
Messages.Type.TagTrigger,
tagId,
]
}

View file

@ -0,0 +1,83 @@
export const WATCHED_TAGS_KEY = '__or__watched_tags__'
class TagWatcher {
intervals: Record<string, ReturnType<typeof setInterval>> = {}
tags: { id: number; selector: string }[] = []
observer: IntersectionObserver
constructor(
private readonly sessionStorage: Storage,
private readonly errLog: (args: any[]) => void,
private readonly onTag: (tag: number) => void,
) {
const tags: { id: number; selector: string }[] = JSON.parse(
sessionStorage.getItem(WATCHED_TAGS_KEY) ?? '[]',
)
this.setTags(tags)
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (entry.target) {
// @ts-ignore
const tag = entry.target.__or_watcher_tagname as number
if (tag) {
this.onTagRendered(tag)
}
this.observer.unobserve(entry.target)
}
}
})
})
}
async fetchTags(ingest: string, token: string) {
return fetch(`${ingest}/v1/web/tags`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((r) => r.json())
.then(({ tags }: { tags: { id: number; selector: string }[] }) => {
if (tags && tags.length) {
this.setTags(tags)
this.sessionStorage.setItem(WATCHED_TAGS_KEY, JSON.stringify(tags) || '')
}
})
.catch((e) => this.errLog(e))
}
setTags(tags: { id: number; selector: string }[]) {
this.tags = tags
this.intervals = {}
tags.forEach((tag) => {
this.intervals[tag.id] = setInterval(() => {
const possibleEls = document.querySelectorAll(tag.selector)
if (possibleEls.length > 0) {
const el = possibleEls[0]
// @ts-ignore
el.__or_watcher_tagname = tag.id
this.observer.observe(el)
}
}, 500)
})
}
onTagRendered(tagId: number) {
if (this.intervals[tagId]) {
clearInterval(this.intervals[tagId])
}
this.onTag(tagId)
}
clear() {
this.tags.forEach((tag) => {
clearInterval(this.intervals[tag.id])
})
this.tags = []
this.intervals = {}
this.observer.disconnect()
}
}
export default TagWatcher

View file

@ -0,0 +1,103 @@
import TagWatcher, { WATCHED_TAGS_KEY } from '../main/modules/TagWatcher'
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
describe('TagWatcher', () => {
let sessionStorageMock: Storage
let errLogMock: (args: any[]) => void
const onTag = jest.fn()
let mockObserve: Function
let mockUnobserve: Function
let mockDisconnect: Function
beforeEach(() => {
sessionStorageMock = {
// @ts-ignore
getItem: jest.fn(),
setItem: jest.fn(),
}
errLogMock = jest.fn()
mockObserve = jest.fn()
mockUnobserve = jest.fn()
mockDisconnect = jest.fn()
// @ts-ignore
global.IntersectionObserver = jest.fn((callback) => ({
observe: mockObserve,
unobserve: mockUnobserve,
disconnect: mockDisconnect,
callback,
}))
jest.useFakeTimers()
// @ts-ignore
global.document.querySelectorAll = jest.fn()
})
afterEach(() => {
jest.restoreAllMocks()
jest.useRealTimers()
})
function triggerIntersection(elements: any, isIntersecting: boolean, observer: any) {
const entries = elements.map((el: any) => ({
isIntersecting,
target: el,
}))
// @ts-ignore
observer.callback(entries)
}
test('constructor initializes with tags from sessionStorage', () => {
// @ts-ignore
sessionStorageMock.getItem.mockReturnValue('div,span')
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
expect(watcher.tags).toEqual(['div', 'span'])
expect(watcher.intervals).toHaveProperty('div')
expect(watcher.intervals).toHaveProperty('span')
})
test('fetchTags sets tags and updates sessionStorage', async () => {
// @ts-ignore
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(['div', 'span', 'p']),
}),
)
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
await watcher.fetchTags('https://localhost.com', '123')
expect(watcher.tags).toEqual(['div', 'span', 'p'])
expect(sessionStorageMock.setItem).toHaveBeenCalledWith(WATCHED_TAGS_KEY, 'div,span,p')
})
test('setTags sets intervals for each tag', () => {
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
watcher.setTags([
{ id: 1, selector: 'div' },
{ id: 2, selector: 'p' },
])
expect(watcher.intervals).toHaveProperty('div')
expect(watcher.intervals).toHaveProperty('p')
expect(mockObserve).not.toHaveBeenCalled() // No elements to observe initially
})
test('onTagRendered sends messages', () => {
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
watcher.setTags([{ id: 1, selector: 'div' }])
// @ts-ignore
document.querySelectorAll.mockReturnValue([{ __or_watcher_tagname: 'div' }]) // Mock a found element
jest.advanceTimersByTime(1000)
triggerIntersection([{ __or_watcher_tagname: 'div' }], true, watcher.observer)
expect(onTag).toHaveBeenCalled()
expect(watcher.observer.unobserve).toHaveBeenCalled()
})
test('clear method clears all intervals and resets tags', () => {
const watcher = new TagWatcher(sessionStorageMock, errLogMock, onTag)
watcher.setTags([
{ id: 1, selector: 'div' },
{ id: 2, selector: 'p' },
])
watcher.clear()
expect(watcher.tags).toEqual([])
expect(watcher.intervals).toEqual({})
expect(watcher.observer.disconnect).toHaveBeenCalled()
})
})

View file

@ -294,6 +294,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.string(msg[1]) && this.uint(msg[2])
break
case Messages.Type.TagTrigger:
return this.int(msg[1])
break
}
}