From 256c065153b3ab9081f7b23282c4a5f84242b570 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 26 Feb 2025 14:52:44 +0100 Subject: [PATCH 1/7] fix(ui): metadata reload on project config --- .../components/Client/Projects/ProjectCaptureRate.tsx | 10 ++++++++-- frontend/app/components/Client/Projects/Projects.tsx | 6 +++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx b/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx index 0f0d76938..f0464ab2a 100644 --- a/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx +++ b/frontend/app/components/Client/Projects/ProjectCaptureRate.tsx @@ -16,7 +16,7 @@ function ProjectCaptureRate(props: Props) { const [conditions, setConditions] = React.useState([]); const { projectId, platform } = props.project; const isMobile = platform !== 'web'; - const { settingsStore, userStore } = useStore(); + const { settingsStore, userStore, customFieldStore } = useStore(); const isAdmin = userStore.account.admin || userStore.account.superAdmin; const isEnterprise = userStore.isEnterprise; const [changed, setChanged] = useState(false); @@ -36,7 +36,13 @@ function ProjectCaptureRate(props: Props) { useEffect(() => { if (projectId) { setChanged(false); - void fetchCaptureConditions(projectId); + const fetchData = async () => { + if (isEnterprise) { + await customFieldStore.fetchListActive(projectId + ''); + } + void fetchCaptureConditions(projectId); + }; + void fetchData(); } }, [projectId]); diff --git a/frontend/app/components/Client/Projects/Projects.tsx b/frontend/app/components/Client/Projects/Projects.tsx index 06e8c64b1..5c78068b4 100644 --- a/frontend/app/components/Client/Projects/Projects.tsx +++ b/frontend/app/components/Client/Projects/Projects.tsx @@ -12,7 +12,7 @@ import ProjectForm from 'Components/Client/Projects/ProjectForm'; import Project from '@/mstore/types/project'; function Projects() { - const { projectsStore } = useStore(); + const { projectsStore, customFieldStore } = useStore(); const history = useHistory(); const { project, pid, tab } = projectsStore.config; const { openModal, closeModal } = useModal(); @@ -23,6 +23,10 @@ function Projects() { const tab = params.get('tab'); projectsStore.setConfigProject(pid ? parseInt(pid) : undefined); projectsStore.setConfigTab(tab); + + return () => { + void customFieldStore.fetchListActive(projectsStore.activeSiteId + ''); + } }, []); React.useEffect(() => { From d79665cbea3fed5db1d99ca8d1c50356e5201561 Mon Sep 17 00:00:00 2001 From: Kraiem Taha Yassine Date: Wed, 26 Feb 2025 15:59:10 +0100 Subject: [PATCH 2/7] fix(chalice): fixed delete/update metadata used in conditional recording (#3068) --- api/chalicelib/core/metadata.py | 21 ++++++++++----- api/chalicelib/core/projects.py | 43 +++++++++++++++++++++++++++++- ee/api/chalicelib/core/projects.py | 43 +++++++++++++++++++++++++++++- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/api/chalicelib/core/metadata.py b/api/chalicelib/core/metadata.py index fafe52c51..e761ab4f4 100644 --- a/api/chalicelib/core/metadata.py +++ b/api/chalicelib/core/metadata.py @@ -98,17 +98,23 @@ def __edit(project_id, col_index, colname, new_name): if col_index not in list(old_metas.keys()): return {"errors": ["custom field not found"]} - with pg_client.PostgresClient() as cur: - if old_metas[col_index]["key"] != new_name: + if old_metas[col_index]["key"] != new_name: + with pg_client.PostgresClient() as cur: query = cur.mogrify(f"""UPDATE public.projects SET {colname} = %(value)s WHERE project_id = %(project_id)s AND deleted_at ISNULL - RETURNING {colname};""", + RETURNING {colname}, + (SELECT {colname} FROM projects WHERE project_id = %(project_id)s) AS old_{colname};""", {"project_id": project_id, "value": new_name}) cur.execute(query=query) - new_name = cur.fetchone()[colname] + row = cur.fetchone() + new_name = row[colname] + old_name = row['old_' + colname] old_metas[col_index]["key"] = new_name + projects.rename_metadata_condition(project_id=project_id, + old_metadata_key=old_name, + new_metadata_key=new_name) return {"data": old_metas[col_index]} @@ -121,8 +127,8 @@ def edit(tenant_id, project_id, index: int, new_name: str): def delete(tenant_id, project_id, index: int): index = int(index) old_segments = get(project_id) - old_segments = [k["index"] for k in old_segments] - if index not in old_segments: + old_indexes = [k["index"] for k in old_segments] + if index not in old_indexes: return {"errors": ["custom field not found"]} with pg_client.PostgresClient() as cur: @@ -132,7 +138,8 @@ def delete(tenant_id, project_id, index: int): WHERE project_id = %(project_id)s AND deleted_at ISNULL;""", {"project_id": project_id}) cur.execute(query=query) - + projects.delete_metadata_condition(project_id=project_id, + metadata_key=old_segments[old_indexes.index(index)]["key"]) return {"data": get(project_id)} diff --git a/api/chalicelib/core/projects.py b/api/chalicelib/core/projects.py index fac4c0dbc..5d33028f8 100644 --- a/api/chalicelib/core/projects.py +++ b/api/chalicelib/core/projects.py @@ -413,7 +413,6 @@ def update_project_conditions(project_id, conditions): create_project_conditions(project_id, to_be_created) if to_be_updated: - logger.debug(to_be_updated) update_project_condition(project_id, to_be_updated) return get_conditions(project_id) @@ -428,3 +427,45 @@ def get_projects_ids(tenant_id): cur.execute(query=query) rows = cur.fetchall() return [r["project_id"] for r in rows] + + +def delete_metadata_condition(project_id, metadata_key): + sql = """\ + UPDATE public.projects_conditions + SET filters=(SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb) + FROM jsonb_array_elements(filters) AS elem + WHERE NOT (elem ->> 'type' = 'metadata' + AND elem ->> 'source' = %(metadata_key)s)) + WHERE project_id = %(project_id)s + AND jsonb_typeof(filters) = 'array' + AND EXISTS (SELECT 1 + FROM jsonb_array_elements(filters) AS elem + WHERE elem ->> 'type' = 'metadata' + AND elem ->> 'source' = %(metadata_key)s);""" + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(sql, {"project_id": project_id, "metadata_key": metadata_key}) + cur.execute(query) + + +def rename_metadata_condition(project_id, old_metadata_key, new_metadata_key): + sql = """\ + UPDATE public.projects_conditions + SET filters = (SELECT jsonb_agg(CASE + WHEN elem ->> 'type' = 'metadata' AND elem ->> 'source' = %(old_metadata_key)s + THEN elem || ('{"source": "'||%(new_metadata_key)s||'"}')::jsonb + ELSE elem END) + FROM jsonb_array_elements(filters) AS elem) + WHERE project_id = %(project_id)s + AND jsonb_typeof(filters) = 'array' + AND EXISTS (SELECT 1 + FROM jsonb_array_elements(filters) AS elem + WHERE elem ->> 'type' = 'metadata' + AND elem ->> 'source' = %(old_metadata_key)s);""" + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(sql, {"project_id": project_id, "old_metadata_key": old_metadata_key, + "new_metadata_key": new_metadata_key}) + cur.execute(query) + +# TODO: make project conditions use metadata-column-name instead of metadata-key diff --git a/ee/api/chalicelib/core/projects.py b/ee/api/chalicelib/core/projects.py index 499a2bc8a..aeac3fa97 100644 --- a/ee/api/chalicelib/core/projects.py +++ b/ee/api/chalicelib/core/projects.py @@ -427,7 +427,6 @@ def update_project_conditions(project_id, conditions): create_project_conditions(project_id, to_be_created) if to_be_updated: - print(to_be_updated) update_project_condition(project_id, to_be_updated) return get_conditions(project_id) @@ -486,3 +485,45 @@ def is_authorized_batch(project_ids, tenant_id): cur.execute(query=query) rows = cur.fetchall() return [r["project_id"] for r in rows] + + +def delete_metadata_condition(project_id, metadata_key): + sql = """\ + UPDATE public.projects_conditions + SET filters=(SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb) + FROM jsonb_array_elements(filters) AS elem + WHERE NOT (elem ->> 'type' = 'metadata' + AND elem ->> 'source' = %(metadata_key)s)) + WHERE project_id = %(project_id)s + AND jsonb_typeof(filters) = 'array' + AND EXISTS (SELECT 1 + FROM jsonb_array_elements(filters) AS elem + WHERE elem ->> 'type' = 'metadata' + AND elem ->> 'source' = %(metadata_key)s);""" + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(sql, {"project_id": project_id, "metadata_key": metadata_key}) + cur.execute(query) + + +def rename_metadata_condition(project_id, old_metadata_key, new_metadata_key): + sql = """\ + UPDATE public.projects_conditions + SET filters = (SELECT jsonb_agg(CASE + WHEN elem ->> 'type' = 'metadata' AND elem ->> 'source' = %(old_metadata_key)s + THEN elem || ('{"source": "'||%(new_metadata_key)s||'"}')::jsonb + ELSE elem END) + FROM jsonb_array_elements(filters) AS elem) + WHERE project_id = %(project_id)s + AND jsonb_typeof(filters) = 'array' + AND EXISTS (SELECT 1 + FROM jsonb_array_elements(filters) AS elem + WHERE elem ->> 'type' = 'metadata' + AND elem ->> 'source' = %(old_metadata_key)s);""" + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(sql, {"project_id": project_id, "old_metadata_key": old_metadata_key, + "new_metadata_key": new_metadata_key}) + cur.execute(query) + +# TODO: make project conditions use metadata-column-name instead of metadata-key From 6873f1c56b6892d23e51067ac75a49860f5c28f4 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Wed, 26 Feb 2025 17:33:13 +0100 Subject: [PATCH 3/7] ui: fix funnel wording --- .../app/components/Funnels/FunnelWidget/FunnelWidget.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx index 3a4365876..e57835d8a 100644 --- a/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx +++ b/frontend/app/components/Funnels/FunnelWidget/FunnelWidget.tsx @@ -88,13 +88,16 @@ function FunnelWidget(props: Props) { const viewType = metric?.viewType; const isHorizontal = viewType === 'columnChart'; const noEvents = metric.series[0].filter.filters.length === 0; + const isUsers = metric?.metricFormat === 'userCount'; return ( - {noEvents ? 'Select an event to start seeing the funnel' : 'No data available for the selected period.'} + {noEvents + ? 'Select an event to start seeing the funnel' + : 'No data available for the selected period.'} } show={!stages || stages.length === 0} @@ -142,7 +145,7 @@ function FunnelWidget(props: Props) {
Total conversion Date: Thu, 27 Feb 2025 09:41:49 +0100 Subject: [PATCH 4/7] ui: pick series by name if no id exist --- .../Dashboard/components/WidgetSessions/WidgetSessions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx index 8a4654397..36de7b09a 100644 --- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx @@ -50,10 +50,10 @@ function WidgetSessions(props: Props) { if (!widget.series) return; const seriesOptions = widget.series.map((item: any) => ({ label: item.name, - value: item.seriesId + value: item.seriesId ?? item.name })); setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]); - }, [widget.series]); + }, [widget.series.length]); const fetchSessions = (metricId: any, filter: any) => { if (!isMounted()) return; From c793d9d1778245107ee028217436189f3c3d4d7a Mon Sep 17 00:00:00 2001 From: Andrey Babushkin <55714097+reyand43@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:12:06 +0100 Subject: [PATCH 5/7] add handler (#3062) * aff handler * fix socket id handling --- assist/utils/assistHelper.js | 3 ++- assist/utils/socketHandlers.js | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/assist/utils/assistHelper.js b/assist/utils/assistHelper.js index c27c80963..ce90aa2a8 100644 --- a/assist/utils/assistHelper.js +++ b/assist/utils/assistHelper.js @@ -10,7 +10,8 @@ const EVENTS_DEFINITION = { UPDATE_EVENT: "UPDATE_SESSION", // tab become active/inactive, page title change, changed session object (rare case), call start/end CONNECT_ERROR: "connect_error", CONNECT_FAILED: "connect_failed", - ERROR: "error" + ERROR: "error", + WEBRTC_AGENT_CALL: "WEBRTC_AGENT_CALL", }, //The following list of events will be only emitted by the server server: { diff --git a/assist/utils/socketHandlers.js b/assist/utils/socketHandlers.js index a9a3e8a14..503969c08 100644 --- a/assist/utils/socketHandlers.js +++ b/assist/utils/socketHandlers.js @@ -127,6 +127,9 @@ async function onConnect(socket) { // Handle update event socket.on(EVENTS_DEFINITION.listen.UPDATE_EVENT, (...args) => onUpdateEvent(socket, ...args)); + // Handle webrtc events + socket.on(EVENTS_DEFINITION.listen.WEBRTC_AGENT_CALL, (...args) => onWebrtcAgentHandler(socket, ...args)); + // Handle errors socket.on(EVENTS_DEFINITION.listen.ERROR, err => errorHandler(EVENTS_DEFINITION.listen.ERROR, err)); socket.on(EVENTS_DEFINITION.listen.CONNECT_ERROR, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_ERROR, err)); @@ -186,6 +189,16 @@ async function onUpdateEvent(socket, ...args) { } } +async function onWebrtcAgentHandler(socket, ...args) { + if (socket.handshake.query.identity === IDENTITIES.agent) { + const agentIdToConnect = args[0]?.data?.toAgentId; + logger.debug(`${socket.id} sent webrtc event to agent:${agentIdToConnect}`); + if (agentIdToConnect && socket.handshake.sessionData.AGENTS_CONNECTED.includes(agentIdToConnect)) { + socket.to(agentIdToConnect).emit(EVENTS_DEFINITION.listen.WEBRTC_AGENT_CALL, args[0]); + } + } +} + async function onAny(socket, eventName, ...args) { if (Object.values(EVENTS_DEFINITION.listen).indexOf(eventName) >= 0) { logger.debug(`received event:${eventName}, should be handled by another listener, stopping onAny.`); From fd76f7c302abd64805b4fa0ff341092bdec3ecfb Mon Sep 17 00:00:00 2001 From: Andrey Babushkin <55714097+reyand43@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:12:27 +0100 Subject: [PATCH 6/7] Migrate to webrtc (#3051) * resolved conflicts * resolved conflicts * translated comments * changed console.log message lang * changed console to logs * implementing conference call * add isAgent flag * add webrtc handlers * add conference call * removed conference calls * fix lint error --------- Co-authored-by: Andrey Babushkin --- .../Assist/ChatWindow/ChatWindow.tsx | 7 +- .../AssistActions/AssistActions.tsx | 28 +- .../VideoContainer/VideoContainer.tsx | 25 +- .../LivePlayer/LivePlayerBlockHeader.tsx | 2 +- frontend/app/player/web/WebLivePlayer.ts | 1 + .../app/player/web/assist/AssistManager.ts | 25 +- frontend/app/player/web/assist/Call.ts | 479 +- .../app/player/web/assist/CanvasReceiver.ts | 124 +- frontend/package.json | 1 - frontend/yarn.lock | 3375 ++--- tracker/tracker-assist/.yarn/install-state.gz | Bin 549988 -> 515844 bytes tracker/tracker-assist/layout/index-chat.html | 933 +- tracker/tracker-assist/layout/index.html | 5 +- tracker/tracker-assist/package.json | 1 - tracker/tracker-assist/src/Assist.ts | 599 +- tracker/tracker-assist/src/CallWindow.ts | 7 +- tracker/tracker-assist/src/RemoteControl.ts | 10 +- tracker/tracker-redux/.pnp.cjs | 10418 ++++++++++++++++ tracker/tracker-redux/.pnp.loader.mjs | 2126 ++++ tracker/tracker-redux/.yarn/install-state.gz | Bin 0 -> 195852 bytes tracker/tracker-redux/package.json | 12 +- 21 files changed, 15646 insertions(+), 2532 deletions(-) create mode 100755 tracker/tracker-redux/.pnp.cjs create mode 100644 tracker/tracker-redux/.pnp.loader.mjs create mode 100644 tracker/tracker-redux/.yarn/install-state.gz diff --git a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx index 29e4826cc..e9e5b8ea2 100644 --- a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx +++ b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx @@ -9,7 +9,7 @@ import type { LocalStream } from 'Player'; import { PlayerContext } from 'App/components/Session/playerContext'; export interface Props { - incomeStream: MediaStream[] | null; + incomeStream: { stream: MediaStream, isAgent: boolean }[] | null; localStream: LocalStream | null; userId: string; isPrestart?: boolean; @@ -50,8 +50,8 @@ function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: > {incomeStream ? ( incomeStream.map((stream) => ( - - + + )) ) : ( @@ -62,6 +62,7 @@ function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: stream={localStream ? localStream.stream : null} muted height={anyRemoteEnabled ? 50 : 'unset'} + local />
diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index 40281b8a9..88317ac79 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -80,7 +80,7 @@ function AssistActions({ } = store.get(); const [isPrestart, setPrestart] = useState(false); - const [incomeStream, setIncomeStream] = useState([]); + const [incomeStream, setIncomeStream] = useState<{ stream: MediaStream; isAgent: boolean }[] | null>([]); const [localStream, setLocalStream] = useState(null); const [callObject, setCallObject] = useState<{ end: () => void } | null>(null); @@ -131,18 +131,25 @@ function AssistActions({ } }, [peerConnectionStatus]); - const addIncomeStream = (stream: MediaStream) => { + const addIncomeStream = (stream: MediaStream, isAgent: boolean) => { setIncomeStream((oldState) => { - if (oldState === null) return [stream]; - if (!oldState.find((existingStream) => existingStream.id === stream.id)) { + if (oldState === null) return [{ stream, isAgent }]; + if (!oldState.find((existingStream) => existingStream.stream.id === stream.id)) { audioContextManager.mergeAudioStreams(stream); - return [...oldState, stream]; + return [...oldState, { stream, isAgent }]; } return oldState; }); }; - function call(additionalAgentIds?: string[]) { + const removeIncomeStream = (stream: MediaStream) => { + setIncomeStream((prevState) => { + if (!prevState) return []; + return prevState.filter((existingStream) => existingStream.stream.id !== stream.id); + }); + }; + + function call() { RequestLocalStream() .then((lStream) => { setLocalStream(lStream); @@ -152,15 +159,16 @@ function AssistActions({ addIncomeStream, () => { player.assistManager.ping(AssistActionsPing.call.end, agentId) - lStream.stop.bind(lStream); + lStream.stop.apply(lStream); + removeIncomeStream(lStream.stream); }, onReject, onError ); setCallObject(callPeer()); - if (additionalAgentIds) { - callPeer(additionalAgentIds); - } + // if (additionalAgentIds) { + // callPeer(additionalAgentIds); + // } }) .catch(onError); } diff --git a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx index 97b8c9f9e..4d8b9ece4 100644 --- a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx +++ b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx @@ -5,9 +5,18 @@ interface Props { muted?: boolean; height?: number | string; setRemoteEnabled?: (isEnabled: boolean) => void; + local?: boolean; + isAgent?: boolean; } -function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled }: Props) { +function VideoContainer({ + stream, + muted = false, + height = 280, + setRemoteEnabled, + local, + isAgent, +}: Props) { const ref = useRef(null); const [isEnabled, setEnabled] = React.useState(false); @@ -15,14 +24,14 @@ function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled if (ref.current) { ref.current.srcObject = stream; } - }, [ref.current, stream, stream.getVideoTracks()[0]?.getSettings().width]); + }, [ref.current, stream, stream?.getVideoTracks()[0]?.getSettings().width]); useEffect(() => { if (!stream) { return; } const iid = setInterval(() => { - const track = stream.getVideoTracks()[0] + const track = stream.getVideoTracks()[0]; const settings = track?.getSettings(); const isDummyVideoTrack = settings ? settings.width === 2 || @@ -47,9 +56,19 @@ function VideoContainer({ stream, muted = false, height = 280, setRemoteEnabled width: isEnabled ? undefined : '0px!important', height: isEnabled ? undefined : '0px!important', border: '1px solid grey', + transform: local ? 'scaleX(-1)' : undefined, }} >