);
}
diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
index 4b27e4a93..6cebb0d94 100644
--- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
+++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx
@@ -37,6 +37,7 @@ function FilterItem(props: Props) {
};
const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => {
+ console.log(subFilter, subFilterIndex)
props.onUpdate({
...filter,
filters: filter.filters.map((i: any, index: any) => {
@@ -48,6 +49,7 @@ function FilterItem(props: Props) {
});
};
+ console.log('filterItem', filter)
return (
diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx
index 7daab8acb..4a13b2c4a 100644
--- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx
+++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx
@@ -40,6 +40,7 @@ function FilterValue(props: Props) {
}
return _;
});
+ console.log(item ,{ ...filter, value: newValues });
props.onUpdate({ ...filter, value: newValues });
};
diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts
index 64378e557..78626f8c1 100644
--- a/frontend/app/constants/card.ts
+++ b/frontend/app/constants/card.ts
@@ -12,7 +12,7 @@ export interface CardType {
export const LIBRARY = 'library';
export const TIMESERIES = 'timeseries';
export const TABLE = 'table';
-export const CLICKMAP = 'clickmap'
+export const CLICKMAP = 'clickMap'
export const TYPES: CardType[] = [
{
diff --git a/frontend/app/constants/filterOptions.js b/frontend/app/constants/filterOptions.js
index 224a32ae6..442717ff5 100644
--- a/frontend/app/constants/filterOptions.js
+++ b/frontend/app/constants/filterOptions.js
@@ -1,7 +1,7 @@
import { FilterKey, IssueType } from 'Types/filter/filterType';
// TODO remove text property from options
export const options = [
- { key: 'on', label: 'on', value: 'on' },
+ { key: 'on', label: 'on', value: 'on' },
{ key: 'notOn', label: 'not on', value: 'notOn' },
{ key: 'onAny', label: 'on any', value: 'onAny' },
{ key: 'is', label: 'is', value: 'is' },
@@ -13,9 +13,9 @@ export const options = [
{ key: 'contains', label: 'contains', value: 'contains' },
{ key: 'notContains', label: 'not contains', value: 'notContains' },
{ key: 'hasAnyValue', label: 'has any value', value: 'hasAnyValue' },
- { key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' },
+ { key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' },
{ key: 'isSignedUp', label: 'is signed up', value: 'isSignedUp' },
- { key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' },
+ { key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' },
{ key: 'before', label: 'before', value: 'before' },
{ key: 'after', label: 'after', value: 'after' },
{ key: 'inRage', label: 'in rage', value: 'inRage' },
@@ -37,6 +37,7 @@ const stringFilterKeysPerformance = ['is', 'inAnyPage', 'isNot', 'contains', 'st
const targetFilterKeys = ['on', 'notOn', 'onAny', 'contains', 'startsWith', 'endsWith', 'notContains'];
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
+const pageUrlFilter = ['contains', 'startsWith', 'endsWith']
const getOperatorsByKeys = (keys) => {
return options.filter(option => keys.includes(option.key));
@@ -50,6 +51,7 @@ export const booleanOperators = [
{ key: 'true', label: 'true', value: 'true' },
{ key: 'false', label: 'false', value: 'false' },
]
+export const pageUrlOperators = options.filter(({key}) => pageUrlFilter.includes(key))
export const customOperators = [
{ key: '=', label: '=', value: '=' },
@@ -86,6 +88,7 @@ export const metricOf = [
{ label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' },
{ label: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' },
{ label: 'URLs', value: FilterKey.LOCATION, type: 'table' },
+
]
export const methodOptions = [
@@ -97,7 +100,7 @@ export const methodOptions = [
{ label: 'HEAD', value: 'HEAD' },
{ label: 'OPTIONS', value: 'OPTIONS' },
{ label: 'TRACE', value: 'TRACE' },
- { label: 'CONNECT', value: 'CONNECT' },
+ { label: 'CONNECT', value: 'CONNECT' },
]
export const issueOptions = [
@@ -128,4 +131,5 @@ export default {
metricOf,
issueOptions,
methodOptions,
+ pageUrlOperators,
}
diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts
index e548f406e..bfb127247 100644
--- a/frontend/app/player/web/MessageManager.ts
+++ b/frontend/app/player/web/MessageManager.ts
@@ -207,6 +207,7 @@ export default class MessageManager {
this.waitingForFiles = false
this.setMessagesLoading(false)
+ // this.state.update({ filesLoaded: true })
}
private async loadMessages() {
diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts
index 8925e387e..f10a3bd35 100644
--- a/frontend/app/player/web/Screen/Screen.ts
+++ b/frontend/app/player/web/Screen/Screen.ts
@@ -217,10 +217,10 @@ export default class Screen {
scaleFullPage() {
const { height, width } = this.document.body.getBoundingClientRect();
+ this.cursor.toggle(false)
const offsetHeight = this.parentElement.getBoundingClientRect().height
if (!this.parentElement) return;
- console.log(height, width)
this.scaleRatio = 1
this.screen.style.transform = `scale(1) translate(-50%, -50%)`;
this.screen.style.overflow = 'scroll';
diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts
index fc0534e2a..7a7c84cb8 100644
--- a/frontend/app/player/web/WebPlayer.ts
+++ b/frontend/app/player/web/WebPlayer.ts
@@ -122,6 +122,11 @@ export default class WebPlayer extends Player {
this.targetMarker.markTargets(...args)
}
+ showClickmap = (...args: Parameters) => {
+ this.pause()
+ this.targetMarker.injectTargets(...args)
+ }
+
// TODO separate message receivers
toggleTimetravel = async () => {
diff --git a/frontend/app/player/web/addons/TargetMarker.ts b/frontend/app/player/web/addons/TargetMarker.ts
index a21e0b089..028858f6d 100644
--- a/frontend/app/player/web/addons/TargetMarker.ts
+++ b/frontend/app/player/web/addons/TargetMarker.ts
@@ -1,7 +1,7 @@
import type Screen from '../Screen/Screen'
import type { Point } from '../Screen/types'
import type { Store } from '../../common/types'
-
+import { clickmapStyles } from './clickmapStyles'
function getOffset(el: Element, innerWindow: Window) {
const rect = el.getBoundingClientRect();
@@ -37,6 +37,8 @@ export interface State {
export default class TargetMarker {
private clickMapOverlay: HTMLDivElement
+ private clickContainers: HTMLDivElement[] = []
+ private smallClicks: HTMLDivElement[] = []
static INITIAL_STATE: State = {
markedTargets: null,
activeTargetIndex: 0
@@ -103,41 +105,16 @@ export default class TargetMarker {
private actualScroll: Point | null = null
markTargets(selections: { selector: string, count: number }[] | null) {
-
if (selections) {
const totalCount = selections.reduce((a, b) => {
return a + b.count
}, 0);
const markedTargets: MarkedTarget[] = [];
let index = 0;
-
- const overlay = document.createElement("div")
- overlay.style.position = "absolute"
- overlay.style.top = "0px"
- overlay.style.left = "0px"
- overlay.style.width = '100%'
- overlay.style.height = "100%"
- overlay.style.background = 'rgba(0,0,0, 0.1)'
- this.screen.document.body.appendChild(overlay)
- this.clickMapOverlay = overlay
selections.forEach((s) => {
const el = this.screen.getElementBySelector(s.selector);
if (!el) return;
- const test = document.createElement("div")
- const top = el.getBoundingClientRect().top
- const left = el.getBoundingClientRect().left
- test.innerHTML = '' + s.count + 'Clicks'
- Object.assign(test.style, {
- position: 'absolute',
- top: top + 'px',
- left: left + 'px',
- padding: '10px',
- borderRadius: '12px',
- background: 'white',
- boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)',
- })
- overlay.appendChild(test)
markedTargets.push({
...s,
el,
@@ -155,10 +132,84 @@ export default class TargetMarker {
this.actualScroll = null
}
this.store.update({ markedTargets: null });
- this.clickMapOverlay.remove()
}
}
+ injectTargets(selections: { selector: string, count: number }[] | null) {
+ if (selections) {
+ const totalCount = selections.reduce((a, b) => {
+ return a + b.count
+ }, 0);
+
+ const overlay = document.createElement("div")
+ Object.assign(overlay.style, clickmapStyles.overlayStyle)
+
+ this.clickMapOverlay = overlay
+ selections.forEach((s, i) => {
+ const el = this.screen.getElementBySelector(s.selector);
+ if (!el) return;
+
+ const bubbleContainer = document.createElement("div")
+ const {top, left, width, height} = el.getBoundingClientRect()
+
+ const totalClicks = document.createElement("div")
+ totalClicks.innerHTML = `${s.count} ${s.count !== 1 ? 'Clicks' : 'Click'}`
+ Object.assign(totalClicks.style, clickmapStyles.totalClicks)
+
+ const percent = document.createElement("div")
+ percent.style.fontSize = "14px"
+ percent.innerHTML = `${Math.round((s.count * 100) / totalCount)}% of the clicks recorded in this page`
+
+ bubbleContainer.appendChild(totalClicks)
+ bubbleContainer.appendChild(percent)
+ const containerId = `clickmap-bubble-${i}`
+ bubbleContainer.id = containerId
+ this.clickContainers.push(bubbleContainer)
+ Object.assign(bubbleContainer.style, clickmapStyles.bubbleContainer({ top, left }))
+
+ const border = document.createElement("div")
+ Object.assign(border.style, clickmapStyles.highlight({ width, height, top, left }))
+
+ const smallClicksBubble = document.createElement("div")
+ smallClicksBubble.innerHTML = '' + s.count
+ const smallClicksId = containerId + '-small'
+ smallClicksBubble.id = smallClicksId
+ this.smallClicks.push(smallClicksBubble)
+
+ border.onclick = () => {
+ this.clickContainers.forEach(container => {
+ if (container.id === containerId) {
+ container.style.visibility = "visible"
+ } else {
+ container.style.visibility = "hidden"
+ }
+ })
+ this.smallClicks.forEach(container => {
+ if (container.id !== smallClicksId) {
+ container.style.visibility = "visible"
+ } else {
+ container.style.visibility = "hidden"
+ }
+ })
+ }
+
+ Object.assign(smallClicksBubble.style, clickmapStyles.clicks)
+
+ border.appendChild(smallClicksBubble)
+ overlay.appendChild(bubbleContainer)
+ overlay.appendChild(border)
+ });
+
+ this.screen.document.body.appendChild(overlay)
+ // this.store.update({ markedTargets });
+ } else {
+ this.store.update({ markedTargets: null });
+ this.clickMapOverlay?.remove()
+ this.clickMapOverlay = null
+ this.smallClicks = []
+ this.clickContainers = []
+ }
+ }
}
diff --git a/frontend/app/player/web/addons/clickmapStyles.ts b/frontend/app/player/web/addons/clickmapStyles.ts
new file mode 100644
index 000000000..086260089
--- /dev/null
+++ b/frontend/app/player/web/addons/clickmapStyles.ts
@@ -0,0 +1,58 @@
+export const clickmapStyles = {
+ overlayStyle: {
+ position: 'absolute',
+ top: '0px',
+ left: '0px',
+ width: '100%',
+ height: '100%',
+ background: 'rgba(0,0,0, 0.15)',
+ zIndex: 9 * 10e3,
+ // pointerEvents: 'none',
+ },
+ totalClicks: {
+ fontSize: '16px',
+ fontWeight: '600',
+ },
+ bubbleContainer: ({ top, left }: { top: number; left: number }) => ({
+ position: 'absolute',
+ top: top + 'px',
+ left: left + 'px',
+ padding: '10px',
+ borderRadius: '6px',
+ background: 'white',
+ border: '1px solid rgba(0, 0, 0, 0.12)',
+ boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)',
+ transform: `translate(-25%, -110%)`,
+ textAlign: 'center',
+ visibility: 'hidden',
+ }),
+ highlight: ({
+ width,
+ height,
+ top,
+ left,
+ }: {
+ width: number;
+ height: number;
+ top: number;
+ left: number;
+ }) => ({
+ width: width + 'px',
+ height: height + 'px',
+ border: '2px dotted red',
+ cursor: 'pointer',
+ top: top + 'px',
+ left: left + 'px',
+ position: 'absolute',
+ }),
+ clicks: {
+ top: 0,
+ left: 0,
+ position: 'absolute',
+ borderRadius: '999px',
+ padding: '6px',
+ background: 'white',
+ lineHeight: '0.5',
+ transform: 'translate(-70%, -70%)',
+ },
+};
diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js
index 8abd3f5fc..2e5410f50 100644
--- a/frontend/app/types/filter/newFilter.js
+++ b/frontend/app/types/filter/newFilter.js
@@ -29,7 +29,7 @@ export const filters = [
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'State Action', placeholder: 'E.g. 12', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error Message', placeholder: 'E.g. Uncaught SyntaxError', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
// { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
-
+
// FILTERS
{ key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
{ key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
@@ -42,7 +42,7 @@ export const filters = [
// { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', placeholder: 'E.g. Alex, or alex@domain.com, or EMP123', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
-
+
// PERFORMANCE
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
@@ -53,6 +53,12 @@ export const filters = [
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', placeholder: 'Select an issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
];
+export const clickmapFilter = {
+ key: FilterKey.LOCATION,
+ type: FilterType.MULTIPLE,
+ category: FilterCategory.INTERACTIONS,
+ label: '', placeholder: 'Enter URL or path to select', operator: 'is', operatorOptions: filterOptions.pageUrlOperators, icon: 'filters/location', isEvent: true }
+
const mapFilters = (list) => {
return list.reduce((acc, filter) => {
acc[filter.key] = filter;
@@ -97,12 +103,12 @@ export const clearMetaFilters = () => {
/**
* Add a new filter to the filter list
- * @param {*} category
- * @param {*} key
- * @param {*} type
- * @param {*} operator
- * @param {*} operatorOptions
- * @param {*} icon
+ * @param {*} category
+ * @param {*} key
+ * @param {*} type
+ * @param {*} operator
+ * @param {*} operatorOptions
+ * @param {*} icon
*/
export const addElementToFiltersMap = (
category = FilterCategory.METADATA,
@@ -143,15 +149,15 @@ export default Record({
value: [""],
source: [""],
category: '',
-
+
custom: '',
// target: Target(),
level: '',
-
+
hasNoValue: false,
isFilter: false,
actualValue: '',
-
+
hasSource: false,
source: [""],
sourceType: '',
@@ -161,7 +167,7 @@ export default Record({
sourceOperatorOptions: [],
operator: '',
- operatorOptions: [],
+ operatorOptions: [],
operatorDisabled: false,
isEvent: false,
index: 0,
@@ -199,8 +205,8 @@ export default Record({
/**
* Group filters by category
- * @param {*} filtersMap
- * @returns
+ * @param {*} filtersMap
+ * @returns
*/
export const generateFilterOptions = (map) => {
const filterSection = {};
@@ -229,4 +235,4 @@ export const generateLiveFilterOptions = (map) => {
}
});
return filterSection;
-}
\ No newline at end of file
+}