From 4defd710f73ce41f43b7b9f5a0212e5fb52282b4 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Mon, 12 Dec 2022 17:16:05 +0100 Subject: [PATCH 01/18] fix(frontend): timeline dnd layer types + remove redundant --- .../Session_/Player/Controls/Timeline.tsx | 16 +- .../Controls/components/CustomDragLayer.tsx | 143 ++++++++---------- 2 files changed, 72 insertions(+), 87 deletions(-) diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx index 686cf85b4..32e6d35eb 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx +++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx @@ -5,14 +5,13 @@ import TimeTracker from './TimeTracker'; import stl from './timeline.module.css'; import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions'; import DraggableCircle from './components/DraggableCircle'; -import CustomDragLayer from './components/CustomDragLayer'; +import CustomDragLayer, { OnDragCallback } from './components/CustomDragLayer'; import { debounce } from 'App/utils'; import TooltipContainer from './components/TooltipContainer'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; -const BOUNDRY = 0; function getTimelinePosition(value: number, scale: number) { const pos = value * scale; @@ -38,8 +37,8 @@ function Timeline(props) { } = store.get() const notes = notesStore.sessionNotes - const progressRef = useRef() - const timelineRef = useRef() + const progressRef = useRef() + const timelineRef = useRef() const scale = 100 / endTime; @@ -64,10 +63,10 @@ function Timeline(props) { } }; - const onDrag = (offset) => { + const onDrag: OnDragCallback = (offset) => { if (live && !liveTimeTravel) return; - const p = (offset.x - BOUNDRY) / progressRef.current.offsetWidth; + const p = (offset.x) / progressRef.current.offsetWidth; const time = Math.max(Math.round(p * endTime), 0); debouncedJump(time); hideTimeTooltip(); @@ -154,7 +153,6 @@ function Timeline(props) { style={{ top: '-4px', zIndex: 100, - padding: `0 ${BOUNDRY}px`, maxWidth: 'calc(100% - 1rem)', left: '0.5rem', }} @@ -177,8 +175,8 @@ function Timeline(props) { /> diff --git a/frontend/app/components/Session_/Player/Controls/components/CustomDragLayer.tsx b/frontend/app/components/Session_/Player/Controls/components/CustomDragLayer.tsx index eb93203b5..c106b14dc 100644 --- a/frontend/app/components/Session_/Player/Controls/components/CustomDragLayer.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/CustomDragLayer.tsx @@ -1,98 +1,85 @@ -import React, { memo } from 'react'; -import { useDragLayer } from "react-dnd"; -import Circle from './Circle' +import React, { memo, useEffect } from 'react'; import type { CSSProperties, FC } from 'react' +import { useDragLayer, XYCoord } from "react-dnd"; +import Circle from './Circle' const layerStyles: CSSProperties = { - position: "fixed", - pointerEvents: "none", - zIndex: 100, - left: 0, - top: 0, - width: "100%", - height: "100%" - }; - -const ItemTypes = { - BOX: 'box', + position: "fixed", + pointerEvents: "none", + zIndex: 100, + left: 0, + top: 0, + width: "100%", + height: "100%" } -function getItemStyles(initialOffset, currentOffset, maxX, minX) { - if (!initialOffset || !currentOffset) { - return { - display: "none" - }; - } - let { x, y } = currentOffset; - // if (isSnapToGrid) { - // x -= initialOffset.x; - // y -= initialOffset.y; - // [x, y] = [x, y]; - // x += initialOffset.x; - // y += initialOffset.y; - // } - if (x > maxX) { - x = maxX; - } - if (x < minX) { - x = minX; - } - const transform = `translate(${x}px, ${initialOffset.y}px)`; +function getItemStyles( + initialOffset: XYCoord | null, + currentOffset: XYCoord | null, + maxX: number, + minX: number, +) { + if (!initialOffset || !currentOffset) { return { - transition: 'transform 0.1s ease-out', - transform, - WebkitTransform: transform - }; + display: "none" + } + } + let { x, y } = currentOffset; + if (x > maxX) { + x = maxX; + } + + if (x < minX) { + x = minX; + } + const transform = `translate(${x}px, ${initialOffset.y}px)`; + return { + transition: 'transform 0.1s ease-out', + transform, + WebkitTransform: transform + } } +export type OnDragCallback = (offset: XYCoord) => void + interface Props { - onDrag: (offset: { x: number, y: number } | null) => void; - maxX: number; - minX: number; + onDrag: OnDragCallback + maxX: number + minX: number } -const CustomDragLayer: FC = memo(function CustomDragLayer(props) { - const { - itemType, - isDragging, - item, - initialOffset, - currentOffset, - } = useDragLayer((monitor) => ({ - item: monitor.getItem(), - itemType: monitor.getItemType(), - initialOffset: monitor.getInitialSourceClientOffset(), - currentOffset: monitor.getSourceClientOffset(), - isDragging: monitor.isDragging(), - })); +const CustomDragLayer: FC = memo(function CustomDragLayer({ maxX, minX, onDrag }) { + const { + isDragging, + initialOffset, + currentOffset, // might be null (why is it not captured by types?) + } = useDragLayer((monitor) => ({ + initialOffset: monitor.getInitialSourceClientOffset(), + currentOffset: monitor.getSourceClientOffset(), + isDragging: monitor.isDragging(), + })) - function renderItem() { - switch (itemType) { - case ItemTypes.BOX: - return ; - default: - return null; - } - } - - if (!isDragging) { - return null; + useEffect(() => { + if (!isDragging || !currentOffset) { + return } + onDrag(currentOffset) + }, [isDragging, currentOffset]) - if (isDragging) { - props.onDrag(currentOffset) - } + if (!isDragging || !currentOffset) { + return null; + } - return ( -
-
- {renderItem()} -
+ return ( +
+
+
- ); +
+ ) }) export default CustomDragLayer; From 0643b8b929fa71b2122d81638dc835ef89cd0dbd Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Mon, 12 Dec 2022 17:17:58 +0100 Subject: [PATCH 02/18] refactor(frontend):small code fix --- frontend/app/hooks/useToggle.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/app/hooks/useToggle.ts b/frontend/app/hooks/useToggle.ts index d4e820388..426e19b41 100644 --- a/frontend/app/hooks/useToggle.ts +++ b/frontend/app/hooks/useToggle.ts @@ -1,9 +1,9 @@ -import { useState, useCallback } from 'react'; +import { useState } from 'react'; export default function useToggle(defaultValue: boolean = false): [ boolean, () => void, () => void, () => void ] { const [ value, setValue ] = useState(defaultValue); - const toggle = useCallback(() => setValue(d => !d), []); - const setFalse = useCallback(() => setValue(false), []); - const setTrue = useCallback(() => setValue(true), []); + const toggle = () => setValue(d => !d) + const setFalse = () => setValue(false) + const setTrue = () => setValue(true) return [ value, toggle, setFalse, setTrue ]; } \ No newline at end of file From 85c23db4f15dd73a15b51f26cde42b59e075267d Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Mon, 12 Dec 2022 17:19:59 +0100 Subject: [PATCH 03/18] feat(frontend/player):use resolver for urlBased message both in FileReader and JSONReader in live-sessions --- .../web/messages/JSONRawMessageReader.ts | 70 +------------------ .../app/player/web/messages/MFileReader.ts | 4 +- .../app/player/web/messages/MStreamReader.ts | 2 +- 3 files changed, 7 insertions(+), 69 deletions(-) diff --git a/frontend/app/player/web/messages/JSONRawMessageReader.ts b/frontend/app/player/web/messages/JSONRawMessageReader.ts index 04f622ecb..6a9cfb4f9 100644 --- a/frontend/app/player/web/messages/JSONRawMessageReader.ts +++ b/frontend/app/player/web/messages/JSONRawMessageReader.ts @@ -1,21 +1,9 @@ -import type { - RawMessage, - RawSetNodeAttributeURLBased, - RawSetNodeAttribute, - RawSetCssDataURLBased, - RawSetCssData, - RawCssInsertRuleURLBased, - RawCssInsertRule, - RawAdoptedSsInsertRuleURLBased, - RawAdoptedSsInsertRule, - RawAdoptedSsReplaceURLBased, - RawAdoptedSsReplace, -} from './raw.gen' +import type { RawMessage } from './raw.gen' import type { TrackerMessage } from './tracker.gen' import { MType } from './raw.gen' import translate from './tracker.gen' import { TP_MAP } from './tracker-legacy.gen' -import { resolveURL, resolveCSS } from './urlResolve' +import resolveURL from './urlBasedResolver' function legacyTranslate(msg: any): RawMessage | null { @@ -29,54 +17,6 @@ function legacyTranslate(msg: any): RawMessage | null { } -// TODO: commonURLBased logic for feilds -const resolvers = { - [MType.SetNodeAttributeURLBased]: (msg: RawSetNodeAttributeURLBased): RawSetNodeAttribute => - ({ - ...msg, - value: msg.name === 'src' || msg.name === 'href' - ? resolveURL(msg.baseURL, msg.value) - : (msg.name === 'style' - ? resolveCSS(msg.baseURL, msg.value) - : msg.value - ), - tp: MType.SetNodeAttribute, - }), - [MType.SetCssDataURLBased]: (msg: RawSetCssDataURLBased): RawSetCssData => - ({ - ...msg, - data: resolveCSS(msg.baseURL, msg.data), - tp: MType.SetCssData, - }), - [MType.CssInsertRuleURLBased]: (msg: RawCssInsertRuleURLBased): RawCssInsertRule => - ({ - ...msg, - rule: resolveCSS(msg.baseURL, msg.rule), - tp: MType.CssInsertRule, - }), - [MType.AdoptedSsInsertRuleURLBased]: (msg: RawAdoptedSsInsertRuleURLBased): RawAdoptedSsInsertRule => - ({ - ...msg, - rule: resolveCSS(msg.baseURL, msg.rule), - tp: MType.AdoptedSsInsertRule, - }), - [MType.AdoptedSsReplaceURLBased]: (msg: RawAdoptedSsReplaceURLBased): RawAdoptedSsReplace => - ({ - ...msg, - text: resolveCSS(msg.baseURL, msg.text), - tp: MType.AdoptedSsReplace, - }), -} as const - -type ResolvableType = keyof typeof resolvers -type ResolvableRawMessage = RawMessage & { tp: ResolvableType } - -function isResolvable(msg: RawMessage): msg is ResolvableRawMessage { - //@ts-ignore - return resolvers[msg.tp] !== undefined -} - - export default class JSONRawMessageReader { constructor(private messages: TrackerMessage[] = []){} append(messages: TrackerMessage[]) { @@ -91,11 +31,7 @@ export default class JSONRawMessageReader { if (!rawMsg) { return this.readMessage() } - if (isResolvable(rawMsg)) { - //@ts-ignore ??? too complex typscript... - return resolvers[rawMsg.tp](rawMsg) - } - return rawMsg + return resolveURL(rawMsg) } } diff --git a/frontend/app/player/web/messages/MFileReader.ts b/frontend/app/player/web/messages/MFileReader.ts index e6fad8c63..b91080b4f 100644 --- a/frontend/app/player/web/messages/MFileReader.ts +++ b/frontend/app/player/web/messages/MFileReader.ts @@ -3,6 +3,8 @@ import type { RawMessage } from './raw.gen'; import { MType } from './raw.gen'; import logger from 'App/logger'; import RawMessageReader from './RawMessageReader.gen'; +import resolveURL from './urlBasedResolver' + // TODO: composition instead of inheritance // needSkipMessage() and next() methods here use buf and p protected properties, @@ -77,7 +79,7 @@ export default class MFileReader extends RawMessageReader { } const index = this.getLastMessageID() - const msg = Object.assign(rMsg, { + const msg = Object.assign(resolveURL(rMsg), { time: this.currentTime, _index: index, }) diff --git a/frontend/app/player/web/messages/MStreamReader.ts b/frontend/app/player/web/messages/MStreamReader.ts index 6446112a3..a37e43c46 100644 --- a/frontend/app/player/web/messages/MStreamReader.ts +++ b/frontend/app/player/web/messages/MStreamReader.ts @@ -8,7 +8,7 @@ interface RawMessageReaderI { } export default class MStreamReader { - constructor(private readonly r: RawMessageReaderI = new RawMessageReader(), private startTs: number = 0){} + constructor(private readonly r: RawMessageReaderI, private startTs: number = 0){} private t: number = 0 private idx: number = 0 From b33e83e6bf2be8f2d058edad99fadd5357365efc Mon Sep 17 00:00:00 2001 From: Mehdi Osman Date: Mon, 12 Dec 2022 17:35:01 +0100 Subject: [PATCH 04/18] Update third-party.md --- third-party.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/third-party.md b/third-party.md index 186ad8817..1c003748b 100644 --- a/third-party.md +++ b/third-party.md @@ -1,4 +1,4 @@ -## Licenses (as of November 04, 2022) +## Licenses (as of December 12, 2022) Below is the list of dependencies used in OpenReplay software. Licenses may change between versions, so please keep this up to date with every new library you use. @@ -105,9 +105,8 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan | kafka | Apache2 | Infrastructure | | stern | Apache2 | Infrastructure | | k9s | Apache2 | Infrastructure | -| minio | GPLv3 | Infrastructure | +| minio | [AGPLv3](https://github.com/minio/minio/blob/master/LICENSE) | Infrastructure | | postgreSQL | PostgreSQL License | Infrastructure | -| ansible | GPLv3 | Infrastructure | | k3s | Apache2 | Infrastructure | | nginx | BSD2 | Infrastructure | | clickhouse | Apache2 | Infrastructure | From d081873ca5615e8d06c7ab54e26c9449eb3d4092 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Mon, 12 Dec 2022 17:50:59 +0100 Subject: [PATCH 05/18] fixup! feat(frontend/player):use resolver for urlBased message both in FileReader and JSONReader in live-sessions --- .../player/web/messages/urlBasedResolver.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 frontend/app/player/web/messages/urlBasedResolver.ts diff --git a/frontend/app/player/web/messages/urlBasedResolver.ts b/frontend/app/player/web/messages/urlBasedResolver.ts new file mode 100644 index 000000000..53bf1ed81 --- /dev/null +++ b/frontend/app/player/web/messages/urlBasedResolver.ts @@ -0,0 +1,69 @@ +import type { + RawMessage, + RawSetNodeAttributeURLBased, + RawSetNodeAttribute, + RawSetCssDataURLBased, + RawSetCssData, + RawCssInsertRuleURLBased, + RawCssInsertRule, + RawAdoptedSsInsertRuleURLBased, + RawAdoptedSsInsertRule, + RawAdoptedSsReplaceURLBased, + RawAdoptedSsReplace, +} from './raw.gen' +import { MType } from './raw.gen' +import { resolveURL, resolveCSS } from './urlResolve' + +// type PickMessage = Extract; +// type ResolversMap = { +// [Key in MType]: (event: PickMessage) => RawMessage +// } + +// TODO: commonURLBased logic for feilds +const resolvers = { + [MType.SetNodeAttributeURLBased]: (msg: RawSetNodeAttributeURLBased): RawSetNodeAttribute => + ({ + ...msg, + value: msg.name === 'src' || msg.name === 'href' + ? resolveURL(msg.baseURL, msg.value) + : (msg.name === 'style' + ? resolveCSS(msg.baseURL, msg.value) + : msg.value + ), + tp: MType.SetNodeAttribute, + }), + [MType.SetCssDataURLBased]: (msg: RawSetCssDataURLBased): RawSetCssData => + ({ + ...msg, + data: resolveCSS(msg.baseURL, msg.data), + tp: MType.SetCssData, + }), + [MType.CssInsertRuleURLBased]: (msg: RawCssInsertRuleURLBased): RawCssInsertRule => + ({ + ...msg, + rule: resolveCSS(msg.baseURL, msg.rule), + tp: MType.CssInsertRule, + }), + [MType.AdoptedSsInsertRuleURLBased]: (msg: RawAdoptedSsInsertRuleURLBased): RawAdoptedSsInsertRule => + ({ + ...msg, + rule: resolveCSS(msg.baseURL, msg.rule), + tp: MType.AdoptedSsInsertRule, + }), + [MType.AdoptedSsReplaceURLBased]: (msg: RawAdoptedSsReplaceURLBased): RawAdoptedSsReplace => + ({ + ...msg, + text: resolveCSS(msg.baseURL, msg.text), + tp: MType.AdoptedSsReplace, + }), +} as const + + +export default function resolve(msg: RawMessage): RawMessage { + // @ts-ignore --- any idea? + if (resolvers[msg.tp]) { + // @ts-ignore + return resolvers[msg.tp](msg) + } + return msg +} \ No newline at end of file From cbbe26a3d58607bf98db2319dcd90f567b751e2d Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Mon, 12 Dec 2022 17:52:02 +0100 Subject: [PATCH 06/18] refactor(frontend): remove deprecated types --- frontend/app/duck/index.js | 9 +- frontend/app/duck/runs.js | 7 - frontend/app/duck/schedules.js | 14 -- frontend/app/duck/steps.js | 77 --------- frontend/app/duck/tests/index.js | 186 -------------------- frontend/app/duck/tests/runs.js | 118 ------------- frontend/app/types/run/index.js | 4 - frontend/app/types/run/run.js | 183 -------------------- frontend/app/types/run/seleniumStep.js | 29 ---- frontend/app/types/run/step.js | 31 ---- frontend/app/types/schedule.js | 228 ------------------------- frontend/app/types/step.js | 152 ----------------- 12 files changed, 1 insertion(+), 1037 deletions(-) delete mode 100644 frontend/app/duck/runs.js delete mode 100644 frontend/app/duck/schedules.js delete mode 100644 frontend/app/duck/steps.js delete mode 100644 frontend/app/duck/tests/index.js delete mode 100644 frontend/app/duck/tests/runs.js delete mode 100644 frontend/app/types/run/index.js delete mode 100644 frontend/app/types/run/run.js delete mode 100644 frontend/app/types/run/seleniumStep.js delete mode 100644 frontend/app/types/run/step.js delete mode 100644 frontend/app/types/schedule.js delete mode 100644 frontend/app/types/step.js diff --git a/frontend/app/duck/index.js b/frontend/app/duck/index.js index 5ad487c93..051ec6933 100644 --- a/frontend/app/duck/index.js +++ b/frontend/app/duck/index.js @@ -7,12 +7,8 @@ import issues from './issues'; import assignments from './assignments'; import target from './target'; import targetCustom from './targetCustom'; -import runs from './runs'; import filters from './filters'; import funnelFilters from './funnelFilters'; -import tests from './tests'; -import steps from './steps'; -import schedules from './schedules'; import events from './events'; import environments from './environments'; import variables from './variables'; @@ -46,12 +42,9 @@ export default combineReducers({ assignments, target, targetCustom, - runs, filters, funnelFilters, - tests, - steps, - schedules, + events, environments, variables, diff --git a/frontend/app/duck/runs.js b/frontend/app/duck/runs.js deleted file mode 100644 index 30b8051ac..000000000 --- a/frontend/app/duck/runs.js +++ /dev/null @@ -1,7 +0,0 @@ -import Run from 'Types/run'; -import crudDuckGenerator from './tools/crudDuck'; - -const crudDuck = crudDuckGenerator('run', Run); -export const { fetchList, fetch, init, edit, save, remove } = crudDuck.actions; - -export default crudDuck.reducer; diff --git a/frontend/app/duck/schedules.js b/frontend/app/duck/schedules.js deleted file mode 100644 index 3f13e9188..000000000 --- a/frontend/app/duck/schedules.js +++ /dev/null @@ -1,14 +0,0 @@ -import Schedule from 'Types/schedule'; -import crudDuckGenerator from './tools/crudDuck'; - -const crudDuck = crudDuckGenerator('scheduler', Schedule); -export const { fetchList, fetch, init, edit, remove } = crudDuck.actions; - -export function save(instance) { // TODO: fix the crudDuckGenerator - return { - types: crudDuck.actionTypes.SAVE.toArray(), - call: client => client.post(`/schedulers${!!instance.schedulerId ? '/' + instance.schedulerId : '' }`, instance), - }; -} - -export default crudDuck.reducer; diff --git a/frontend/app/duck/steps.js b/frontend/app/duck/steps.js deleted file mode 100644 index 02bfdbb90..000000000 --- a/frontend/app/duck/steps.js +++ /dev/null @@ -1,77 +0,0 @@ -import { List, Map } from 'immutable'; -import { RequestTypes } from 'Duck/requestStateCreator'; -import Step from 'Types/step'; -import Event from 'Types/filter/event'; -import { getRE } from 'App/utils'; -import Test from 'Types/appTest'; -import { countries } from 'App/constants'; -import { KEYS } from 'Types/filter/customFilter'; - -const countryOptions = Object.keys(countries).map(c => ({filterKey: KEYS.USER_COUNTRY, label: KEYS.USER_COUNTRY, type: KEYS.USER_COUNTRY, value: c, actualValue: countries[c], isFilter: true })); - -const INIT = 'steps/INIT'; -const EDIT = 'steps/EDIT'; - -const SET_TEST = 'steps/SET_TEST'; -const FETCH_LIST = new RequestTypes('steps/FETCH_LIST'); - -const initialState = Map({ - list: List(), - test: Test(), - instance: Step(), - editingIndex: null, -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_LIST.SUCCESS: { - return state.set('list', List(action.data).map(i => { - const type = i.type === 'navigate' ? i.type : 'location'; - return {...i, type: type.toUpperCase()} - })) - } - case INIT: - return state - .set('instance', Step(action.instance)) - .set('editingIndex', action.index) - .set('test', Test()); - case EDIT: - return state.mergeIn([ 'instance' ], action.instance); - case SET_TEST: - return state.set('test', Test(action.test)); - } - return state; -}; - -export default reducer; - -export function init(instance, index) { - return { - type: INIT, - instance, - index, - }; -} - -export function edit(instance) { - return { - type: EDIT, - instance, - }; -} - -export function setTest(test) { - return { - type: SET_TEST, - test, - }; -} - - -export function fetchList(params) { - return { - types: FETCH_LIST.toArray(), - call: client => client.get('/tests/steps/search', params), - params, - }; -} diff --git a/frontend/app/duck/tests/index.js b/frontend/app/duck/tests/index.js deleted file mode 100644 index eda7edc43..000000000 --- a/frontend/app/duck/tests/index.js +++ /dev/null @@ -1,186 +0,0 @@ -import { List, Map, Set } from 'immutable'; -import Test from 'Types/appTest'; -import stepFromJS from 'Types/step'; -import crudDuckGenerator from 'Duck/tools/crudDuck'; -import { reduceDucks } from 'Duck/tools'; -import runsDuck from './runs'; -import Run from 'Types/run'; - -const sampleRun = Run({"runId":8,"testId":7,"name":"test import","createdAt":1601481986264,"createdBy":283,"starter":"on-demand","state":"failed","steps":[{"label":"Open URL","order":0,"title":"navigate","status":"passed","startedAt":1601647536513,"finishedAt":1601647546211,"screenshot":"https://parrot-tests.s3.eu-central-1.amazonaws.com/115/7/8/screenshots/1601647546211.jpg","executionTime":9698},{"label":"Open URL","order":1,"title":"Visit OpenReplay","status":"passed","startedAt":1601647548354,"finishedAt":1601647556991,"screenshot":"https://parrot-tests.s3.eu-central-1.amazonaws.com/115/7/8/screenshots/1601647556991.jpg","executionTime":8637},{"info":"Unhandled promise rejection: TimeoutError: waiting for selector \"[name=\"email\"]\" failed: timeout 30000ms exceeded","input":"failed","label":"Send Keys to Element","order":2,"title":"input","status":"failed","startedAt":1601647559091,"finishedAt":1601647589099,"screenshot":"https://parrot-tests.s3.eu-central-1.amazonaws.com/115/7/8/screenshots/1601647589099.jpg","executionTime":30008}],"browser":"chrome","meta":{"startedAt":1601487715818},"location":"FR","startedAt":1601647524205,"finishedAt":1601647591217,"network":[{"url":"http://yahoo.fr/","method":"GET","duration":1760,"requestID":"769C483871CB3D35DF4BB1CD7D3258C4","timestamp":1601647537533,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null,"response":{"data":[{"key":"user_id","index":1},{"key":"virtual_number","index":2}]}},{"url":"http://fr.yahoo.com/","method":"GET","duration":1112,"requestID":"769C483871CB3D35DF4BB1CD7D3258C4","timestamp":1601647539293,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null,"payload":{"data":[{"key":"user_id","index":1},{"key":"virtual_number","index":2}]}},{"url":"https://fr.yahoo.com/","method":"GET","duration":1204,"requestID":"769C483871CB3D35DF4BB1CD7D3258C4","timestamp":1601647540405,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://guce.yahoo.com/consent?brandType=eu&gcrumb=bxFB6Ac&lang=fr-FR&done=https%3A%2F%2Ffr.yahoo.com%2F","method":"GET","duration":1173,"requestID":"769C483871CB3D35DF4BB1CD7D3258C4","timestamp":1601647541609,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://consent.yahoo.com/v2/collectConsent?sessionId=3_cc-session_fab600d0-8323-4b52-88c1-5698e6288f48","method":"GET","duration":1169,"requestID":"769C483871CB3D35DF4BB1CD7D3258C4","timestamp":1601647542782,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64)AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://s.yimg.com/oa/build/css/site-ltr-b1aa14b0.css","method":"GET","duration":1179,"requestID":"56.2","timestamp":1601647543958,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/rz/p/yahoo_frontpage_en-US_s_f_p_bestfit_frontpage.png","method":"GET","duration":1189,"requestID":"56.3","timestamp":1601647543959,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/oa/build/js/site-ee81be05.js","method":"GET","duration":1194,"requestID":"56.5","timestamp":1601647543961,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/rz/p/yahoo_frontpage_en-US_s_f_w_bestfit_frontpage.png","method":"GET","duration":1189,"requestID":"56.4","timestamp":1601647543961,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/oa/build/images/fr-FR-home_11f60c18d02223c8.jpeg","method":"GET","duration":1068,"requestID":"56.7","timestamp":1601647545141,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://s.yimg.com/oa/build/css/site-ltr-b1aa14b0.css","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"http://yahoo.fr/","method":"GET","duration":1312,"requestID":"7CA8FE6239B07643872BF48C30D639D3","timestamp":1601647549363,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"http://fr.yahoo.com/","method":"GET","duration":1005,"requestID":"7CA8FE6239B07643872BF48C30D639D3","timestamp":1601647550675,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://fr.yahoo.com/","method":"GET","duration":1037,"requestID":"7CA8FE6239B07643872BF48C30D639D3","timestamp":1601647551680,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://guce.yahoo.com/consent?brandType=eu&gcrumb=ESjhlqw&lang=fr-FR&done=https%3A%2F%2Ffr.yahoo.com%2F","method":"GET","duration":1045,"requestID":"7CA8FE6239B07643872BF48C30D639D3","timestamp":1601647552717,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://consent.yahoo.com/v2/collectConsent?sessionId=3_cc-session_3b367c91-9f88-498b-96a5-728947dda245","method":"GET","duration":1115,"requestID":"7CA8FE6239B07643872BF48C30D639D3","timestamp":1601647553762,"requestHeaders":{"cookie":"key1=myvalue1","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36","upgrade-insecure-requests":"1"},"responseHeaders":null},{"url":"https://s.yimg.com/oa/build/css/site-ltr-b1aa14b0.css","method":"GET","duration":1052,"requestID":"56.14","timestamp":1601647554885,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/rz/p/yahoo_frontpage_en-US_s_f_p_bestfit_frontpage.png","method":"GET","duration":1060,"requestID":"56.15","timestamp":1601647554886,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/oa/build/js/site-ee81be05.js","method":"GET","duration":1065,"requestID":"56.17","timestamp":1601647554886,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/rz/p/yahoo_frontpage_en-US_s_f_w_bestfit_frontpage.png","method":"GET","duration":1063,"requestID":"56.16","timestamp":1601647554886,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://consent.yahoo.com/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null},{"url":"https://s.yimg.com/oa/build/images/fr-FR-home_11f60c18d02223c8.jpeg","method":"GET","duration":1046,"requestID":"56.19","timestamp":1601647555944,"requestHeaders":{"cookie":"key1=myvalue1","referer":"https://s.yimg.com/oa/build/css/site-ltr-b1aa14b0.css","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/83.0.4103.0 Safari/537.36"},"responseHeaders":null}],"environmentId":null,"tenantId":115,"consoleLogs":[{"_type":"warning","_text":"A cookie associated with a resource at http://openreplay.com/ was set with `SameSite=None` but without `Secure`. It has been blocked, as Chrome now only delivers cookies marked `SameSite=None` if they are also marked `Secure`. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5633521622188032.","_args":[],"_location":{"url":"https://app.openreplay.com/"},"timestamp":1602089840909},{"_type":"warning","_text":"A cookie associated with a resource at http://app.openreplay.com/ was set with `SameSite=None` but without `Secure`. It has been blocked, as Chrome now only delivers cookies marked `SameSite=None` if they are also marked `Secure`. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5633521622188032.","_args":[],"_location":{"url":"https://app.openreplay.com/"},"timestamp":1602089840918}]}); - -const ADD_STEPS = 'tests/ADD_STEPS'; -const MOVE_STEP = 'tests/MOVE_STEP'; -const REMOVE_STEP = 'tests/REMOVE_STEP'; -const COPY_STEP = 'tests/COPY_STEP'; -const EDIT_STEP = 'tests/EDIT_STEP'; -const TOGGLE_STEP = 'tests/TOGGLE_STEP'; -const ADD_TAG = 'tests/ADD_TAG'; -const REMOVE_TAG = 'tests/REMOVE_TAG'; -const TOGGLE_TAG = 'tests/TOGGLE_TAG'; -const SET_MODIFIED = 'tests/SET_MODIFIED'; -const SET_QUERY = 'tests/SET_QUERY'; - -const MOVE_TEST = 'tests/MOVE_TEST'; - -const initialState = Map({ - tags: Set(), - query: '', - modified: false, - sampleRun: sampleRun, -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case SET_MODIFIED: - return state.set('modified', action.state); - case SET_QUERY: - return state.set('query', action.query); - case ADD_STEPS: - // TODO check frameworks - return state - .updateIn([ 'instance', 'steps' ], list => list.concat(action.steps.map(stepFromJS))).set('modified', true); - case MOVE_STEP: { - const { fromI, toI } = action; - return state - .updateIn([ 'instance', 'steps' ], list => - list.remove(fromI).insert(toI, list.get(fromI))).set('modified', true); - } - case REMOVE_STEP: - return state.removeIn([ 'instance', 'steps', action.index ]).set('modified', true); - case COPY_STEP: { - // Use fromJS to make another key. - const copiedStep = stepFromJS(state - .getIn([ 'instance', 'steps', action.index ]) - .set('imported', false)); - return state - .updateIn([ 'instance', 'steps' ], steps => - steps.insert(action.index + 1, copiedStep)).set('modified', true); - } - case EDIT_STEP: - return state.mergeIn([ 'instance', 'steps', action.index ], action.step).set('modified', true); - case TOGGLE_STEP: - return state.updateIn([ 'instance', 'steps', action.index, 'isDisabled' ], isDisabled => !isDisabled).set('modified', true); - case ADD_TAG: - return state.updateIn([ 'instance', 'tags' ], tags => tags.add(action.tag)).set('modified', true); - case REMOVE_TAG: - return state.updateIn([ 'instance', 'tags' ], tags => tags.remove(action.tag)).set('modified', true); - case TOGGLE_TAG: { - const { tag, flag } = action; - const adding = typeof flag === 'boolean' - ? flag - : !state.hasIn([ 'tags', tag ]); - return state.update('tags', tags => (adding - ? tags.add(tag) - : tags.remove(tag))); - } - case MOVE_TEST: { - const { fromI, toI } = action; - return state - .updateIn([ 'list' ], list => - list.remove(fromI).insert(toI, list.get(fromI))); - } - } - return state; -}; - -const crudDuck = crudDuckGenerator('test', Test); -export const { fetchList, fetch, init, edit, save, remove } = crudDuck.actions; -export { runTest, stopRun, checkRun, generateTest, stopAllRuns, resetErrors } from './runs'; -export default reduceDucks(crudDuck, { reducer, initialState }, runsDuck).reducer; - -export function addSteps(stepOrSteps) { - const steps = Array.isArray(stepOrSteps) || List.isList(stepOrSteps) - ? stepOrSteps - : [ stepOrSteps ]; - return { - type: ADD_STEPS, - steps, - }; -} - -export function moveStep(fromI, toI) { - return { - type: MOVE_STEP, - fromI, - toI, - }; -} - -export function removeStep(index) { - return { - type: REMOVE_STEP, - index, - }; -} - -export function copyStep(index) { - return { - type: COPY_STEP, - index, - }; -} - -export function editStep(index, step) { - return { - type: EDIT_STEP, - index, - step, - }; -} - -export function setModified(state) { - return { - type: SET_MODIFIED, - state, - }; -} - -export function toggleStep(index) { - return { - type: TOGGLE_STEP, - index, - }; -} - -export const addTag = (tag) => (dispatch) => { - return new Promise((resolve) => { - dispatch({ - type: ADD_TAG, - tag, - }) - resolve() - }) -} - -export const removeTag = (tag) => (dispatch) => { - return new Promise((resolve) => { - dispatch({ - type: REMOVE_TAG, - tag, - }); - resolve() - }) -} - -export function toggleTag(tag, flag) { - return { - type: TOGGLE_TAG, - tag, - flag, - }; -} - -export function setQuery(query) { - return { - type: SET_QUERY, - query - }; -} - -export function moveTest(fromI, toI) { - return { - type: MOVE_TEST, - fromI, - toI, - }; -} diff --git a/frontend/app/duck/tests/runs.js b/frontend/app/duck/tests/runs.js deleted file mode 100644 index e5f1ecc3d..000000000 --- a/frontend/app/duck/tests/runs.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Map } from 'immutable'; -import Test from 'Types/appTest'; -import Run, { RUNNING, STOPPED } from 'Types/run'; -import requestDuckGenerator, { RequestTypes } from 'Duck/tools/requestDuck'; -import { reduceDucks } from 'Duck/tools'; - -const GEN_TEST = new RequestTypes('tests/GEN_TEST'); -const RUN_TEST = new RequestTypes('tests/RUN_TEST'); -const STOP_RUN = new RequestTypes('tests/STOP_RUN'); -const STOP_ALL_RUNS = new RequestTypes('tests/STOP_ALL_RUNS'); -const CHECK_RUN = new RequestTypes('tests/CHECK_RUN'); -const RESET_ERRORS = 'tests/RESET_ERRORS'; - -const updateRunInTest = run => (test) => { - const runIndex = test.runHistory - .findLastIndex(({ runId }) => run.runId === runId); - return runIndex === -1 - ? test.update('runHistory', list => list.push(run)) - : test.mergeIn([ 'runHistory', runIndex ], run); -}; - -const updateRun = (state, testId, run) => { - const testIndex = state.get('list').findIndex(test => test.testId === testId); - if (testIndex === -1) return state; - const updater = updateRunInTest(run); - return state - .updateIn([ 'list', testIndex ], updater) - .updateIn([ 'instance' ], test => (test.testId === testId - ? updater(test) - : test)); -}; - -const initialState = Map({}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case GEN_TEST.SUCCESS: - return state.set('instance', Test(action.data).set('generated', true)); - case RUN_TEST.SUCCESS: { - const test = state.get('list').find(({ testId }) => testId === action.testId); - const run = Run({ - runId: action.data.id, state: RUNNING, testId: action.testId, name: test.name - }); - return updateRun(state, action.testId, run); - } - case STOP_RUN.SUCCESS: { - const { testId, runId } = action; - return updateRun(state, testId, { runId, state: STOPPED }); - } - case STOP_ALL_RUNS.SUCCESS: - return state.update('list', list => list.map(test => { - test.runHistory.map(run => run.state === RUNNING ? run.set('state', STOPPED) : run.state); - return test; - })).setIn(['runRequest', 'errors'], null); - case CHECK_RUN.SUCCESS: - return updateRun(state, action.testId, Run(action.data)); - case RESET_ERRORS: - return state.setIn(['runRequest', 'errors'], null); - } - return state; -}; - -const requestDuck = requestDuckGenerator({ - runRequest: RUN_TEST, - stopRunRequest: STOP_RUN, - stopAllRunsRequest: STOP_ALL_RUNS, - genTestRequest: GEN_TEST, -}); - -export default reduceDucks({ reducer, initialState }, requestDuck); - - -export function generateTest(sessionId, params) { - return { - types: GEN_TEST.toArray(), - call: client => client.post(`/sessions/${ sessionId }/gentest`, params), - }; -} - - -export function runTest(testId, params) { - return { - testId, - types: RUN_TEST.toArray(), - call: client => client.post(`/tests/${ testId }/execute`, params), - }; -} - -export function stopRun(testId, runId) { - return { - runId, - testId, - types: STOP_RUN.toArray(), - call: client => client.get(`/runs/${ runId }/stop`), - }; -} - -export function stopAllRuns() { - return { - types: STOP_ALL_RUNS.toArray(), - call: client => client.get(`/runs/all/stop`), - }; -} - -export function resetErrors() { - return { - type: RESET_ERRORS, - } -} - -export function checkRun(testId, runId) { - return { - runId, - testId, - types: CHECK_RUN.toArray(), - call: client => client.get(`/runs/${ runId }`), - }; -} diff --git a/frontend/app/types/run/index.js b/frontend/app/types/run/index.js deleted file mode 100644 index 658043461..000000000 --- a/frontend/app/types/run/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import fromJS from './run'; - -export * from './run'; -export default fromJS; \ No newline at end of file diff --git a/frontend/app/types/run/run.js b/frontend/app/types/run/run.js deleted file mode 100644 index cfe35488e..000000000 --- a/frontend/app/types/run/run.js +++ /dev/null @@ -1,183 +0,0 @@ -import { Record, List, Map } from 'immutable'; -import { DateTime } from 'luxon'; -import Environment from 'Types/environment'; -import stepFromJS from './step'; -import seleniumStepFromJS from './seleniumStep'; -import Resource from '../session/resource'; - -export const NOT_FETCHED = undefined; -export const QUEUED = 'queued'; -export const INITIALIZING = 'initializing'; -export const RUNNING = 'running'; -export const COMPLETED = 'completed'; -export const PASSED = 'passed'; -export const FAILED = 'failed'; -export const STOPPED = 'stopped'; -export const CRASHED = 'crashed'; -export const EXPIRED = 'expired'; - -export const STATUS = { - NOT_FETCHED, - QUEUED, - INITIALIZING, - RUNNING, - COMPLETED, - PASSED, - FAILED, - STOPPED, - CRASHED, - EXPIRED, -} - -class Run extends Record({ - runId: undefined, - testId: undefined, - name: '', - tags: List(), - environment: Environment(), - scheduled: false, - schedulerId: undefined, - browser: undefined, - sessionId: undefined, - startedAt: undefined, - url_video: undefined, - finishedAt: undefined, - steps: List(), - resources: [], - seleniumSteps: List(), - url_browser_logs: undefined, - url_logs: undefined, - url_selenium_project: undefined, - sourceCode: undefined, - screenshotUrl: undefined, - clientId: undefined, - state: NOT_FETCHED, - baseRunId: undefined, - lastExecutedString: undefined, - durationString: undefined, - hour: undefined, // TODO: fine API - day: undefined, - location: undefined, - deviceType: undefined, - advancedOptions: undefined, - harfile: undefined, - lighthouseHtmlFile: undefined, - resultsFile: undefined, - lighthouseJsonFile: undefined, - totalStepsCount: undefined, - auditsPerformance: Map(), - auditsAd: Map(), - transferredSize: undefined, - resourcesSize: undefined, - domBuildingTime: undefined, - domContentLoadedTime: undefined, - loadTime: undefined, - starter: undefined, - // { - // "id": '', - // "title": '', - // "description": '', - // "score": 0, - // "scoreDisplayMode": '', - // "numericValue": 0, - // "numericUnit": '', - // "displayValue": '' - // } -}) { - idKey = 'runId'; - isRunning() { - return this.state === RUNNING; - } - isQueued() { - return this.state === QUEUED; - } - isPassed() { - return this.state === PASSED; - } -} - -// eslint-disable-next-line complexity -function fromJS(run = {}) { - if (run instanceof Run) return run; - - const startedAt = run.startedAt && DateTime.fromMillis(run.startedAt); - const finishedAt = run.finishedAt && DateTime.fromMillis(run.finishedAt); - let durationString; - let lastExecutedString; - if (run.state === 'running') { - durationString = 'Running...'; - lastExecutedString = 'Now'; - } else if (startedAt && finishedAt) { - const _duration = Math.floor(finishedAt - startedAt); - if (_duration > 10000) { - const min = Math.floor(_duration / 60000); - durationString = `${ min < 1 ? 1 : min } min`; - } else { - durationString = `${ Math.floor(_duration / 1000) } secs`; - } - const diff = startedAt.diffNow([ 'days', 'hours', 'minutes', 'seconds' ]).negate(); - if (diff.days > 0) { - lastExecutedString = `${ Math.round(diff.days) } day${ diff.days > 1 ? 's' : '' } ago`; - } else if (diff.hours > 0) { - lastExecutedString = `${ Math.round(diff.hours) } hrs ago`; - } else if (diff.minutes > 0) { - lastExecutedString = `${ Math.round(diff.minutes) } min ago`; - } else { - lastExecutedString = `${ Math.round(diff.seconds) } sec ago`; - } - } - - const steps = List(run.steps).map(stepFromJS); - const seleniumSteps = List(run.seleniumSteps).map(seleniumStepFromJS); - const tags = List(run.tags); - const environment = Environment(run.environment); - - let resources = List(run.network) - .map(i => Resource({ - ...i, - // success: 1, - // time: i.timestamp, - // type: 'xhr', - // headerSize: 1200, - // timings: {}, - })); - const firstResourceTime = resources.map(r => r.time).reduce((a,b)=>Math.min(a,b), Number.MAX_SAFE_INTEGER); - resources = resources - .map(r => r.set("time", r.time - firstResourceTime)) - .sort((r1, r2) => r1.time - r2.time).toArray() - - const screenshotUrl = run.screenshot_url || - seleniumSteps.find(({ screenshotUrl }) => !!screenshotUrl, null, {}).screenshotUrl; - - const state = run.state === 'completed' ? PASSED : run.state; - const networkOverview = run.networkOverview || {}; - - return new Run({ - ...run, - startedAt, - finishedAt, - durationString, - lastExecutedString, - steps, - resources, - seleniumSteps, - tags, - environment, - screenshotUrl, - state, - deviceType: run.device || run.deviceType, - auditsPerformance: run.lighthouseJson && run.lighthouseJson.performance, - auditsAd: run.lighthouseJson && run.lighthouseJson.ad, - transferredSize: networkOverview.transferredSize, - resourcesSize: networkOverview.resourcesSize, - domBuildingTime: networkOverview.domBuildingTime, - domContentLoadedTime: networkOverview.domContentLoadedTime, - loadTime: networkOverview.loadTime, - }); -} - -Run.prototype.exists = function () { - return this.runId !== undefined; -}; - -export default fromJS; diff --git a/frontend/app/types/run/seleniumStep.js b/frontend/app/types/run/seleniumStep.js deleted file mode 100644 index 5178240c7..000000000 --- a/frontend/app/types/run/seleniumStep.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Record, List } from 'immutable'; -import { DateTime, Duration } from 'luxon'; - -const Step = Record({ - duration: undefined, - startedAt: undefined, - label: undefined, - input: undefined, - info: undefined, - order: undefined, - screenshotUrl: undefined, - steps: List(), -}); - -function fromJS(step = {}) { - const startedAt = step.startedAt && DateTime.fromMillis(step.startedAt * 1000); - const duration = step.executionTime && Duration.fromMillis(step.executionTime); - const steps = List(step.steps).map(Step); - const screenshotUrl = step.screenshot_url; - return new Step({ - ...step, - steps, - startedAt, - duration, - screenshotUrl, - }); -}; - -export default fromJS; \ No newline at end of file diff --git a/frontend/app/types/run/step.js b/frontend/app/types/run/step.js deleted file mode 100644 index 5358c0985..000000000 --- a/frontend/app/types/run/step.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Record, List } from 'immutable'; -import { DateTime, Duration } from 'luxon'; - -const Step = Record({ - duration: undefined, - startedAt: undefined, - label: undefined, - input: undefined, - info: undefined, - order: undefined, - status: undefined, - title: undefined, - screenshotUrl: undefined, - steps: List(), -}); - -function fromJS(step = {}) { - const startedAt = step.startedAt && DateTime.fromMillis(step.startedAt); - const duration = step.executionTime && Duration.fromMillis(step.executionTime); - const steps = List(step.steps).map(Step); - const screenshotUrl = step.screenshot; - return new Step({ - ...step, - steps, - startedAt, - duration, - screenshotUrl, - }); -}; - -export default fromJS; \ No newline at end of file diff --git a/frontend/app/types/schedule.js b/frontend/app/types/schedule.js deleted file mode 100644 index 87b38eb6d..000000000 --- a/frontend/app/types/schedule.js +++ /dev/null @@ -1,228 +0,0 @@ -import { Record, List, Map } from 'immutable'; -import { DateTime } from 'luxon'; -import { - CHANNEL, - DAYS, - HOURS, - EMAIL, - SLACK, - WEBHOOK -} from 'App/constants/schedule'; -// import runFromJS from './run'; -import { validateEmail } from 'App/validate'; - -export const DEFAULT_ENV_VALUE = '_'; -const Schedule = Record({ - minutes: 30, - hour: 0, - day: -2, - testId: '', - sourceCode: '', - name: '', - nextExecutionTime: undefined, - numberOFExecutions: undefined, - schedulerId: undefined, - environmentId: DEFAULT_ENV_VALUE, - device: 'desktop', - locations: [], - - advancedOptions: false, - headers: [{}], - cookies: [{}], - basicAuth: {}, - network: 'wifi', - bypassCSP: false, - - slack: false, - slackInput: [], - webhook: false, - webhookInput: [], - email: false, - emailInput: [], - hasNotification: false, - options: Map({ message: [], device: 'desktop' }), - - extraCaps: {}, - - validateEvery() { - if (this.day > -2) return true; - return this.minutes >= 5 && this.minutes <= 1440; - }, - validateWebhookEmail() { - if (this.channel !== EMAIL) return true; - return validateEmail(this.webhookEmail); - }, - validateWebhook() { - if (this.channel !== WEBHOOK) return true; - return this.webhookId !== ''; - } -}); - -function fromJS(schedule = {}) { - if (schedule instanceof Schedule) return schedule; - const options = schedule.options || { message: [] }; - const extraCaps = options.extraCaps || { }; - - let channel = ''; - if (schedule.webhookEmail) { - channel = EMAIL; - } else if (schedule.webhookId && schedule.webhook) { - channel = schedule.webhook.type === 'slack' ? SLACK : WEBHOOK; - } - - const nextExecutionTime = schedule.nextExecutionTime ? - DateTime.fromMillis(schedule.nextExecutionTime) : undefined; - - - let { day, minutes } = schedule; - let hour; - if (day !== -2) { - const utcOffset = new Date().getTimezoneOffset(); - minutes = minutes - utcOffset - minutes = minutes >= 1440 ? (minutes - 1440) : minutes; - hour = Math.floor(minutes / 60); - } - // if (day !== -2) { - // const utcOffset = new Date().getTimezoneOffset(); - // const hourOffset = Math.floor(utcOffset / 60); - // const minuteOffset = utcOffset - 60*hourOffset; - - // minutes -= minuteOffset; - // hour -= hourOffset; - // if (day !== -1) { - // const dayOffset = Math.floor(hour/24); // +/-1 - // day = (day + dayOffset + 7) % 7; - // } - // hour = (hour + 24) % 24; - // } - - const slack = List(options.message).filter(i => i.type === 'slack'); - const email = List(options.message).filter(i => i.type === 'email'); - const webhook = List(options.message).filter(i => i.type === 'webhook'); - - const headers = extraCaps.headers ? Object.keys(extraCaps.headers).map(k => ({ name: k, value: extraCaps.headers[k] })) : [{}]; - const cookies = extraCaps.cookies ? Object.keys(extraCaps.cookies).map(k => ({ name: k, value: extraCaps.cookies[k] })) : [{}]; - - return new Schedule({ - ...schedule, - day, - minutes, - hour, - channel, - nextExecutionTime, - device: options.device, - options, - advancedOptions: !!options.extraCaps, - bypassCSP: options.bypassCSP, - network: options.network, - headers, - cookies, - basicAuth: extraCaps.basicAuth, - - slack: slack.size > 0, - slackInput: slack.map(i => parseInt(i.value)).toJS(), - - email: email.size > 0, - emailInput: email.map(i => i.value).toJS(), - - webhook: webhook.size > 0, - webhookInput: webhook.map(i => parseInt(i.value)).toJS(), - - hasNotification: !!slack || !!email || !!webhook - }); -} - -function getObjetctFromArr(arr) { - const obj = {} - for (var i = 0; i < arr.length; i++) { - const temp = arr[i]; - obj[temp.name] = temp.value - } - return obj; -} - -Schedule.prototype.toData = function toData() { - const { - name, schedulerId, environmentId, device, options, bypassCSP, network, headers, cookies, basicAuth - } = this; - - const js = this.toJS(); - options.device = device; - options.bypassCSP = bypassCSP; - options.network = network; - - options.extraCaps = { - headers: getObjetctFromArr(headers), - cookies: getObjetctFromArr(cookies), - basicAuth - }; - - if (js.slack && js.slackInput) - options.message = js.slackInput.map(i => ({ type: 'slack', value: i })) - if (js.email && js.emailInput) - options.message = options.message.concat(js.emailInput.map(i => ({ type: 'email', value: i }))) - if (js.webhook && js.webhookInput) - options.message = options.message.concat(js.webhookInput.map(i => ({ type: 'webhook', value: i }))) - - let day = this.day; - let hour = undefined; - let minutes = this.minutes; - if (day !== -2) { - const utcOffset = new Date().getTimezoneOffset(); - minutes = (this.hour * 60) + utcOffset; - // minutes += utcOffset; - minutes = minutes < 0 ? minutes + 1440 : minutes; - } - // if (day !== -2) { - // const utcOffset = new Date().getTimezoneOffset(); - // const hourOffset = Math.floor(utcOffset / 60); - // const minuteOffset = utcOffset - 60*hourOffset; - - // minutes = minuteOffset; - // hour = this.hour + hourOffset; - // if (day !== -1) { - // const dayOffset = Math.floor(hour/24); // +/-1 - // day = (day + dayOffset + 7) % 7; - // } - // hour = (hour + 24) % 24; - // } - - delete js.slack; - delete js.webhook; - delete js.email; - delete js.slackInput; - delete js.webhookInput; - delete js.emailInput; - delete js.hasNotification; - delete js.headers; - delete js.cookies; - - delete js.device; - delete js.extraCaps; - - // return { - // day, hour, name, minutes, schedulerId, environment, - // }; - return { ...js, day, hour, name, minutes, schedulerId, environmentId, options: options }; -}; - -Schedule.prototype.exists = function exists() { - return this.schedulerId !== undefined; -}; - -Schedule.prototype.valid = function validate() { - return this.validateEvery; -}; - -Schedule.prototype.getInterval = function getInterval() { - const DAY = List(DAYS).filter(item => item.value === this.day).first(); - - if (DAY.value === -2) { - return DAY.text + ' ' + this.minutes + ' Minutes'; // Every 30 minutes - } - - const HOUR = List(HOURS).filter(item => item.value === this.hour).first(); - return DAY.text + ' ' + HOUR.text; // Everyday/Sunday 2 AM; -}; - -export default fromJS; diff --git a/frontend/app/types/step.js b/frontend/app/types/step.js deleted file mode 100644 index 438b403d8..000000000 --- a/frontend/app/types/step.js +++ /dev/null @@ -1,152 +0,0 @@ -import { Record, List, Set, isImmutable } from 'immutable'; -import { TYPES as EVENT_TYPES } from 'Types/session/event'; - -export const CUSTOM = 'custom'; -export const CLICK = 'click'; -export const INPUT = 'input'; -export const NAVIGATE = 'navigate'; -export const TEST = 'test'; - -export const TYPES = { - CLICK, - INPUT, - CUSTOM, - NAVIGATE, - TEST, -}; - - -const Step = defaultValues => class extends Record({ - key: undefined, - name: '', - imported: false, - isDisabled: false, - importTestId: undefined, - ...defaultValues, -}) { - hasTarget() { - return this.type === CLICK || this.type === INPUT; - } - - isTest() { - return this.type === TEST; - } - - getEventType() { - switch (this.type) { - case INPUT: - return EVENT_TYPES.INPUT; - case CLICK: - return EVENT_TYPES.CLICK; - case NAVIGATE: - return EVENT_TYPES.LOCATION; - default: - return null; - } - } - - validate() { - const selectorsOK = this.selectors && this.selectors.size > 0; - const valueOK = this.value && this.value.trim().length > 0; - switch (this.type) { - case INPUT: - return selectorsOK; - case CLICK: - return selectorsOK; - case NAVIGATE: - return valueOK; - case CUSTOM: - // if (this.name.length === 0) return false; - /* if (window.JSHINT) { - window.JSHINT(this.code, { esversion: 6 }); - const noErrors = window.JSHINT.errors.every(({ code }) => code && code.startsWith('W')); - return noErrors; - } */ - return this.code && this.code.length > 0; - default: - return true; - } - } - - toData() { - const { - value, - ...step - } = this.toJS(); - delete step.key; - return { - values: value && [ value ], - ...step, - }; - } -}; - -const Custom = Step({ - type: CUSTOM, - code: '', - framework: 'any', - template: '', -}); - -const Click = Step({ - type: CLICK, - selectors: List(), - customSelector: true, -}); - -const Input = Step({ - type: INPUT, - selectors: List(), - value: '', - customSelector: true, -}); - -const Navigate = Step({ - type: NAVIGATE, - value: '', -}); - -const TestAsStep = Step({ - type: TEST, - testId: '', - name: '', - stepsCount: '', - steps: List(), -}); - -const EmptyStep = Step(); - -let uniqueKey = 0xff; -function nextKey() { - uniqueKey += 1; - return `${ uniqueKey }`; -} - -function fromJS(initStep = {}) { - // TODO: more clear - if (initStep.importTestId) return new TestAsStep(initStep).set('steps', List(initStep.steps ? initStep.steps : initStep.test.steps).map(fromJS)); - // todo: ? - if (isImmutable(initStep)) return initStep.set('key', nextKey()); - - const values = initStep.values && initStep.values.length > 0 && initStep.values[ 0 ]; - - // bad code - const step = { - ...initStep, - selectors: Set(initStep.selectors).toList(), // to List not nrcrssary. TODO: check - value: initStep.value ? [initStep.value] : values, - key: nextKey(), - isDisabled: initStep.disabled - }; - // bad code - - if (step.type === CUSTOM) return new Custom(step); - if (step.type === CLICK) return new Click(step); - if (step.type === INPUT) return new Input(step); - if (step.type === NAVIGATE) return new Navigate(step); - - return new EmptyStep(); - // throw new Error(`Unknown step type: ${step.type}`); -} - -export default fromJS; From 0ed4444c1df22c4f660146f689a9a9bf39d87445 Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 12 Dec 2022 17:51:15 +0100 Subject: [PATCH 07/18] change(tracker): add docs link to readme --- tracker/tracker-assist/README.md | 8 +++++-- tracker/tracker/README.md | 40 ++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/tracker/tracker-assist/README.md b/tracker/tracker-assist/README.md index 4897477b3..a09d201b1 100644 --- a/tracker/tracker-assist/README.md +++ b/tracker/tracker-assist/README.md @@ -2,6 +2,10 @@ OpenReplay Assist Plugin allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software. +## Documentation + +For launch options and available public methods, (refer to the documentation)[https://docs.openreplay.com/plugins/assist] + ## Installation ```bash @@ -72,7 +76,7 @@ trackerAssist({ type ConfirmOptions = { text?:string, style?: StyleObject, // style object (i.e {color: 'red', borderRadius: '10px'}) - confirmBtn?: ButtonOptions, + confirmBtn?: ButtonOptions, declineBtn?: ButtonOptions } @@ -82,7 +86,7 @@ type ButtonOptions = HTMLButtonElement | string | { } ``` -- `callConfirm`: Customize the text and/or layout of the call request popup. +- `callConfirm`: Customize the text and/or layout of the call request popup. - `controlConfirm`: Customize the text and/or layout of the remote control request popup. - `config`: Contains any custom ICE/TURN server configuration. Defaults to `{ 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }], 'sdpSemantics': 'unified-plan' }`. - `onAgentConnect: () => (()=>void | void)`: This callback function is fired when someone from OpenReplay UI connects to the current live session. It can return another function. In this case, returned callback will be called when the same agent connection gets closed. diff --git a/tracker/tracker/README.md b/tracker/tracker/README.md index c47f301dc..d28ab18ff 100644 --- a/tracker/tracker/README.md +++ b/tracker/tracker/README.md @@ -2,10 +2,14 @@ The main package of the [OpenReplay](https://openreplay.com/) tracker. +## Documentation + +For launch options and available public methods, (refer to the documentation)[https://docs.openreplay.com/installation/javascript-sdk#options] + ## Installation ```bash -npm i @openreplay/tracker +npm i @openreplay/tracker ``` ## Usage @@ -13,30 +17,30 @@ npm i @openreplay/tracker Initialize the package from your codebase entry point and start the tracker. You must set the `projectKey` option in the constructor. Its value can can be found in your OpenReplay dashboard under [Preferences -> Projects](https://app.openreplay.com/client/projects). ```js -import Tracker from '@openreplay/tracker'; +import Tracker from '@openreplay/tracker' const tracker = new Tracker({ projectKey: YOUR_PROJECT_KEY, -}); -tracker.start({ - userID: "Mr.Smith", - metadata: { - version: "3.5.0", - balance: "10M", - role: "admin", - } -}).then(startedSession => { - if (startedSession.success) { - console.log(startedSession) - } }) +tracker + .start({ + userID: 'Mr.Smith', + metadata: { + version: '3.5.0', + balance: '10M', + role: 'admin', + }, + }) + .then((startedSession) => { + if (startedSession.success) { + console.log(startedSession) + } + }) ``` Then you can use OpenReplay JavaScript API anywhere in your code. ```js -tracker.setUserID('my_user_id'); -tracker.setMetadata('env', 'prod'); +tracker.setUserID('my_user_id') +tracker.setMetadata('env', 'prod') ``` - -Read [our docs](https://docs.openreplay.com/) for more information. From 021f37b2cf24807325460a48f54d6b49d8167109 Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 12 Dec 2022 17:53:40 +0100 Subject: [PATCH 08/18] change(tracker): fix links format --- tracker/tracker-assist/README.md | 2 +- tracker/tracker/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tracker/tracker-assist/README.md b/tracker/tracker-assist/README.md index a09d201b1..662e1e084 100644 --- a/tracker/tracker-assist/README.md +++ b/tracker/tracker-assist/README.md @@ -4,7 +4,7 @@ OpenReplay Assist Plugin allows you to support your users by seeing their live s ## Documentation -For launch options and available public methods, (refer to the documentation)[https://docs.openreplay.com/plugins/assist] +For launch options and available public methods, [refer to the documentation](https://docs.openreplay.com/plugins/assist) ## Installation diff --git a/tracker/tracker/README.md b/tracker/tracker/README.md index d28ab18ff..b1daf6d4d 100644 --- a/tracker/tracker/README.md +++ b/tracker/tracker/README.md @@ -4,7 +4,7 @@ The main package of the [OpenReplay](https://openreplay.com/) tracker. ## Documentation -For launch options and available public methods, (refer to the documentation)[https://docs.openreplay.com/installation/javascript-sdk#options] +For launch options and available public methods, [refer to the documentation](https://docs.openreplay.com/installation/javascript-sdk#options) ## Installation From 23720082486fb52e3ff817a810f19f266286f33f Mon Sep 17 00:00:00 2001 From: Mehdi Osman Date: Mon, 12 Dec 2022 22:12:39 +0100 Subject: [PATCH 09/18] Turning failover to false by default --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 0d7cad075..63b75dc61 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -69,7 +69,7 @@ ENV TZ=UTC \ PARTITIONS_NUMBER=16 \ QUEUE_MESSAGE_SIZE_LIMIT=1048576 \ BEACON_SIZE_LIMIT=1000000 \ - USE_FAILOVER=true \ + USE_FAILOVER=false \ GROUP_STORAGE_FAILOVER=failover \ TOPIC_STORAGE_FAILOVER=storage-failover From 54adc377e9af18a70ab0209ec162a1133ca22536 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 10:50:54 +0100 Subject: [PATCH 10/18] fix(tracker): fix wording in log --- tracker/tracker/src/main/app/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index cd783b4e9..90e766f56 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -452,7 +452,7 @@ export default class App { return Promise.reject('no worker found after start request (this might not happen)') } if (this.activityState === ActivityState.NotActive) { - return Promise.reject('Tracker stopped during authorisation') + return Promise.reject('Tracker stopped during authorization') } const { token, From 22b60af72cbdef79f49db4eb3c3b379f3b660d86 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 12:13:09 +0100 Subject: [PATCH 11/18] fix(ui): fix player context call --- .../components/SelectorsList/SelectorsList.tsx | 2 +- .../components/Session_/Player/Overlay/ElementsMarker.tsx | 8 +++----- frontend/app/components/Session_/PlayerBlockHeader.tsx | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx index fabdca831..183b9754e 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/components/SelectorsList/SelectorsList.tsx @@ -13,7 +13,7 @@ function SelectorsList() { return (
- {targets && targets.map((target, index) => )} + {targets && targets.map((target, index) => )}
); diff --git a/frontend/app/components/Session_/Player/Overlay/ElementsMarker.tsx b/frontend/app/components/Session_/Player/Overlay/ElementsMarker.tsx index 99499cac6..7be45a432 100644 --- a/frontend/app/components/Session_/Player/Overlay/ElementsMarker.tsx +++ b/frontend/app/components/Session_/Player/Overlay/ElementsMarker.tsx @@ -1,9 +1,7 @@ import React from 'react'; import Marker from './ElementsMarker/Marker'; +import type { MarkedTarget } from 'Player'; -export default function ElementsMarker({ targets, activeIndex }) { - return targets && targets.map(t => ) +export default function ElementsMarker({ targets, activeIndex }: { targets: MarkedTarget[], activeIndex: number }) { + return targets && targets.map(t => ) } - - - diff --git a/frontend/app/components/Session_/PlayerBlockHeader.tsx b/frontend/app/components/Session_/PlayerBlockHeader.tsx index 1932ccb85..744db22cc 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.tsx +++ b/frontend/app/components/Session_/PlayerBlockHeader.tsx @@ -31,7 +31,6 @@ function PlayerBlockHeader(props: any) { const { assistMultiviewStore } = useStore(); const { width, height, showEvents } = store.get(); - const toggleEvents = player.toggleEvents; const { session, @@ -147,10 +146,10 @@ function PlayerBlockHeader(props: any) { onClick={(tab) => { if (activeTab === tab) { setActiveTab(''); - toggleEvents(); + player.toggleEvents(); } else { setActiveTab(tab); - !showEvents && toggleEvents(); + !showEvents && player.toggleEvents(); } }} border={false} From bab5a819595ff7274d35bcf80b77d41003fe9f3f Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 13 Dec 2022 14:35:41 +0100 Subject: [PATCH 12/18] [Storage] added workers perf improvements (#877) * feat(backend): added workers for storage service --- backend/cmd/storage/main.go | 9 +- backend/internal/storage/storage.go | 306 ++++++++++++++-------------- ee/backend/pkg/failover/failover.go | 2 +- 3 files changed, 161 insertions(+), 156 deletions(-) diff --git a/backend/cmd/storage/main.go b/backend/cmd/storage/main.go index 251ce82e2..267cb898d 100644 --- a/backend/cmd/storage/main.go +++ b/backend/cmd/storage/main.go @@ -44,7 +44,7 @@ func main() { messages.NewMessageIterator( func(msg messages.Message) { sesEnd := msg.(*messages.SessionEnd) - if err := srv.UploadSessionFiles(sesEnd); err != nil { + if err := srv.Upload(sesEnd); err != nil { log.Printf("can't find session: %d", msg.SessionID()) sessionFinder.Find(msg.SessionID(), sesEnd.Timestamp) } @@ -54,7 +54,7 @@ func main() { []int{messages.MsgSessionEnd}, true, ), - true, + false, cfg.MessageSizeLimit, ) @@ -69,10 +69,15 @@ func main() { case sig := <-sigchan: log.Printf("Caught signal %v: terminating\n", sig) sessionFinder.Stop() + srv.Wait() consumer.Close() os.Exit(0) case <-counterTick: go counter.Print() + srv.Wait() + if err := consumer.Commit(); err != nil { + log.Printf("can't commit messages: %s", err) + } case msg := <-consumer.Rebalanced(): log.Println(msg) default: diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index 12a37183f..594d97eea 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -2,20 +2,33 @@ package storage import ( "bytes" - "context" "fmt" + gzip "github.com/klauspost/pgzip" "go.opentelemetry.io/otel/metric/instrument/syncfloat64" "log" config "openreplay/backend/internal/config/storage" - "openreplay/backend/pkg/flakeid" "openreplay/backend/pkg/messages" "openreplay/backend/pkg/monitoring" "openreplay/backend/pkg/storage" "os" "strconv" - "time" + "sync" ) +type FileType string + +const ( + DOM FileType = "/dom.mob" + DEV FileType = "/devtools.mob" +) + +type Task struct { + id string + doms *bytes.Buffer + dome *bytes.Buffer + dev *bytes.Buffer +} + type Storage struct { cfg *config.Config s3 *storage.S3 @@ -27,6 +40,9 @@ type Storage struct { readingDOMTime syncfloat64.Histogram readingTime syncfloat64.Histogram archivingTime syncfloat64.Histogram + + tasks chan *Task + ready chan struct{} } func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Storage, error) { @@ -57,7 +73,7 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor if err != nil { log.Printf("can't create archiving_duration metric: %s", err) } - return &Storage{ + newStorage := &Storage{ cfg: cfg, s3: s3, startBytes: make([]byte, cfg.FileSplitSize), @@ -66,169 +82,153 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor sessionDevtoolsSize: sessionDevtoolsSize, readingTime: readingTime, archivingTime: archivingTime, - }, nil + tasks: make(chan *Task, 1), + ready: make(chan struct{}), + } + go newStorage.worker() + return newStorage, nil } -func (s *Storage) UploadSessionFiles(msg *messages.SessionEnd) error { - if err := s.uploadKey(msg.SessionID(), "/dom.mob", true, 5, msg.EncryptionKey); err != nil { - return err - } - if err := s.uploadKey(msg.SessionID(), "/devtools.mob", false, 4, msg.EncryptionKey); err != nil { - log.Printf("can't find devtools for session: %d, err: %s", msg.SessionID(), err) - } - return nil +func (s *Storage) Wait() { + <-s.ready } -// TODO: make a bit cleaner. -// TODO: Of course, I'll do! -func (s *Storage) uploadKey(sessID uint64, suffix string, shouldSplit bool, retryCount int, encryptionKey string) error { - if retryCount <= 0 { - return nil +func (s *Storage) Upload(msg *messages.SessionEnd) (err error) { + // Generate file path + sessionID := strconv.FormatUint(msg.SessionID(), 10) + filePath := s.cfg.FSDir + "/" + sessionID + // Prepare sessions + newTask := &Task{ + id: sessionID, } - start := time.Now() - fileName := strconv.FormatUint(sessID, 10) - mobFileName := fileName - if suffix == "/devtools.mob" { - mobFileName += "devtools" - } - filePath := s.cfg.FSDir + "/" + mobFileName + wg := &sync.WaitGroup{} + wg.Add(2) + go func() { + if prepErr := s.prepareSession(filePath, DOM, newTask); prepErr != nil { + err = fmt.Errorf("prepare session err: %s", prepErr) + } + wg.Done() + }() + go func() { + if prepErr := s.prepareSession(filePath, DOM, newTask); prepErr != nil { + err = fmt.Errorf("prepare session err: %s", prepErr) + } + wg.Done() + }() + wg.Wait() + // Send new task to worker + s.tasks <- newTask + // Unload worker + <-s.ready + return err +} +func (s *Storage) openSession(filePath string) ([]byte, error) { // Check file size before download into memory info, err := os.Stat(filePath) - if err == nil { - if info.Size() > s.cfg.MaxFileSize { - log.Printf("big file, size: %d, session: %d", info.Size(), sessID) - return nil - } + if err == nil && info.Size() > s.cfg.MaxFileSize { + return nil, fmt.Errorf("big file, size: %d", info.Size()) } - file, err := os.Open(filePath) + // Read file into memory + return os.ReadFile(filePath) +} + +func (s *Storage) prepareSession(path string, tp FileType, task *Task) error { + // Open mob file + if tp == DEV { + path += "devtools" + } + mob, err := s.openSession(path) if err != nil { - return fmt.Errorf("File open error: %v; sessID: %s, part: %d, sessStart: %s\n", - err, fileName, sessID%16, - time.UnixMilli(int64(flakeid.ExtractTimestamp(sessID))), - ) + return err } - defer file.Close() - - var fileSize int64 = 0 - fileInfo, err := file.Stat() - if err != nil { - log.Printf("can't get file info: %s", err) + if tp == DEV { + task.dev = s.compressSession(mob) } else { - fileSize = fileInfo.Size() - } - - var encryptedData []byte - fileName += suffix - if shouldSplit { - nRead, err := file.Read(s.startBytes) - if err != nil { - log.Printf("File read error: %s; sessID: %s, part: %d, sessStart: %s", - err, - fileName, - sessID%16, - time.UnixMilli(int64(flakeid.ExtractTimestamp(sessID))), - ) - time.AfterFunc(s.cfg.RetryTimeout, func() { - s.uploadKey(sessID, suffix, shouldSplit, retryCount-1, encryptionKey) - }) + if len(mob) <= s.cfg.FileSplitSize { + task.doms = s.compressSession(mob) return nil } - s.readingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds())) - - start = time.Now() - // Encrypt session file if we have encryption key - if encryptionKey != "" { - encryptedData, err = EncryptData(s.startBytes[:nRead], []byte(encryptionKey)) - if err != nil { - log.Printf("can't encrypt data: %s", err) - encryptedData = s.startBytes[:nRead] - } - } else { - encryptedData = s.startBytes[:nRead] - } - // Compress and save to s3 - startReader := bytes.NewBuffer(encryptedData) - if err := s.s3.Upload(s.gzipFile(startReader), fileName+"s", "application/octet-stream", true); err != nil { - log.Fatalf("Storage: start upload failed. %v\n", err) - } - // TODO: fix possible error (if we read less then FileSplitSize) - if nRead == s.cfg.FileSplitSize { - restPartSize := fileSize - int64(nRead) - fileData := make([]byte, restPartSize) - nRead, err = file.Read(fileData) - if err != nil { - log.Printf("File read error: %s; sessID: %s, part: %d, sessStart: %s", - err, - fileName, - sessID%16, - time.UnixMilli(int64(flakeid.ExtractTimestamp(sessID))), - ) - return nil - } - if int64(nRead) != restPartSize { - log.Printf("can't read the rest part of file") - } - - // Encrypt session file if we have encryption key - if encryptionKey != "" { - encryptedData, err = EncryptData(fileData, []byte(encryptionKey)) - if err != nil { - log.Printf("can't encrypt data: %s", err) - encryptedData = fileData - } - } else { - encryptedData = fileData - } - // Compress and save to s3 - endReader := bytes.NewBuffer(encryptedData) - if err := s.s3.Upload(s.gzipFile(endReader), fileName+"e", "application/octet-stream", true); err != nil { - log.Fatalf("Storage: end upload failed. %v\n", err) - } - } - s.archivingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds())) - } else { - start = time.Now() - fileData := make([]byte, fileSize) - nRead, err := file.Read(fileData) - if err != nil { - log.Printf("File read error: %s; sessID: %s, part: %d, sessStart: %s", - err, - fileName, - sessID%16, - time.UnixMilli(int64(flakeid.ExtractTimestamp(sessID))), - ) - return nil - } - if int64(nRead) != fileSize { - log.Printf("can't read the rest part of file") - } - - // Encrypt session file if we have encryption key - if encryptionKey != "" { - encryptedData, err = EncryptData(fileData, []byte(encryptionKey)) - if err != nil { - log.Printf("can't encrypt data: %s", err) - encryptedData = fileData - } - } else { - encryptedData = fileData - } - endReader := bytes.NewBuffer(encryptedData) - if err := s.s3.Upload(s.gzipFile(endReader), fileName, "application/octet-stream", true); err != nil { - log.Fatalf("Storage: end upload failed. %v\n", err) - } - s.archivingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds())) + wg := &sync.WaitGroup{} + wg.Add(2) + go func() { + task.doms = s.compressSession(mob[:s.cfg.FileSplitSize]) + wg.Done() + }() + go func() { + task.dome = s.compressSession(mob[s.cfg.FileSplitSize:]) + wg.Done() + }() + wg.Wait() } - - // Save metrics - ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*200) - if shouldSplit { - s.totalSessions.Add(ctx, 1) - s.sessionDOMSize.Record(ctx, float64(fileSize)) - } else { - s.sessionDevtoolsSize.Record(ctx, float64(fileSize)) - } - return nil } + +func (s *Storage) encryptSession(data []byte, encryptionKey string) []byte { + var encryptedData []byte + var err error + if encryptionKey != "" { + encryptedData, err = EncryptData(data, []byte(encryptionKey)) + if err != nil { + log.Printf("can't encrypt data: %s", err) + encryptedData = data + } + } else { + encryptedData = data + } + return encryptedData +} + +func (s *Storage) compressSession(data []byte) *bytes.Buffer { + zippedMob := new(bytes.Buffer) + z, _ := gzip.NewWriterLevel(zippedMob, gzip.BestSpeed) + if _, err := z.Write(data); err != nil { + log.Printf("can't write session data to compressor: %s", err) + } + if err := z.Close(); err != nil { + log.Printf("can't close compressor: %s", err) + } + return zippedMob +} + +func (s *Storage) uploadSession(task *Task) { + wg := &sync.WaitGroup{} + wg.Add(3) + go func() { + if task.doms != nil { + if err := s.s3.Upload(task.doms, task.id+string(DOM)+"s", "application/octet-stream", true); err != nil { + log.Fatalf("Storage: start upload failed. %s", err) + } + } + wg.Done() + }() + go func() { + if task.dome != nil { + if err := s.s3.Upload(task.dome, task.id+string(DOM)+"e", "application/octet-stream", true); err != nil { + log.Fatalf("Storage: start upload failed. %s", err) + } + } + wg.Done() + }() + go func() { + if task.dev != nil { + if err := s.s3.Upload(task.dev, task.id+string(DEV), "application/octet-stream", true); err != nil { + log.Fatalf("Storage: start upload failed. %s", err) + } + } + wg.Done() + }() + wg.Wait() +} + +func (s *Storage) worker() { + for { + select { + case task := <-s.tasks: + s.uploadSession(task) + default: + // Signal that worker finished all tasks + s.ready <- struct{}{} + } + } +} diff --git a/ee/backend/pkg/failover/failover.go b/ee/backend/pkg/failover/failover.go index 1b9321afc..11ff7e4be 100644 --- a/ee/backend/pkg/failover/failover.go +++ b/ee/backend/pkg/failover/failover.go @@ -91,7 +91,7 @@ func (s *sessionFinderImpl) worker() { func (s *sessionFinderImpl) findSession(sessionID, timestamp, partition uint64) { sessEnd := &messages.SessionEnd{Timestamp: timestamp} sessEnd.SetSessionID(sessionID) - err := s.storage.UploadSessionFiles(sessEnd) + err := s.storage.Upload(sessEnd) if err == nil { log.Printf("found session: %d in partition: %d, original: %d", sessionID, partition, sessionID%numberOfPartitions) From 91b4b3b9247c8967071eebbdce61fdf8998d5de0 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Tue, 13 Dec 2022 15:00:28 +0100 Subject: [PATCH 13/18] fix(tracker): WebworkerSender: reset token on sender clean; start sending;5D on token update --- tracker/tracker/src/webworker/QueueSender.ts | 40 ++++++++------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/tracker/tracker/src/webworker/QueueSender.ts b/tracker/tracker/src/webworker/QueueSender.ts index aa1ff4589..2a2863192 100644 --- a/tracker/tracker/src/webworker/QueueSender.ts +++ b/tracker/tracker/src/webworker/QueueSender.ts @@ -2,24 +2,6 @@ const INGEST_PATH = '/v1/web/i' const KEEPALIVE_SIZE_LIMIT = 64 << 10 // 64 kB -// function sendXHR(url: string, token: string, batch: Uint8Array): Promise { -// const req = new XMLHttpRequest() -// req.open("POST", url) -// req.setRequestHeader("Authorization", "Bearer " + token) -// return new Promise((res, rej) => { -// req.onreadystatechange = function() { -// if (this.readyState === 4) { -// if (this.status == 0) { -// return; // happens simultaneously with onerror -// } -// res(this) -// } -// } -// req.onerror = rej -// req.send(batch.buffer) -// }) -// } - export default class QueueSender { private attemptsCount = 0 private busy = false @@ -38,6 +20,10 @@ export default class QueueSender { authorise(token: string): void { this.token = token + if (!this.busy) { + // TODO: transparent busy/send logic + this.sendNext() + } } push(batch: Uint8Array): void { @@ -48,9 +34,19 @@ export default class QueueSender { } } + private sendNext() { + const nextBatch = this.queue.shift() + if (nextBatch) { + this.sendBatch(nextBatch) + } else { + this.busy = false + } + } + private retry(batch: Uint8Array): void { if (this.attemptsCount >= this.MAX_ATTEMPTS_COUNT) { this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`) + // remains this.busy === true return } this.attemptsCount++ @@ -83,12 +79,7 @@ export default class QueueSender { // Success this.attemptsCount = 0 - const nextBatch = this.queue.shift() - if (nextBatch) { - this.sendBatch(nextBatch) - } else { - this.busy = false - } + this.sendNext() }) .catch((e) => { console.warn('OpenReplay:', e) @@ -98,5 +89,6 @@ export default class QueueSender { clean() { this.queue.length = 0 + this.token = null } } From ea69792159be7c6f275a416d4eff9e2d00ed84e1 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 15:35:59 +0100 Subject: [PATCH 14/18] change(ui): allow changing from relative timest to actual time --- .../Session_/Player/Controls/Timeline.tsx | 11 ++++++----- .../Player/Controls/components/TimeTooltip.tsx | 9 ++++----- frontend/app/components/Session_/Subheader.js | 14 ++++++++++++++ .../shared/DevTools/NetworkPanel/NetworkPanel.tsx | 14 ++++++++------ .../FetchDetailsModal/FetchDetailsModal.tsx | 4 +++- .../FetchBasicDetails/FetchBasicDetails.tsx | 15 +++++++++++++-- frontend/app/mstore/notesStore.ts | 6 +++++- frontend/app/mstore/settingsStore.ts | 5 +++++ 8 files changed, 58 insertions(+), 20 deletions(-) diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx index 32e6d35eb..5a47b1df2 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx +++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx @@ -11,7 +11,7 @@ import TooltipContainer from './components/TooltipContainer'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; - +import { DateTime, Duration } from 'luxon'; function getTimelinePosition(value: number, scale: number) { const pos = value * scale; @@ -22,7 +22,7 @@ function getTimelinePosition(value: number, scale: number) { function Timeline(props) { const { player, store } = useContext(PlayerContext) const [wasPlaying, setWasPlaying] = useState(false) - const { notesStore } = useStore(); + const { notesStore, settingsStore } = useStore(); const { playing, time, @@ -89,20 +89,21 @@ function Timeline(props) { return props.tooltipVisible && hideTimeTooltip(); } - let timeLineTooltip; if (live) { const [time, duration] = getLiveTime(e); timeLineTooltip = { - time: duration - time, + time: Duration.fromMillis(duration - time).toFormat(`-mm:ss`), offset: e.nativeEvent.offsetX, isVisible: true, }; } else { const time = getTime(e); timeLineTooltip = { - time: time, + time: !settingsStore.isUniTs + ? Duration.fromMillis(time).toFormat(`mm:ss`) + : DateTime.fromMillis(props.startedAt + time).toFormat(`hh:mm:ss a`), offset: e.nativeEvent.offsetX, isVisible: true, }; diff --git a/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx b/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx index e1be98622..5c4d19c0d 100644 --- a/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx @@ -8,26 +8,25 @@ interface Props { time: number; offset: number; isVisible: boolean; - liveTimeTravel: boolean; } function TimeTooltip({ time, offset, isVisible, - liveTimeTravel, }: Props) { - const duration = Duration.fromMillis(time).toFormat(`${liveTimeTravel ? '-' : ''}mm:ss`); return (
- {!time ? 'Loading' : duration} + {!time ? 'Loading' : time}
); } diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index be7679fd5..45d5540bb 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -11,7 +11,9 @@ import { useModal } from 'App/components/Modal'; import BugReportModal from './BugReport/BugReportModal'; import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; import AutoplayToggle from 'Shared/AutoplayToggle'; +import { Toggler } from 'UI'; function SubHeader(props) { const { player, store } = React.useContext(PlayerContext) @@ -26,6 +28,7 @@ function SubHeader(props) { eventList: eventsList, endTime, } = store.get() + const { settingsStore } = useStore() const mappedResourceList = resourceList .filter((r) => r.isRed() || r.isYellow()) @@ -53,8 +56,19 @@ function SubHeader(props) { showModal(, { right: true }); }; + const timeStr = settingsStore.isUniTs ? 'Local Time' : 'Relative Timestamp' + const onFormatCh = (e) => { + e.stopPropagation(); + e.preventDefault(); + settingsStore.toggleTimeFormat() + } return (
+ + {!isAssist && (
+ + {timeStr} +
)} {location && (
+ const list = useMemo(() => resourceList.filter(res => !fetchList.some(ft => { if (res.url !== ft.url) { return false } if (Math.abs(res.time - ft.time) > 200) { return false } // TODO: find good epsilons @@ -228,7 +228,7 @@ function NetworkPanel() { const showDetailsModal = (item: any) => { setIsDetailsModalActive(true) showModal( - 0} />, + 0} />, { right: true, onClose: () => { @@ -366,7 +366,7 @@ function NetworkPanel() { hidden: activeTab === XHR, }, { - label: 'Time', + label: 'Duration', width: 80, dataKey: 'duration', render: renderDuration, @@ -380,4 +380,6 @@ function NetworkPanel() { ); } -export default observer(NetworkPanel); +export default connect((state: any) => ({ + startedAt: state.getIn(['sessions', 'current', 'startedAt']), +}))(observer(NetworkPanel)); diff --git a/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.tsx b/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.tsx index a9f90e723..be5386eab 100644 --- a/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.tsx +++ b/frontend/app/components/shared/FetchDetailsModal/FetchDetailsModal.tsx @@ -5,9 +5,11 @@ import FetchPluginMessage from './components/FetchPluginMessage'; import { TYPES } from 'Types/session/resource'; import FetchTabs from './components/FetchTabs/FetchTabs'; import { useStore } from 'App/mstore'; +import { DateTime } from 'luxon'; interface Props { resource: any; + time?: number; rows?: any; fetchPresented?: boolean; } @@ -47,7 +49,7 @@ function FetchDetailsModal(props: Props) { return (
Network Request
- + {isXHR && !fetchPresented && } {isXHR && } diff --git a/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx b/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx index 49e16c00f..94aadfb1d 100644 --- a/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx +++ b/frontend/app/components/shared/FetchDetailsModal/components/FetchBasicDetails/FetchBasicDetails.tsx @@ -5,8 +5,9 @@ import cn from 'classnames'; interface Props { resource: any; + timestamp?: string; } -function FetchBasicDetails({ resource }: Props) { +function FetchBasicDetails({ resource, timestamp }: Props) { const _duration = parseInt(resource.duration); const text = useMemo(() => { if (resource.url.length > 50) { @@ -69,12 +70,22 @@ function FetchBasicDetails({ resource }: Props) { {!!_duration && (
-
Time
+
Duration
{_duration} ms
)} + + {timestamp && ( +
+
Time
+
+ {timestamp} +
+
+ + )}
); } diff --git a/frontend/app/mstore/notesStore.ts b/frontend/app/mstore/notesStore.ts index 1cb041049..ea52cb2e7 100644 --- a/frontend/app/mstore/notesStore.ts +++ b/frontend/app/mstore/notesStore.ts @@ -44,7 +44,7 @@ export default class NotesStore { this.loading = true try { const notes = await notesService.getNotesBySessionId(sessionId) - this.sessionNotes = notes + this.setNotes(notes) return notes; } catch (e) { console.error(e) @@ -53,6 +53,10 @@ export default class NotesStore { } } + setNotes(notes: Note[]) { + this.sessionNotes = notes + } + async addNote(sessionId: string, note: WriteNote) { this.loading = true try { diff --git a/frontend/app/mstore/settingsStore.ts b/frontend/app/mstore/settingsStore.ts index 45cb9610c..b05c9ce58 100644 --- a/frontend/app/mstore/settingsStore.ts +++ b/frontend/app/mstore/settingsStore.ts @@ -8,6 +8,7 @@ export default class SettingsStore { sessionSettings: SessionSettings = new SessionSettings() captureRateFetched: boolean = false; limits: any = null; + isUniTs = false; constructor() { makeAutoObservable(this, { @@ -15,6 +16,10 @@ export default class SettingsStore { }) } + toggleTimeFormat = () => { + this.isUniTs = !this.isUniTs + } + saveCaptureRate(data: any) { return sessionService.saveCaptureRate(data) .then(data => data.json()) From d96ae9e2595076fb7585cb6dcb70dcb1ec96fc84 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 16:29:56 +0100 Subject: [PATCH 15/18] change(ui): add more tz, change abs/rel timest --- .../Session_/Player/Controls/Timeline.tsx | 7 ++-- .../Controls/components/TimeTooltip.tsx | 17 ++++++-- frontend/app/components/Session_/Subheader.js | 12 ------ .../FetchDetailsModal/FetchDetailsModal.tsx | 3 +- frontend/app/duck/sessions.js | 4 +- frontend/app/mstore/settingsStore.ts | 5 --- frontend/app/mstore/types/sessionSettings.ts | 39 +++++++++++++++---- 7 files changed, 53 insertions(+), 34 deletions(-) diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx index 5a47b1df2..89ce80e65 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx +++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx @@ -100,10 +100,11 @@ function Timeline(props) { }; } else { const time = getTime(e); + const tz = settingsStore.sessionSettings.timezone.value + const timeStr = DateTime.fromMillis(props.startedAt + time).setZone(tz).toFormat(`hh:mm:ss a`) timeLineTooltip = { - time: !settingsStore.isUniTs - ? Duration.fromMillis(time).toFormat(`mm:ss`) - : DateTime.fromMillis(props.startedAt + time).toFormat(`hh:mm:ss a`), + time: Duration.fromMillis(time).toFormat(`mm:ss`), + timeStr, offset: e.nativeEvent.offsetX, isVisible: true, }; diff --git a/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx b/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx index 5c4d19c0d..e47593b97 100644 --- a/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx +++ b/frontend/app/components/Session_/Player/Controls/components/TimeTooltip.tsx @@ -8,30 +8,39 @@ interface Props { time: number; offset: number; isVisible: boolean; + timeStr: string; } function TimeTooltip({ time, offset, isVisible, + timeStr, }: Props) { return (
{!time ? 'Loading' : time} + {timeStr ? ( + <> +
+ ({timeStr}) + + ) : null}
); } export default connect((state) => { - const { time = 0, offset = 0, isVisible } = state.getIn(['sessions', 'timeLineTooltip']); - return { time, offset, isVisible }; + const { time = 0, offset = 0, isVisible, timeStr } = state.getIn(['sessions', 'timeLineTooltip']); + return { time, offset, isVisible, timeStr }; })(TimeTooltip); diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index 45d5540bb..72ae28e3d 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -13,7 +13,6 @@ import { PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import AutoplayToggle from 'Shared/AutoplayToggle'; -import { Toggler } from 'UI'; function SubHeader(props) { const { player, store } = React.useContext(PlayerContext) @@ -28,7 +27,6 @@ function SubHeader(props) { eventList: eventsList, endTime, } = store.get() - const { settingsStore } = useStore() const mappedResourceList = resourceList .filter((r) => r.isRed() || r.isYellow()) @@ -56,19 +54,9 @@ function SubHeader(props) { showModal(, { right: true }); }; - const timeStr = settingsStore.isUniTs ? 'Local Time' : 'Relative Timestamp' - const onFormatCh = (e) => { - e.stopPropagation(); - e.preventDefault(); - settingsStore.toggleTimeFormat() - } return (
- {!isAssist && (
- - {timeStr} -
)} {location && (
{ @@ -49,7 +50,7 @@ function FetchDetailsModal(props: Props) { return (
Network Request
- + {isXHR && !fetchPresented && } {isXHR && } diff --git a/frontend/app/duck/sessions.js b/frontend/app/duck/sessions.js index ecce3d713..8afb6d073 100644 --- a/frontend/app/duck/sessions.js +++ b/frontend/app/duck/sessions.js @@ -70,7 +70,7 @@ const initialState = Map({ timelinePointer: null, sessionPath: {}, lastPlayedSessionId: null, - timeLineTooltip: { time: 0, offset: 0, isVisible: false }, + timeLineTooltip: { time: 0, offset: 0, isVisible: false, timeStr: '' }, createNoteTooltip: { time: 0, isVisible: false, isEdit: false, note: null }, }); @@ -454,4 +454,4 @@ export function updateLastPlayedSession(sessionId) { type: LAST_PLAYED_SESSION_ID, sessionId, }; -} \ No newline at end of file +} diff --git a/frontend/app/mstore/settingsStore.ts b/frontend/app/mstore/settingsStore.ts index b05c9ce58..45cb9610c 100644 --- a/frontend/app/mstore/settingsStore.ts +++ b/frontend/app/mstore/settingsStore.ts @@ -8,7 +8,6 @@ export default class SettingsStore { sessionSettings: SessionSettings = new SessionSettings() captureRateFetched: boolean = false; limits: any = null; - isUniTs = false; constructor() { makeAutoObservable(this, { @@ -16,10 +15,6 @@ export default class SettingsStore { }) } - toggleTimeFormat = () => { - this.isUniTs = !this.isUniTs - } - saveCaptureRate(data: any) { return sessionService.saveCaptureRate(data) .then(data => data.json()) diff --git a/frontend/app/mstore/types/sessionSettings.ts b/frontend/app/mstore/types/sessionSettings.ts index 95005b85d..bde66cdaf 100644 --- a/frontend/app/mstore/types/sessionSettings.ts +++ b/frontend/app/mstore/types/sessionSettings.ts @@ -13,27 +13,52 @@ const defaultDurationFilter = { countType: 'sec' } +const negativeExceptions = { + 4: ['-04:30'], + 3: ['-03:30'], + +} +const exceptions = { + 3: ['+03:30'], + 4: ['+04:30'], + 5: ['+05:30', '+05:45'], + 6: ['+06:30'], + 9: ['+09:30'] +} + export const generateGMTZones = (): Timezone[] => { const timezones: Timezone[] = []; - const positiveNumbers = [...Array(12).keys()]; - const negativeNumbers = [...Array(12).keys()].reverse(); + const positiveNumbers = [...Array(13).keys()]; + const negativeNumbers = [...Array(13).keys()].reverse(); negativeNumbers.pop(); // remove trailing zero since we have one in positive numbers array const combinedArray = [...negativeNumbers, ...positiveNumbers]; for (let i = 0; i < combinedArray.length; i++) { - let symbol = i < 11 ? '-' : '+'; - let isUTC = i === 11; - let value = String(combinedArray[i]).padStart(2, '0'); + let symbol = i < 12 ? '-' : '+'; + let isUTC = i === 12; + const item = combinedArray[i] + let value = String(item).padStart(2, '0'); - let tz = `UTC ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00`; + let tz = `UTC ${symbol}${String(item).padStart(2, '0')}:00`; let dropdownValue = `UTC${symbol}${value}`; timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue }); + + // @ts-ignore + const negativeMatch = negativeExceptions[item], positiveMatch = exceptions[item] + if (i < 11 && negativeMatch) { + negativeMatch.forEach((str: string) => { + timezones.push({ label: `UTC ${str}`, value: `UTC${str}`}) + }) + } else if (i > 11 && positiveMatch) { + positiveMatch.forEach((str: string) => { + timezones.push({ label: `UTC ${str}`, value: `UTC${str}`}) + }) + } } - timezones.splice(17, 0, { label: 'GMT +05:30', value: 'UTC+05:30' }); return timezones; }; From 3ac436bdb9bd7a30463eae44a9c37ddab0784609 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 16:38:37 +0100 Subject: [PATCH 16/18] change(player): dont show cursor icon if session is played on mobvile --- frontend/app/player/web/Screen/Cursor.ts | 3 ++- frontend/app/player/web/Screen/Screen.ts | 4 ++-- frontend/app/player/web/WebPlayer.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/app/player/web/Screen/Cursor.ts b/frontend/app/player/web/Screen/Cursor.ts index 4d8094b4e..83dba4ab1 100644 --- a/frontend/app/player/web/Screen/Cursor.ts +++ b/frontend/app/player/web/Screen/Cursor.ts @@ -5,9 +5,10 @@ import styles from './cursor.module.css'; export default class Cursor { private readonly cursor: HTMLDivElement; private tagElement: HTMLDivElement; - constructor(overlay: HTMLDivElement) { + constructor(overlay: HTMLDivElement, isMobile: boolean) { this.cursor = document.createElement('div'); this.cursor.className = styles.cursor; + if (isMobile) this.cursor.style.backgroundImage = 'unset' overlay.appendChild(this.cursor); } diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index b1ceff509..043be5357 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -57,7 +57,7 @@ export default class Screen { private readonly screen: HTMLDivElement; private parentElement: HTMLElement | null = null; - constructor() { + constructor(isMobile: boolean) { const iframe = document.createElement('iframe'); iframe.className = styles.iframe; this.iframe = iframe; @@ -73,7 +73,7 @@ export default class Screen { screen.appendChild(overlay); this.screen = screen; - this.cursor = new Cursor(this.overlay) // TODO: move outside + this.cursor = new Cursor(this.overlay, isMobile) // TODO: move outside } attach(parentElement: HTMLElement) { diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index 8df6214d4..e80964b0a 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -31,7 +31,7 @@ export default class WebPlayer extends Player { private targetMarker: TargetMarker constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { - + const isMobile = session.userOs === 'iPhone' || session.userOs === 'Android' let initialLists = live ? {} : { event: session.events.toJSON(), stack: session.stackEvents.toJSON(), @@ -46,7 +46,7 @@ export default class WebPlayer extends Player { ), } - const screen = new Screen() + const screen = new Screen(isMobile) const messageManager = new MessageManager(session, wpState, screen, initialLists) super(wpState, messageManager) this.screen = screen From 3a5a4e14722ade93934cc796b2a664892adfbf93 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 16:41:17 +0100 Subject: [PATCH 17/18] change(player): use ismobile --- frontend/app/player/web/WebPlayer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index e80964b0a..bd80c1189 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -31,7 +31,6 @@ export default class WebPlayer extends Player { private targetMarker: TargetMarker constructor(private wpState: Store, session, config: RTCIceServer[], live: boolean) { - const isMobile = session.userOs === 'iPhone' || session.userOs === 'Android' let initialLists = live ? {} : { event: session.events.toJSON(), stack: session.stackEvents.toJSON(), @@ -46,7 +45,7 @@ export default class WebPlayer extends Player { ), } - const screen = new Screen(isMobile) + const screen = new Screen(session.isMobile) const messageManager = new MessageManager(session, wpState, screen, initialLists) super(wpState, messageManager) this.screen = screen From f368ab624ed887fa0e1d48b133fea6a985c88238 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 16:55:15 +0100 Subject: [PATCH 18/18] change(player): add mobile animation --- frontend/app/player/web/Screen/Cursor.ts | 8 +++- .../app/player/web/Screen/cursor.module.css | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/frontend/app/player/web/Screen/Cursor.ts b/frontend/app/player/web/Screen/Cursor.ts index 83dba4ab1..f2d371062 100644 --- a/frontend/app/player/web/Screen/Cursor.ts +++ b/frontend/app/player/web/Screen/Cursor.ts @@ -5,11 +5,14 @@ import styles from './cursor.module.css'; export default class Cursor { private readonly cursor: HTMLDivElement; private tagElement: HTMLDivElement; + private isMobile: boolean; + constructor(overlay: HTMLDivElement, isMobile: boolean) { this.cursor = document.createElement('div'); this.cursor.className = styles.cursor; if (isMobile) this.cursor.style.backgroundImage = 'unset' overlay.appendChild(this.cursor); + this.isMobile = isMobile; } toggle(flag: boolean) { @@ -52,9 +55,10 @@ export default class Cursor { } click() { - this.cursor.classList.add(styles.clicked) + const styleList = this.isMobile ? styles.clickedMobile : styles.clicked + this.cursor.classList.add(styleList) setTimeout(() => { - this.cursor.classList.remove(styles.clicked) + this.cursor.classList.remove(styleList) }, 600) } diff --git a/frontend/app/player/web/Screen/cursor.module.css b/frontend/app/player/web/Screen/cursor.module.css index 7a94c99b8..93f3d05ff 100644 --- a/frontend/app/player/web/Screen/cursor.module.css +++ b/frontend/app/player/web/Screen/cursor.module.css @@ -67,3 +67,44 @@ transform: scale3d(1.2, 1.2, 1); } } + +.cursor.clickedMobile::after { + -webkit-animation: anim-effect-sanja 1s ease-out forwards; + animation: anim-effect-sanja 1s ease-out forwards; +} + +@-webkit-keyframes anim-effect-sanja { + 0% { + opacity: 1; + -webkit-transform: scale3d(0.5, 0.5, 1); + transform: scale3d(0.5, 0.5, 1); + } + 25% { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes anim-effect-sanja { + 0% { + opacity: 1; + -webkit-transform: scale3d(0.5, 0.5, 1); + transform: scale3d(0.5, 0.5, 1); + } + 25% { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +}