Compare commits

..

56 commits

Author SHA1 Message Date
nick-delirium
90510aa33b ui: fix double metric selection in list 2025-06-06 16:19:54 +02:00
GitHub Action
96a70f5d41 Increment frontend chart version to v1.22.42 2025-06-04 11:41:56 +02:00
rjshrjndrn
d4a13edcf0 fix(actions): frontend image with proper tag
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-06-04 11:33:19 +02:00
GitHub Action
51fad91a22 Increment frontend chart version to v1.22.41 2025-06-04 10:48:50 +02:00
nick-delirium
36abcda1e1 ui: fix audioplayer start point 2025-06-04 10:39:08 +02:00
Mehdi Osman
dd5f464f73
Increment frontend chart version to v1.22.40 (#3479)
Co-authored-by: GitHub Action <action@github.com>
2025-06-03 16:22:12 +02:00
Delirium
f9ada41272
ui: recreate period on db visit (#3478) 2025-06-03 16:05:52 +02:00
rjshrjndrn
9e24a3583e feat(nginx): add integrations endpoint with CORS support
Add new /integrations/ location block that proxies requests to
integrations-openreplay:8080 service. Includes proper CORS headers
for cross-origin requests and WebSocket upgrade support.

- Rewrite /integrations/ path to root
- Configure proxy headers for forwarding
- Set connection timeouts for stability
- Add CORS headers for API access

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-06-02 10:55:50 +02:00
Taha Yassine Kraiem
0a3129d3cd fix(chalice): fixed JIRA integration 2025-05-30 15:25:41 +02:00
Mehdi Osman
99d61db9d9
Increment frontend chart version to v1.22.39 (#3460)
Co-authored-by: GitHub Action <action@github.com>
2025-05-30 15:07:29 +02:00
Delirium
133958622e
ui: fix alert create button (#3459) 2025-05-30 14:56:21 +02:00
GitHub Action
fb021f606f Increment frontend chart version to v1.22.38 2025-05-29 12:21:04 +02:00
rjshrjndrn
a2905fa8ed fix: move cd - command after git operations in patch workflow
Move the directory restoration command after the git operations to
ensure all git commands execute in the correct working directory
before returning to the previous directory.

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-29 12:16:28 +02:00
rjshrjndrn
beec2283fd refactor(ci): restructure patch-build workflow script
- Extract inline bash script into structured functions
- Add proper error handling with set -euo pipefail
- Improve variable scoping with readonly and local declarations
- Add descriptive function names and comments
- Fix shell quoting and parameter expansion
- Consolidate build logic into reusable functions
- Add proper cleanup of temporary files
- Improve readability and maintainability of the CI script

The refactored script maintains the same functionality while being
more robust and easier to understand.

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-29 12:16:28 +02:00
GitHub Action
6c8b55019e Increment frontend chart version 2025-05-29 10:29:46 +02:00
rjshrjndrn
e3e3e11227 fix(action): proper registry
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-29 10:18:55 +02:00
Shekar Siri
c6f7de04cc Revert "fix(ui): new card data state is not updating"
This reverts commit 2921c17cbf.
2025-05-28 22:16:00 +02:00
Shekar Siri
2921c17cbf fix(ui): new card data state is not updating 2025-05-28 19:49:01 +02:00
Mehdi Osman
7eb3f5c4c8
Increment frontend chart version (#3436)
Co-authored-by: GitHub Action <action@github.com>
2025-05-26 16:10:35 +02:00
Rajesh Rajendran
5a9a8e588a
chore(actions): rebase only if not main (#3435)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-26 16:04:50 +02:00
Rajesh Rajendran
4b14258266
fix(action): clone repo (#3433)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-26 15:50:13 +02:00
Rajesh Rajendran
744d2d4311
actions fix or 2070 (#3432)
* chore(build): Better error handling

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>

* fix(build): remove fetch depth, as it might cause issue in rebase

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>

* fix(build): proper platform

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>

---------

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-26 15:45:48 +02:00
Taha Yassine Kraiem
64242a5dc0 refactor(DB): changed supported platforms in CH 2025-05-26 11:51:49 +02:00
Rajesh Rajendran
cae3002697
feat(ci): Support building from branch for old patch (#3419)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-20 15:19:04 +02:00
GitHub Action
3d3c62196b Increment frontend chart version 2025-05-20 11:44:16 +02:00
nick-delirium
e810958a5d ui: fix ant imports 2025-05-20 11:26:20 +02:00
nick-delirium
39fa9787d1 ui: prevent network row modal from changing replayer time 2025-05-20 11:21:50 +02:00
nick-delirium
c9c1ad4dde ui: comments etc 2025-05-20 11:21:50 +02:00
nick-delirium
d9868928be ui: improve network panel row mapping 2025-05-20 11:21:50 +02:00
GitHub Action
a460d8c9a2 Increment frontend chart version 2025-05-15 15:18:19 +02:00
nick-delirium
930417aab4 ui: fix session search on url change 2025-05-15 15:12:30 +02:00
GitHub Action
07bc184f4d Increment chalice chart version 2025-05-14 18:59:43 +02:00
Rajesh Rajendran
71b7cca569
Patch/api v1.22.0 (#3401)
* fix(chalice): fixed duplicate autocomplete values

* ci(actions): possible fix for pull --rebase

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>

---------

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
Co-authored-by: Taha Yassine Kraiem <tahayk2@gmail.com>
2025-05-14 18:42:25 +02:00
Mehdi Osman
355d27eaa0
Increment frontend chart version (#3397)
Co-authored-by: GitHub Action <action@github.com>
2025-05-13 13:38:15 +02:00
Mehdi Osman
66b485cccf
Increment db chart version (#3396)
Co-authored-by: GitHub Action <action@github.com>
2025-05-13 10:34:28 +02:00
Alexander
de33a42151
feat(db): custom event's ts (#3395) 2025-05-12 17:52:24 +02:00
Rajesh Rajendran
f12bdebf82
ci(actions): fix push denied (#3392) (#3393) (#3394)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-12 17:19:41 +02:00
Rajesh Rajendran
bbfa20c693
ci(actions): fix push denied (#3392) (#3393)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-12 16:58:19 +02:00
Rajesh Rajendran
f264ba043d
ci(actions): fix push denied (#3392)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-12 16:55:23 +02:00
Rajesh Rajendran
a05dce8125
main (#3391)
* ci(actions): Update pr description

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>

* ci(actions): run only on pull request merge

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>

---------

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-12 16:50:20 +02:00
Mehdi Osman
3a1635d81f
Increment frontend chart version (#3389)
Co-authored-by: GitHub Action <action@github.com>
2025-05-12 16:12:43 +02:00
Delirium
ccb332c636
ui: change <slot> check (#3388) 2025-05-12 16:02:26 +02:00
Rajesh Rajendran
80ffa15959
ci(actions): Auto update tag for patch build (#3387)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-12 15:54:10 +02:00
Rajesh Rajendran
b2e961d621
ci(actions): Auto update tag for patch build (#3386)
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-05-12 15:49:19 +02:00
Mehdi Osman
b4d0598f23
Increment frontend chart version (#3385)
Co-authored-by: GitHub Action <action@github.com>
2025-05-12 15:46:29 +02:00
Delirium
e77f083f10
ui: fixup toggler closing (#3384) 2025-05-12 15:40:30 +02:00
Delirium
58da1d3f64
fix litjs support, fix autocomplete modal options reset, fix dashboard chart density (#3382)
* Litjs fixes2 (#3381)

* ui: fixes for litjs capture

* ui: introduce vmode for lwc light dom

* ui: fixup the mode toggle and remover

* ui: fix filter options reset, fix dashboard chart density
2025-05-12 15:27:44 +02:00
GitHub Action
447fc26a2a Increment frontend chart version 2025-05-12 10:46:33 +02:00
nick-delirium
9bdf6e4f92 ui: fix heatmaps crash 2025-05-12 10:37:48 +02:00
GitHub Action
01f403e12d Increment chalice chart version 2025-05-07 12:28:44 +02:00
Taha Yassine Kraiem
39eb943b86 fix(chalice): fixed get error's details 2025-05-07 12:15:33 +02:00
GitHub Action
366b0d38b0 Increment frontend chart version 2025-05-06 16:28:28 +02:00
nick-delirium
f4d5b3c06e ui: fix max meta length, add horizontal layout for player 2025-05-06 16:23:47 +02:00
Mehdi Osman
93ae18133e
Increment frontend chart version (#3366)
Co-authored-by: GitHub Action <action@github.com>
2025-05-06 13:16:57 +02:00
Andrey Babushkin
fbe5d78270
Revert update (#3365)
* Revert "Increment chalice chart version"

This reverts commit 5e0e5730ba.

* revert updates

* changed chalice version
2025-05-06 13:08:08 +02:00
Mehdi Osman
b803eed1d4
Increment frontend chart version (#3362)
Co-authored-by: GitHub Action <action@github.com>
2025-05-05 17:49:39 +02:00
66 changed files with 1043 additions and 549 deletions

View file

@ -8,7 +8,11 @@ on:
required: true
default: 'chalice,frontend'
tag:
description: 'Tag to build patches from.'
description: 'Tag to update.'
required: true
type: string
branch:
description: 'Branch to build patches from. Make sure the branch is uptodate with tag. Else itll cause missing commits.'
required: true
type: string
@ -73,7 +77,7 @@ jobs:
- name: Get HEAD Commit ID
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
- name: Define Branch Name
run: echo "BRANCH_NAME=patch/main/${HEAD_COMMIT_ID}" >> $GITHUB_ENV
run: echo "BRANCH_NAME=${{inputs.branch}}" >> $GITHUB_ENV
- name: Build
id: build-image

View file

@ -2,7 +2,6 @@
on:
workflow_dispatch:
description: 'This workflow will build for patches for latest tag, and will Always use commit from main branch.'
inputs:
services:
description: 'Comma separated names of services to build(in small letters).'
@ -20,12 +19,20 @@ jobs:
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 1
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Rebase with main branch, to make sure the code has latest main changes
if: github.ref != 'refs/heads/main'
run: |
git pull --rebase origin main
git remote -v
git config --global user.email "action@github.com"
git config --global user.name "GitHub Action"
git config --global rebase.autoStash true
git fetch origin main:main
git rebase main
git log -3
- name: Downloading yq
run: |
@ -48,6 +55,8 @@ jobs:
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
- uses: depot/setup-action@v1
env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
- name: Get HEAD Commit ID
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
- name: Define Branch Name
@ -65,78 +74,168 @@ jobs:
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
MSAAS_REPO_FOLDER: /tmp/msaas
SERVICES_INPUT: ${{ github.event.inputs.services }}
run: |
set -exo pipefail
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git checkout -b $BRANCH_NAME
working_dir=$(pwd)
function image_version(){
local service=$1
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
current_version=$(yq eval '.AppVersion' $chart_path)
new_version=$(echo $current_version | awk -F. '{$NF += 1 ; print $1"."$2"."$3}')
echo $new_version
# yq eval ".AppVersion = \"$new_version\"" -i $chart_path
#!/bin/bash
set -euo pipefail
# Configuration
readonly WORKING_DIR=$(pwd)
readonly BUILD_SCRIPT_NAME="build.sh"
readonly BACKEND_SERVICES_FILE="/tmp/backend.txt"
# Initialize git configuration
setup_git() {
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git checkout -b "$BRANCH_NAME"
}
function clone_msaas() {
[ -d $MSAAS_REPO_FOLDER ] || {
git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
cd $MSAAS_REPO_FOLDER
cd openreplay && git fetch origin && git checkout main # This have to be changed to specific tag
git log -1
cd $MSAAS_REPO_FOLDER
bash git-init.sh
git checkout
}
# Get and increment image version
image_version() {
local service=$1
local chart_path="$WORKING_DIR/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
local current_version new_version
current_version=$(yq eval '.AppVersion' "$chart_path")
new_version=$(echo "$current_version" | awk -F. '{$NF += 1; print $1"."$2"."$3}')
echo "$new_version"
}
function build_managed() {
local service=$1
local version=$2
echo building managed
clone_msaas
if [[ $service == 'chalice' ]]; then
cd $MSAAS_REPO_FOLDER/openreplay/api
else
cd $MSAAS_REPO_FOLDER/openreplay/$service
fi
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt
# Clone MSAAS repository if not exists
clone_msaas() {
if [[ ! -d "$MSAAS_REPO_FOLDER" ]]; then
git clone -b dev --recursive "https://x-access-token:${MSAAS_REPO_CLONE_TOKEN}@${MSAAS_REPO_URL}" "$MSAAS_REPO_FOLDER"
cd "$MSAAS_REPO_FOLDER"
cd openreplay && git fetch origin && git checkout main
git log -1
cd "$MSAAS_REPO_FOLDER"
bash git-init.sh
git checkout
fi
}
# Checking for backend images
ls backend/cmd >> /tmp/backend.txt
echo Services: "${{ github.event.inputs.services }}"
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
BUILD_SCRIPT_NAME="build.sh"
# Build FOSS
for SERVICE in "${SERVICES[@]}"; do
# Check if service is backend
if grep -q $SERVICE /tmp/backend.txt; then
cd backend
foss_build_args="nil $SERVICE"
ee_build_args="ee $SERVICE"
else
[[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE
[[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh"
ee_build_args="ee"
fi
version=$(image_version $SERVICE)
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
echo IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
if [[ "$SERVICE" != "chalice" && "$SERVICE" != "frontend" ]]; then
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
else
build_managed $SERVICE $version
fi
cd $working_dir
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$SERVICE/Chart.yaml"
yq eval ".AppVersion = \"$version\"" -i $chart_path
git add $chart_path
git commit -m "Increment $SERVICE chart version"
git push --set-upstream origin $BRANCH_NAME
done
# Build managed services
build_managed() {
local service=$1
local version=$2
echo "Building managed service: $service"
clone_msaas
if [[ $service == 'chalice' ]]; then
cd "$MSAAS_REPO_FOLDER/openreplay/api"
else
cd "$MSAAS_REPO_FOLDER/openreplay/$service"
fi
local build_cmd="IMAGE_TAG=$version DOCKER_RUNTIME=depot DOCKER_BUILD_ARGS=--push ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh"
echo "Executing: $build_cmd"
if ! eval "$build_cmd" 2>&1; then
echo "Build failed for $service"
exit 1
fi
}
# Build service with given arguments
build_service() {
local service=$1
local version=$2
local build_args=$3
local build_script=${4:-$BUILD_SCRIPT_NAME}
local command="IMAGE_TAG=$version DOCKER_RUNTIME=depot DOCKER_BUILD_ARGS=--push ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash $build_script $build_args"
echo "Executing: $command"
eval "$command"
}
# Update chart version and commit changes
update_chart_version() {
local service=$1
local version=$2
local chart_path="$WORKING_DIR/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
# Ensure we're in the original working directory/repository
cd "$WORKING_DIR"
yq eval ".AppVersion = \"$version\"" -i "$chart_path"
git add "$chart_path"
git commit -m "Increment $service chart version to $version"
git push --set-upstream origin "$BRANCH_NAME"
cd -
}
# Main execution
main() {
setup_git
# Get backend services list
ls backend/cmd >"$BACKEND_SERVICES_FILE"
# Parse services input (fix for GitHub Actions syntax)
echo "Services: ${SERVICES_INPUT:-$1}"
IFS=',' read -ra services <<<"${SERVICES_INPUT:-$1}"
# Process each service
for service in "${services[@]}"; do
echo "Processing service: $service"
cd "$WORKING_DIR"
local foss_build_args="" ee_build_args="" build_script="$BUILD_SCRIPT_NAME"
# Determine build configuration based on service type
if grep -q "$service" "$BACKEND_SERVICES_FILE"; then
# Backend service
cd backend
foss_build_args="nil $service"
ee_build_args="ee $service"
else
# Non-backend service
case "$service" in
chalice | alerts | crons)
cd "$WORKING_DIR/api"
;;
*)
cd "$service"
;;
esac
# Special build scripts for alerts/crons
if [[ $service == 'alerts' || $service == 'crons' ]]; then
build_script="build_${service}.sh"
fi
ee_build_args="ee"
fi
# Get version and build
local version
version=$(image_version "$service")
# Build FOSS and EE versions
build_service "$service" "$version" "$foss_build_args"
build_service "$service" "${version}-ee" "$ee_build_args"
# Build managed version for specific services
if [[ "$service" != "chalice" && "$service" != "frontend" ]]; then
echo "Nothing to build in managed for service $service"
else
build_managed "$service" "$version"
fi
# Update chart and commit
update_chart_version "$service" "$version"
done
cd "$WORKING_DIR"
# Cleanup
rm -f "$BACKEND_SERVICES_FILE"
}
echo "Working directory: $WORKING_DIR"
# Run main function with all arguments
main "$SERVICES_INPUT"
- name: Create Pull Request
uses: repo-sync/pull-request@v2
@ -147,8 +246,7 @@ jobs:
pr_title: "Updated patch build from main ${{ env.HEAD_COMMIT_ID }}"
pr_body: |
This PR updates the Helm chart version after building the patch from $HEAD_COMMIT_ID.
Once this PR is merged, To update the latest tag, run the following workflow.
https://github.com/openreplay/openreplay/actions/workflows/update-tag.yaml
Once this PR is merged, tag update job will run automatically.
# - name: Debug Job
# if: ${{ failure() }}

View file

@ -1,35 +1,42 @@
on:
workflow_dispatch:
description: "This workflow will build for patches for latest tag, and will Always use commit from main branch."
inputs:
services:
description: "This action will update the latest tag with current main branch HEAD. Should I proceed ? true/false"
required: true
default: "false"
name: Force Push tag with main branch HEAD
pull_request:
types: [closed]
branches:
- main
name: Release tag update --force
jobs:
deploy:
name: Build Patch from main
runs-on: ubuntu-latest
env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || github.event.inputs.services == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get latest release tag using GitHub API
id: get-latest-tag
run: |
LATEST_TAG=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/latest" \
| jq -r .tag_name)
# Fallback to git command if API doesn't return a tag
if [ "$LATEST_TAG" == "null" ] || [ -z "$LATEST_TAG" ]; then
echo "Not found latest tag"
exit 100
fi
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
echo "Latest tag: $LATEST_TAG"
- name: Set Remote with GITHUB_TOKEN
run: |
git config --unset http.https://github.com/.extraheader
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}
- name: Push main branch to tag
run: |
git fetch --tags
git checkout main
git push origin HEAD:refs/tags/$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1) --force
# - name: Debug Job
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
# with:
# limit-access-to-actor: true
echo "Updating tag ${{ env.LATEST_TAG }} to point to latest commit on main"
git push origin HEAD:refs/tags/${{ env.LATEST_TAG }} --force

View file

@ -85,7 +85,8 @@ def __generic_query(typename, value_length=None):
ORDER BY value"""
if value_length is None or value_length > 2:
return f"""(SELECT DISTINCT value, type
return f"""SELECT DISTINCT ON(value,type) value, type
((SELECT DISTINCT value, type
FROM {TABLE}
WHERE
project_id = %(project_id)s
@ -101,7 +102,7 @@ def __generic_query(typename, value_length=None):
AND type='{typename.upper()}'
AND value ILIKE %(value)s
ORDER BY value
LIMIT 5);"""
LIMIT 5)) AS raw;"""
return f"""SELECT DISTINCT value, type
FROM {TABLE}
WHERE
@ -326,7 +327,7 @@ def __search_metadata(project_id, value, key=None, source=None):
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
with pg_client.PostgresClient() as cur:
cur.execute(cur.mogrify(f"""\
SELECT key, value, 'METADATA' AS TYPE
SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
LIMIT 5;""", {"project_id": project_id, "value": helper.string_to_sql_like(value),
"svalue": helper.string_to_sql_like("^" + value)}))

View file

@ -50,8 +50,8 @@ class JIRAIntegration(base.BaseIntegration):
cur.execute(
cur.mogrify(
"""SELECT username, token, url
FROM public.jira_cloud
WHERE user_id=%(user_id)s;""",
FROM public.jira_cloud
WHERE user_id = %(user_id)s;""",
{"user_id": self._user_id})
)
data = helper.dict_to_camel_case(cur.fetchone())
@ -95,10 +95,9 @@ class JIRAIntegration(base.BaseIntegration):
def add(self, username, token, url, obfuscate=False):
with pg_client.PostgresClient() as cur:
cur.execute(
cur.mogrify("""\
INSERT INTO public.jira_cloud(username, token, user_id,url)
VALUES (%(username)s, %(token)s, %(user_id)s,%(url)s)
RETURNING username, token, url;""",
cur.mogrify(""" \
INSERT INTO public.jira_cloud(username, token, user_id, url)
VALUES (%(username)s, %(token)s, %(user_id)s, %(url)s) RETURNING username, token, url;""",
{"user_id": self._user_id, "username": username,
"token": token, "url": url})
)
@ -112,9 +111,10 @@ class JIRAIntegration(base.BaseIntegration):
def delete(self):
with pg_client.PostgresClient() as cur:
cur.execute(
cur.mogrify("""\
DELETE FROM public.jira_cloud
WHERE user_id=%(user_id)s;""",
cur.mogrify(""" \
DELETE
FROM public.jira_cloud
WHERE user_id = %(user_id)s;""",
{"user_id": self._user_id})
)
return {"state": "success"}
@ -125,7 +125,7 @@ class JIRAIntegration(base.BaseIntegration):
changes={
"username": data.username,
"token": data.token if len(data.token) > 0 and data.token.find("***") == -1 \
else self.integration.token,
else self.integration["token"],
"url": str(data.url)
},
obfuscate=True

View file

@ -2,11 +2,12 @@ package datasaver
import (
"context"
"encoding/json"
"openreplay/backend/pkg/db/types"
"openreplay/backend/internal/config/db"
"openreplay/backend/pkg/db/clickhouse"
"openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/db/types"
"openreplay/backend/pkg/logger"
. "openreplay/backend/pkg/messages"
queue "openreplay/backend/pkg/queue/types"
@ -50,10 +51,6 @@ func New(log logger.Logger, cfg *db.Config, pg *postgres.Conn, ch clickhouse.Con
}
func (s *saverImpl) Handle(msg Message) {
if msg.TypeID() == MsgCustomEvent {
defer s.Handle(types.WrapCustomEvent(msg.(*CustomEvent)))
}
var (
sessCtx = context.WithValue(context.Background(), "sessionID", msg.SessionID())
session *sessions.Session
@ -69,6 +66,23 @@ func (s *saverImpl) Handle(msg Message) {
return
}
if msg.TypeID() == MsgCustomEvent {
m := msg.(*CustomEvent)
// Try to parse custom event payload to JSON and extract or_payload field
type CustomEventPayload struct {
CustomTimestamp uint64 `json:"or_timestamp"`
}
customPayload := &CustomEventPayload{}
if err := json.Unmarshal([]byte(m.Payload), customPayload); err == nil {
if customPayload.CustomTimestamp >= session.Timestamp {
s.log.Info(sessCtx, "custom event timestamp received: %v", m.Timestamp)
msg.Meta().Timestamp = customPayload.CustomTimestamp
s.log.Info(sessCtx, "custom event timestamp updated: %v", m.Timestamp)
}
}
defer s.Handle(types.WrapCustomEvent(m))
}
if IsMobileType(msg.TypeID()) {
if err := s.handleMobileMessage(sessCtx, session, msg); err != nil {
if !postgres.IsPkeyViolation(err) {

View file

@ -86,7 +86,8 @@ def __generic_query(typename, value_length=None):
ORDER BY value"""
if value_length is None or value_length > 2:
return f"""(SELECT DISTINCT value, type
return f"""SELECT DISTINCT ON(value, type) value, type
FROM ((SELECT DISTINCT value, type
FROM {TABLE}
WHERE
project_id = %(project_id)s
@ -102,7 +103,7 @@ def __generic_query(typename, value_length=None):
AND type='{typename.upper()}'
AND value ILIKE %(value)s
ORDER BY value
LIMIT 5);"""
LIMIT 5)) AS raw;"""
return f"""SELECT DISTINCT value, type
FROM {TABLE}
WHERE
@ -257,7 +258,7 @@ def __search_metadata(project_id, value, key=None, source=None):
WHERE project_id = %(project_id)s
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
with ch_client.ClickHouseClient() as cur:
query = cur.format(query=f"""SELECT key, value, 'METADATA' AS TYPE
query = cur.format(query=f"""SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
LIMIT 5;""", parameters={"project_id": project_id, "value": helper.string_to_sql_like(value),
"svalue": helper.string_to_sql_like("^" + value)})

View file

@ -71,7 +71,7 @@ def get_details(project_id, error_id, user_id, **data):
MAIN_EVENTS_TABLE = exp_ch_helper.get_main_events_table(0)
ch_basic_query = errors_helper.__get_basic_constraints_ch(time_constraint=False)
ch_basic_query.append("toString(`$properties`.error_id) = %(error_id)s")
ch_basic_query.append("error_id = %(error_id)s")
with ch_client.ClickHouseClient() as ch:
data["startDate24"] = TimeUTC.now(-1)
@ -95,7 +95,7 @@ def get_details(project_id, error_id, user_id, **data):
"error_id": error_id}
main_ch_query = f"""\
WITH pre_processed AS (SELECT toString(`$properties`.error_id) AS error_id,
WITH pre_processed AS (SELECT error_id,
toString(`$properties`.name) AS name,
toString(`$properties`.message) AS message,
session_id,
@ -183,7 +183,7 @@ def get_details(project_id, error_id, user_id, **data):
AND `$event_name` = 'ERROR'
AND events.created_at >= toDateTime(timestamp / 1000)
AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000)
AND toString(`$properties`.error_id) = %(error_id)s
AND error_id = %(error_id)s
GROUP BY timestamp
ORDER BY timestamp) AS chart_details
) AS chart_details24 ON TRUE
@ -196,7 +196,7 @@ def get_details(project_id, error_id, user_id, **data):
AND `$event_name` = 'ERROR'
AND events.created_at >= toDateTime(timestamp / 1000)
AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000)
AND toString(`$properties`.error_id) = %(error_id)s
AND error_id = %(error_id)s
GROUP BY timestamp
ORDER BY timestamp) AS chart_details
) AS chart_details30 ON TRUE;"""

View file

@ -1,3 +1,16 @@
SELECT 1
FROM (SELECT throwIf(platform = 'ios', 'IOS sessions found')
FROM experimental.sessions) AS raw
LIMIT 1;
SELECT 1
FROM (SELECT throwIf(platform = 'android', 'Android sessions found')
FROM experimental.sessions) AS raw
LIMIT 1;
ALTER TABLE experimental.sessions
MODIFY COLUMN platform Enum8('web'=1,'mobile'=2) DEFAULT 'web';
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.22.0-ee';
SET allow_experimental_json_type = 1;

View file

@ -106,7 +106,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
user_city LowCardinality(String),
user_state LowCardinality(String),
platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
platform Enum8('web'=1,'mobile'=2) DEFAULT 'web',
datetime DateTime,
timezone LowCardinality(Nullable(String)),
duration UInt32,

View file

@ -1,5 +1,4 @@
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
import React, { Suspense, lazy } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { observer } from 'mobx-react-lite';
@ -10,7 +9,7 @@ import { Loader } from 'UI';
import APIClient from './api_client';
import * as routes from './routes';
import { debounce } from '@/utils';
import { debounceCall } from '@/utils';
const components: any = {
SessionPure: lazy(() => import('Components/Session/Session')),
@ -88,7 +87,6 @@ const ASSIST_PATH = routes.assist();
const LIVE_SESSION_PATH = routes.liveSession();
const MULTIVIEW_PATH = routes.multiview();
const MULTIVIEW_INDEX_PATH = routes.multiviewIndex();
const ASSIST_STATS_PATH = routes.assistStats();
const USABILITY_TESTING_PATH = routes.usabilityTesting();
const USABILITY_TESTING_EDIT_PATH = routes.usabilityTestingEdit();
@ -99,7 +97,6 @@ const SPOT_PATH = routes.spot();
const SCOPE_SETUP = routes.scopeSetup();
const HIGHLIGHTS_PATH = routes.highlights();
let debounceSearch: any = () => {};
function PrivateRoutes() {
const { projectsStore, userStore, integrationsStore, searchStore } = useStore();
@ -124,13 +121,9 @@ function PrivateRoutes() {
}
}, [siteId]);
React.useEffect(() => {
debounceSearch = debounce(() => searchStore.fetchSessions(), 250);
}, []);
React.useEffect(() => {
if (!searchStore.urlParsed) return;
debounceSearch();
debounceCall(() => searchStore.fetchSessions(true), 250)()
}, [searchStore.urlParsed, searchStore.instance.filters, searchStore.instance.eventsOrder]);
return (

View file

@ -6,6 +6,7 @@ import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying';
import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone';
import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility';
import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings';
import VirtualModeSettings from '../shared/SessionSettings/components/VirtualMode';
import DebugLog from './DebugLog';
import { useTranslation } from 'react-i18next';
@ -35,6 +36,7 @@ function SessionsListingSettings() {
<div className="flex flex-col gap-2">
<MouseTrailSettings />
<DebugLog />
<VirtualModeSettings />
</div>
</div>
</div>

View file

@ -23,6 +23,7 @@ function BottomButtons({
<Button
loading={loading}
type="primary"
htmlType="submit"
disabled={loading || !instance.validate()}
id="submit-button"
>

View file

@ -43,7 +43,7 @@ function ClickMapRagePicker() {
<Checkbox onChange={onToggle} label={t('Include rage clicks')} />
<Button size="small" onClick={refreshHeatmapSession}>
{t('Get new session')}
{t('Get new image')}
</Button>
</div>
);

View file

@ -64,6 +64,7 @@ function DashboardView(props: Props) {
};
useEffect(() => {
dashboardStore.resetPeriod();
if (queryParams.has('modal')) {
onAddWidgets();
trimQuery();

View file

@ -117,8 +117,6 @@ const ListView: React.FC<Props> = ({
if (disableSelection) {
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
history.push(path);
} else {
toggleSelection?.(metric.metricId);
}
};

View file

@ -181,9 +181,10 @@ function WidgetChart(props: Props) {
}
prevMetricRef.current = _metric;
const timestmaps = drillDownPeriod.toTimestamps();
const density = props.isPreview ? metric.density : dashboardStore.selectedDensity
const payload = isSaved
? { ...metricParams }
: { ...params, ...timestmaps, ..._metric.toJson() };
? { ...metricParams, density }
: { ...params, ...timestmaps, ..._metric.toJson(), density };
debounceRequest(
_metric,
payload,

View file

@ -55,7 +55,7 @@ function RangeGranularity({
}
const PAST_24_HR_MS = 24 * 60 * 60 * 1000;
function calculateGranularities(periodDurationMs: number) {
export function calculateGranularities(periodDurationMs: number) {
const granularities = [
{ label: 'Hourly', durationMs: 60 * 60 * 1000 },
{ label: 'Daily', durationMs: 24 * 60 * 60 * 1000 },

View file

@ -8,7 +8,7 @@ import {
LikeFilled,
LikeOutlined,
} from '@ant-design/icons';
import { Tour, TourProps } from './.store/antd-virtual-7db13b4af6/package';
import { Tour, TourProps } from 'antd';
import { useTranslation } from 'react-i18next';
interface Props {

View file

@ -42,7 +42,7 @@ function DropdownAudioPlayer({
return {
url: data.url,
timestamp: data.timestamp,
start: startTs,
start: Math.max(0, startTs),
};
}),
[audioEvents.length, sessionStart],

View file

@ -16,14 +16,12 @@ import { IFRAME } from 'App/constants/storageKeys';
import stl from './playerBlockHeader.module.css';
import UserCard from './EventsBlock/UserCard';
import { useTranslation } from 'react-i18next';
import { Switch } from 'antd';
const SESSIONS_ROUTE = sessionsRoute();
function PlayerBlockHeader(props: any) {
const { t } = useTranslation();
const [hideBack, setHideBack] = React.useState(false);
const { uiPlayerStore } = useStore();
const { player, store } = React.useContext(PlayerContext);
const { uxtestingStore, customFieldStore, projectsStore, sessionStore } =
useStore();
@ -116,34 +114,16 @@ function PlayerBlockHeader(props: any) {
)}
{_metaList.length > 0 && (
<div className="h-full flex items-center px-2 gap-1">
<SessionMetaList
className=""
metaList={_metaList}
maxLength={2}
/>
</div>
<SessionMetaList
horizontal
metaList={_metaList}
maxLength={2}
/>
)}
</div>
{uiPlayerStore.showSearchEventsSwitchButton ? (
<div className="px-2 relative flex items-center border-r border-r-gray-lighter">
<Switch
checked={uiPlayerStore.showOnlySearchEvents}
onChange={uiPlayerStore.setShowOnlySearchEvents}
style={{
background: uiPlayerStore.showOnlySearchEvents
? '#f0a930'
: 'rgba(0, 0, 0, 0.25)',
}}
/>
<span className="ml-2 whitespace-nowrap">
{t('Search Events Only')}
</span>
</div>
) : null}
</div>
<div
className="px-2 relative"
className="px-2 relative border-l border-l-gray-lighter"
style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }}
>
<Tabs

View file

@ -31,10 +31,6 @@ const UXTTABS = {
let playerInst: IPlayerContext['player'] | undefined;
const isDefaultEventsFilterSearch = (filters: FilterItem[]) => {
return filters.length === 1 && filters[0].key === 'location' && filters[0].value[0] === '';
}
function WebPlayer(props: any) {
const {
notesStore,
@ -42,7 +38,6 @@ function WebPlayer(props: any) {
uxtestingStore,
uiPlayerStore,
integrationsStore,
searchStore,
} = useStore();
const devTools = sessionStore.devTools
const session = sessionStore.current;
@ -62,17 +57,6 @@ function WebPlayer(props: any) {
const [fullView, setFullView] = useState(false);
React.useEffect(() => {
if (searchStore.instance.filters?.length && !isDefaultEventsFilterSearch(searchStore.instance.filters)) {
uiPlayerStore.setSearchEventsSwitchButton(true);
uiPlayerStore.setShowOnlySearchEvents(true);
} else {
uiPlayerStore.setSearchEventsSwitchButton(false);
uiPlayerStore.setShowOnlySearchEvents(false);
}
}, [searchStore.instance.filters]);
React.useEffect(() => {
openedAt.current = Date.now();
const handleActivation = () => {
if (!document.hidden) {
setWindowActive(true);

View file

@ -25,7 +25,6 @@ function EventGroupWrapper(props) {
isLastInGroup,
isSelected,
isCurrent,
isSearched,
isEditing,
showSelection,
isFirst,
@ -100,7 +99,7 @@ function EventGroupWrapper(props) {
);
};
const shadowColor = isSearched ? '#F0A930' : props.isPrev
const shadowColor = props.isPrev
? '#A7BFFF'
: props.isCurrent
? '#394EFF'
@ -128,7 +127,7 @@ function EventGroupWrapper(props) {
width: 10,
height: 10,
transform: 'rotate(45deg) translate(0, -50%)',
background: isSearched ? '#F0A930' : '#394EFF',
background: '#394EFF',
zIndex: 99,
borderRadius: '.15rem',
}}

View file

@ -2,7 +2,7 @@ import { mergeEventLists, sortEvents } from 'Types/session';
import { TYPES } from 'Types/session/event';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
import React from 'react';
import { VList, VListHandle } from 'virtua';
import { Button } from 'antd';
import { PlayerContext } from 'App/components/Session/playerContext';
@ -13,8 +13,8 @@ import EventGroupWrapper from './EventGroupWrapper';
import EventSearch from './EventSearch/EventSearch';
import styles from './eventsBlock.module.css';
import { useTranslation } from 'react-i18next';
import { CloseOutlined } from ".store/@ant-design-icons-virtual-42686020c5/package";
import { Tooltip } from ".store/antd-virtual-9dbfadb7f6/package";
import { CloseOutlined } from "@ant-design/icons";
import { Tooltip } from "antd";
import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal";
interface IProps {
@ -47,7 +47,6 @@ function EventsBlock(props: IProps) {
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { store, player } = React.useContext(PlayerContext);
const [currentTimeEventIndex, setCurrentTimeEventIndex] = React.useState(0);
const {
time,
@ -95,8 +94,8 @@ function EventsBlock(props: IProps) {
? 'time' in e
? e.time >= zoomStartTs && e.time <= zoomEndTs
: false
: true
).filter((e: any) => !e.noteId && e.type !== 'TABCHANGE' && uiPlayerStore.showOnlySearchEvents ? e.isHighlighted : true);
: true,
);
}, [
filteredLength,
notesWithEvtsLength,
@ -104,7 +103,6 @@ function EventsBlock(props: IProps) {
zoomEnabled,
zoomStartTs,
zoomEndTs,
uiPlayerStore.showOnlySearchEvents
]);
const findLastFitting = React.useCallback(
(time: number) => {
@ -129,9 +127,7 @@ function EventsBlock(props: IProps) {
[usedEvents, time, endTime],
);
useEffect(() => {
setCurrentTimeEventIndex(findLastFitting(time));
}, [])
const currentTimeEventIndex = findLastFitting(time);
const write = ({
target: { value },
@ -187,7 +183,6 @@ function EventsBlock(props: IProps) {
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
const isCurrent = index === currentTimeEventIndex;
const isPrev = index < currentTimeEventIndex;
const isSearched = event.isHighlighted
return (
<EventGroupWrapper
@ -199,7 +194,6 @@ function EventsBlock(props: IProps) {
isLastEvent={isLastEvent}
isLastInGroup={isLastInGroup}
isCurrent={isCurrent}
isSearched={isSearched}
showSelection={!playing}
isNote={isNote}
isTabChange={isTabChange}

View file

@ -4,6 +4,7 @@ import {
MobilePlayerContext,
} from 'Components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import stl from './timeline.module.css';
import { getTimelinePosition } from './getTimelinePosition';
import { useStore } from '@/mstore';
@ -15,14 +16,8 @@ function EventsList() {
const { tabStates } = store.get();
const scale = 100 / endTime;
const events = React.useMemo(
() => Object.values(tabStates)[0]?.eventList.filter((e) => {
if (uiPlayerStore.showOnlySearchEvents) {
return e.time && (e as any).isHighlighted
} else {
return e.time
}
}) || [],
[eventCount, uiPlayerStore.showOnlySearchEvents],
() => Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [],
[eventCount],
);
React.useEffect(() => {
const hasDuplicates = events.some(
@ -39,7 +34,7 @@ function EventsList() {
<div
/* @ts-ignore TODO */
key={`${e.key}_${e.time}`}
className={`absolute w-[2px] h-[10px] z-[3] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
className={stl.event}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/>
))}
@ -59,7 +54,7 @@ function MobileEventsList() {
<div
/* @ts-ignore TODO */
key={`${e.key}_${e.time}`}
className={`absolute w-[2px] h-[10px] z-[3] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
className={stl.event}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/>
))}

View file

@ -1,51 +0,0 @@
import FilterItem from '@/mstore/types/filterItem';
export const checkEventWithFilters = (event: Event, filters: FilterItem[]) => {
let result = false;
filters.forEach((filter) => {
if (filter.key.toUpperCase() === event.type.toUpperCase()) {
if (filter.operator) {
const operator = operators[filter.operator];
if (operator) {
result = !!operator(event.label, filter.value);
}
}
}
});
return result
};
const operators = {
is: (val: string, target: string[]) => target.some((t) => val.includes(t)),
isAny: () => true,
isNot: (val: string, target: string[]) =>
!target.some((t) => val.includes(t)),
contains: (val: string, target: string[]) =>
target.some((t) => val.includes(t)),
notContains: (val: string, target: string[]) =>
!target.some((t) => val.includes(t)),
startsWith: (val: string, target: string[]) =>
target.some((t) => val.startsWith(t)),
endsWith: (val: string, target: string[]) =>
target.some((t) => val.endsWith(t)),
greaterThan: (val: number, target: number) => val > target,
greaterOrEqual: (val: number, target: number) => val >= target,
lessOrEqual: (val: number, target: number) => val <= target,
lessThan: (val: number, target: number) => val < target,
on: (val: string, target: string[]) => target.some((t) => val.includes(t)),
notOn: (val: string, target: string[]) =>
!target.some((t) => val.includes(t)),
onAny: () => true,
selectorIs: (val: string, target: string[]) => target.some((t) => val.includes(t)),
selectorIsAny: () => true,
selectorIsNot: (val: string, target: string[]) =>
!target.some((t) => val.includes(t)),
selectorContains: (val: string, target: string[]) =>
target.some((t) => val.includes(t)),
selectorNotContains: (val: string, target: string[]) =>
!target.some((t) => val.includes(t)),
selectorStartsWith: (val: string, target: string[]) =>
target.some((t) => val.startsWith(t)),
selectorEndsWith: (val: string, target: string[]) =>
target.some((t) => val.endsWith(t)),
};

View file

@ -49,6 +49,23 @@
z-index: 2;
}
.event {
position: absolute;
width: 2px;
height: 10px;
background: $main;
z-index: 3;
pointer-events: none;
/* top: 0; */
/* bottom: 0; */
/* &:hover {
width: 10px;
height: 10px;
margin-left: -6px;
z-index: 1;
};*/
}
/* .event.click, .event.input {
background: $green;
}

View file

@ -38,6 +38,7 @@ function SubHeader(props) {
projectsStore,
userStore,
issueReportingStore,
settingsStore
} = useStore();
const { t } = useTranslation();
const { favorite } = sessionStore.current;
@ -45,7 +46,7 @@ function SubHeader(props) {
const currentSession = sessionStore.current;
const projectId = projectsStore.siteId;
const integrations = integrationsStore.issues.list;
const { store } = React.useContext(PlayerContext);
const { player, store } = React.useContext(PlayerContext);
const { location: currentLocation = 'loading...' } = store.get();
const hasIframe = localStorage.getItem(IFRAME) === 'true';
const [hideTools, setHideTools] = React.useState(false);
@ -127,6 +128,13 @@ function SubHeader(props) {
});
};
const showVModeBadge = store.get().vModeBadge;
const onVMode = () => {
settingsStore.sessionSettings.updateKey('virtualMode', true);
player.enableVMode?.();
location.reload();
}
return (
<>
<div
@ -143,6 +151,8 @@ function SubHeader(props) {
siteId={projectId!}
currentLocation={currentLocation}
version={currentSession?.trackerVersion ?? ''}
virtualElsFailed={showVModeBadge}
onVMode={onVMode}
/>
<SessionTabs />

View file

@ -34,38 +34,46 @@ const WarnBadge = React.memo(
currentLocation,
version,
siteId,
virtualElsFailed,
onVMode,
}: {
currentLocation: string;
version: string;
siteId: string;
virtualElsFailed: boolean;
onVMode: () => void;
}) => {
const { t } = useTranslation();
const localhostWarnSiteKey = localhostWarn(siteId);
const defaultLocalhostWarn =
localStorage.getItem(localhostWarnSiteKey) !== '1';
const localhostWarnActive =
const localhostWarnActive = Boolean(
currentLocation &&
defaultLocalhostWarn &&
/(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation);
/(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation)
)
const trackerVersion = window.env.TRACKER_VERSION ?? undefined;
const trackerVerDiff = compareVersions(version, trackerVersion);
const trackerWarnActive = trackerVerDiff !== VersionComparison.Same;
const [showLocalhostWarn, setLocalhostWarn] =
React.useState(localhostWarnActive);
const [showTrackerWarn, setTrackerWarn] = React.useState(trackerWarnActive);
const [warnings, setWarnings] = React.useState<[localhostWarn: boolean, trackerWarn: boolean, virtualElsFailWarn: boolean]>([localhostWarnActive, trackerWarnActive, virtualElsFailed])
const closeWarning = (type: 1 | 2) => {
React.useEffect(() => {
setWarnings([localhostWarnActive, trackerWarnActive, virtualElsFailed])
}, [localhostWarnActive, trackerWarnActive, virtualElsFailed])
const closeWarning = (type: 0 | 1 | 2) => {
if (type === 1) {
localStorage.setItem(localhostWarnSiteKey, '1');
setLocalhostWarn(false);
}
if (type === 2) {
setTrackerWarn(false);
}
setWarnings((prev) => {
const newWarnings = [...prev];
newWarnings[type] = false;
return newWarnings;
});
};
if (!showLocalhostWarn && !showTrackerWarn) return null;
if (!warnings.some(el => el === true)) return null;
return (
<div
@ -79,7 +87,7 @@ const WarnBadge = React.memo(
fontWeight: 500,
}}
>
{showLocalhostWarn ? (
{warnings[0] ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
<div>
<span>{t('Some assets may load incorrectly on localhost.')}</span>
@ -101,7 +109,7 @@ const WarnBadge = React.memo(
</div>
</div>
) : null}
{showTrackerWarn ? (
{warnings[1] ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
<div>
<div>
@ -125,6 +133,21 @@ const WarnBadge = React.memo(
</div>
</div>
<div
className="py-1 ml-3 cursor-pointer"
onClick={() => closeWarning(1)}
>
<Icon name="close" size={16} color="black" />
</div>
</div>
) : null}
{warnings[2] ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
<div className="flex flex-col">
<div>{t('If you have issues displaying custom HTML elements (i.e when using LWC), consider turning on Virtual Mode.')}</div>
<div className='link' onClick={onVMode}>{t('Enable')}</div>
</div>
<div
className="py-1 ml-3 cursor-pointer"
onClick={() => closeWarning(2)}

View file

@ -1,9 +1,17 @@
/* eslint-disable i18next/no-literal-string */
import { ResourceType, Timed } from 'Player';
import { WsChannel } from 'Player/web/messages';
import MobilePlayer from 'Player/mobile/IOSPlayer';
import WebPlayer from 'Player/web/WebPlayer';
import { observer } from 'mobx-react-lite';
import React, { useMemo, useState } from 'react';
import React, {
useMemo,
useState,
useEffect,
useCallback,
useRef,
} from 'react';
import i18n from 'App/i18n'
import { useModal } from 'App/components/Modal';
import {
@ -12,25 +20,27 @@ import {
} from 'App/components/Session/playerContext';
import { formatMs } from 'App/date';
import { useStore } from 'App/mstore';
import { formatBytes } from 'App/utils';
import { formatBytes, debounceCall } from 'App/utils';
import { Icon, NoContent, Tabs } from 'UI';
import { Tooltip, Input, Switch, Form } from 'antd';
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
import {
SearchOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { WsChannel } from 'App/player/web/messages';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import TabSelector from '../TabSelector';
import TimeTable from '../TimeTable';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import WSPanel from './WSPanel';
import { useTranslation } from 'react-i18next';
import { mergeListsWithZoom, processInChunks } from './utils'
// Constants remain the same
const INDEX_KEY = 'network';
const ALL = 'ALL';
const XHR = 'xhr';
const JS = 'js';
@ -62,6 +72,9 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
const BATCH_SIZE = 2500;
const INITIAL_LOAD_SIZE = 5000;
export function renderType(r: any) {
return (
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
@ -79,13 +92,17 @@ export function renderName(r: any) {
}
function renderSize(r: any) {
const { t } = useTranslation();
if (r.responseBodySize) return formatBytes(r.responseBodySize);
const t = i18n.t;
const notCaptured = t('Not captured');
const resSizeStr = t('Resource size')
let triggerText;
let content;
if (r.decodedBodySize == null || r.decodedBodySize === 0) {
if (r.responseBodySize) {
triggerText = formatBytes(r.responseBodySize);
content = undefined;
} else if (r.decodedBodySize == null || r.decodedBodySize === 0) {
triggerText = 'x';
content = t('Not captured');
content = notCaptured;
} else {
const headerSize = r.headerSize || 0;
const showTransferred = r.headerSize != null;
@ -100,7 +117,7 @@ function renderSize(r: any) {
)} transferred over network`}
</li>
)}
<li>{`${t('Resource size')}: ${formatBytes(r.decodedBodySize)} `}</li>
<li>{`${resSizeStr}: ${formatBytes(r.decodedBodySize)} `}</li>
</ul>
);
}
@ -168,6 +185,8 @@ function renderStatus({
);
}
// Main component for Network Panel
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
const { player, store } = React.useContext(PlayerContext);
const { sessionStore, uiPlayerStore } = useStore();
@ -216,6 +235,7 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
const getTabName = (tabId: string) => tabNames[tabId];
return (
<NetworkPanelComp
loadTime={loadTime}
@ -228,8 +248,8 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
resourceListNow={resourceListNow}
player={player}
startedAt={startedAt}
websocketList={websocketList as WSMessage[]}
websocketListNow={websocketListNow as WSMessage[]}
websocketList={websocketList}
websocketListNow={websocketListNow}
getTabNum={getTabNum}
getTabName={getTabName}
showSingleTab={showSingleTab}
@ -269,9 +289,7 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
resourceListNow={resourceListNow}
player={player}
startedAt={startedAt}
// @ts-ignore
websocketList={websocketList}
// @ts-ignore
websocketListNow={websocketListNow}
zoomEnabled={zoomEnabled}
zoomStartTs={zoomStartTs}
@ -280,12 +298,35 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
);
}
type WSMessage = Timed & {
channelName: string;
data: string;
timestamp: number;
dir: 'up' | 'down';
messageType: string;
const useInfiniteScroll = (loadMoreCallback: () => void, hasMore: boolean) => {
const observerRef = useRef<IntersectionObserver>(null);
const loadingRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting && hasMore) {
loadMoreCallback();
}
},
{ threshold: 0.1 },
);
if (loadingRef.current) {
observer.observe(loadingRef.current);
}
// @ts-ignore
observerRef.current = observer;
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [loadMoreCallback, hasMore, loadingRef]);
return loadingRef;
};
interface Props {
@ -302,8 +343,8 @@ interface Props {
resourceList: Timed[];
fetchListNow: Timed[];
resourceListNow: Timed[];
websocketList: Array<WSMessage>;
websocketListNow: Array<WSMessage>;
websocketList: Array<WsChannel>;
websocketListNow: Array<WsChannel>;
player: WebPlayer | MobilePlayer;
startedAt: number;
isMobile?: boolean;
@ -349,107 +390,189 @@ export const NetworkPanelComp = observer(
>(null);
const { showModal } = useModal();
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isProcessing, setIsProcessing] = useState(false);
const [displayedItems, setDisplayedItems] = useState([]);
const [totalItems, setTotalItems] = useState(0);
const [summaryStats, setSummaryStats] = useState({
resourcesSize: 0,
transferredSize: 0,
});
const originalListRef = useRef([]);
const socketListRef = useRef([]);
const {
sessionStore: { devTools },
} = useStore();
const { filter } = devTools[INDEX_KEY];
const { activeTab } = devTools[INDEX_KEY];
const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index;
const [inputFilterValue, setInputFilterValue] = useState(filter);
const socketList = useMemo(
() =>
websocketList.filter(
(ws, i, arr) =>
arr.findIndex((it) => it.channelName === ws.channelName) === i,
),
[websocketList],
const debouncedFilter = useCallback(
debounceCall((filterValue) => {
devTools.update(INDEX_KEY, { filter: filterValue });
}, 300),
[],
);
const list = useMemo(
() =>
// TODO: better merge (with body size info) - do it in player
resourceList
.filter(
(res) =>
!fetchList.some((ft) => {
// res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player)
if (res.name === ft.name) {
if (res.time === ft.time) return true;
if (res.url.includes(ft.url)) {
return (
Math.abs(res.time - ft.time) < 350 ||
Math.abs(res.timestamp - ft.timestamp) < 350
);
}
}
if (res.name !== ft.name) {
return false;
}
if (Math.abs(res.time - ft.time) > 250) {
return false;
} // TODO: find good epsilons
if (Math.abs(res.duration - ft.duration) > 200) {
return false;
}
return true;
}),
)
.concat(fetchList)
.concat(
socketList.map((ws) => ({
...ws,
type: 'websocket',
method: 'ws',
url: ws.channelName,
name: ws.channelName,
status: '101',
duration: 0,
transferredBodySize: 0,
})),
)
.filter((req) =>
zoomEnabled
? req.time >= zoomStartTs! && req.time <= zoomEndTs!
: true,
)
.sort((a, b) => a.time - b.time),
[resourceList.length, fetchList.length, socketList.length],
);
let filteredList = useMemo(() => {
if (!showOnlyErrors) {
return list;
}
return list.filter(
(it) => parseInt(it.status) >= 400 || !it.success || it.error,
// Process socket lists once
useEffect(() => {
const uniqueSocketList = websocketList.filter(
(ws, i, arr) =>
arr.findIndex((it) => it.channelName === ws.channelName) === i,
);
}, [showOnlyErrors, list]);
filteredList = useRegExListFilterMemo(
filteredList,
(it) => [it.status, it.name, it.type, it.method],
filter,
);
filteredList = useTabListFilterMemo(
filteredList,
(it) => TYPE_TO_TAB[it.type],
ALL,
activeTab,
);
socketListRef.current = uniqueSocketList;
}, [websocketList.length]);
const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) =>
// Initial data processing - do this only once when data changes
useEffect(() => {
setIsLoading(true);
// Heaviest operation here, will create a final merged network list
const processData = async () => {
const fetchUrls = new Set(
fetchList.map((ft) => {
return `${ft.name}-${Math.floor(ft.time / 100)}-${Math.floor(ft.duration / 100)}`;
}),
);
// We want to get resources that aren't in fetch list
const filteredResources = await processInChunks(resourceList, (chunk) =>
chunk.filter((res: any) => {
const key = `${res.name}-${Math.floor(res.time / 100)}-${Math.floor(res.duration / 100)}`;
return !fetchUrls.has(key);
}),
BATCH_SIZE,
25,
);
const processedSockets = socketListRef.current.map((ws: any) => ({
...ws,
type: 'websocket',
method: 'ws',
url: ws.channelName,
name: ws.channelName,
status: '101',
duration: 0,
transferredBodySize: 0,
}));
const mergedList: Timed[] = mergeListsWithZoom(
filteredResources as Timed[],
fetchList,
processedSockets as Timed[],
{ enabled: Boolean(zoomEnabled), start: zoomStartTs ?? 0, end: zoomEndTs ?? 0 }
)
originalListRef.current = mergedList;
setTotalItems(mergedList.length);
calculateResourceStats(resourceList);
// Only display initial chunk
setDisplayedItems(mergedList.slice(0, INITIAL_LOAD_SIZE));
setIsLoading(false);
};
void processData();
}, [
resourceList.length,
fetchList.length,
socketListRef.current.length,
zoomEnabled,
zoomStartTs,
zoomEndTs,
]);
const calculateResourceStats = (resourceList: Record<string, any>) => {
setTimeout(() => {
let resourcesSize = 0
let transferredSize = 0
resourceList.forEach(({ decodedBodySize, headerSize, encodedBodySize }: any) => {
resourcesSize += decodedBodySize || 0
transferredSize += (headerSize || 0) + (encodedBodySize || 0)
})
setSummaryStats({
resourcesSize,
transferredSize,
});
}, 0);
}
useEffect(() => {
if (originalListRef.current.length === 0) return;
setIsProcessing(true);
const applyFilters = async () => {
let filteredItems: any[] = originalListRef.current;
filteredItems = await processInChunks(filteredItems, (chunk) =>
chunk.filter(
(it) => {
let valid = true;
if (showOnlyErrors) {
valid = parseInt(it.status) >= 400 || !it.success || it.error
}
if (filter) {
try {
const regex = new RegExp(filter, 'i');
valid = valid && regex.test(it.status) || regex.test(it.name) || regex.test(it.type) || regex.test(it.method);
} catch (e) {
valid = valid && String(it.status).includes(filter) || it.name.includes(filter) || it.type.includes(filter) || (it.method && it.method.includes(filter));
}
}
if (activeTab !== ALL) {
valid = valid && TYPE_TO_TAB[it.type] === activeTab;
}
return valid;
},
),
);
// Update displayed items
setDisplayedItems(filteredItems.slice(0, INITIAL_LOAD_SIZE));
setTotalItems(filteredItems.length);
setIsProcessing(false);
};
void applyFilters();
}, [filter, activeTab, showOnlyErrors]);
const loadMoreItems = useCallback(() => {
if (isProcessing) return;
setIsProcessing(true);
setTimeout(() => {
setDisplayedItems((prevItems) => {
const currentLength = prevItems.length;
const newItems = originalListRef.current.slice(
currentLength,
currentLength + BATCH_SIZE,
);
return [...prevItems, ...newItems];
});
setIsProcessing(false);
}, 10);
}, [isProcessing]);
const hasMoreItems = displayedItems.length < totalItems;
const loadingRef = useInfiniteScroll(loadMoreItems, hasMoreItems);
const onTabClick = (activeTab) => {
devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) =>
devTools.update(INDEX_KEY, { filter: value });
};
const onFilterChange = ({ target: { value } }) => {
setInputFilterValue(value)
debouncedFilter(value);
};
// AutoScroll
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
filteredList,
displayedItems,
getLastItemTime(fetchListNow, resourceListNow),
activeIndex,
(index) => devTools.update(INDEX_KEY, { index }),
@ -462,24 +585,6 @@ export const NetworkPanelComp = observer(
timeoutStartAutoscroll();
};
const resourcesSize = useMemo(
() =>
resourceList.reduce(
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
0,
),
[resourceList.length],
);
const transferredSize = useMemo(
() =>
resourceList.reduce(
(sum, { headerSize, encodedBodySize }) =>
sum + (headerSize || 0) + (encodedBodySize || 0),
0,
),
[resourceList.length],
);
const referenceLines = useMemo(() => {
const arr = [];
@ -513,7 +618,7 @@ export const NetworkPanelComp = observer(
isSpot={isSpot}
time={item.time + startedAt}
resource={item}
rows={filteredList}
rows={displayedItems}
fetchPresented={fetchList.length > 0}
/>,
{
@ -525,12 +630,10 @@ export const NetworkPanelComp = observer(
},
},
);
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
stopAutoscroll();
};
const tableCols = React.useMemo(() => {
const cols: any[] = [
const tableCols = useMemo(() => {
const cols = [
{
label: t('Status'),
dataKey: 'status',
@ -585,7 +688,7 @@ export const NetworkPanelComp = observer(
});
}
return cols;
}, [showSingleTab]);
}, [showSingleTab, activeTab, t, getTabName, getTabNum, isSpot]);
return (
<BottomBlock
@ -617,7 +720,7 @@ export const NetworkPanelComp = observer(
name="filter"
onChange={onFilterChange}
width={280}
value={filter}
value={inputFilterValue}
size="small"
prefix={<SearchOutlined className="text-neutral-400" />}
/>
@ -625,7 +728,7 @@ export const NetworkPanelComp = observer(
</BottomBlock.Header>
<BottomBlock.Content>
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
<div>
<div className="flex items-center">
<Form.Item name="show-errors-only" className="mb-0">
<label
style={{
@ -642,21 +745,29 @@ export const NetworkPanelComp = observer(
<span className="text-sm ms-2">4xx-5xx Only</span>
</label>
</Form.Item>
{isProcessing && (
<span className="text-xs text-gray-500 ml-4">
Processing data...
</span>
)}
</div>
<InfoLine>
<InfoLine.Point label={`${totalItems}`} value="requests" />
<InfoLine.Point
label={`${filteredList.length}`}
value=" requests"
label={`${displayedItems.length}/${totalItems}`}
value="displayed"
display={displayedItems.length < totalItems}
/>
<InfoLine.Point
label={formatBytes(transferredSize)}
label={formatBytes(summaryStats.transferredSize)}
value="transferred"
display={transferredSize > 0}
display={summaryStats.transferredSize > 0}
/>
<InfoLine.Point
label={formatBytes(resourcesSize)}
label={formatBytes(summaryStats.resourcesSize)}
value="resources"
display={resourcesSize > 0}
display={summaryStats.resourcesSize > 0}
/>
<InfoLine.Point
label={formatMs(domBuildingTime)}
@ -679,42 +790,67 @@ export const NetworkPanelComp = observer(
/>
</InfoLine>
</div>
<NoContent
title={
<div className="capitalize flex items-center gap-2">
<InfoCircleOutlined size={18} />
{t('No Data')}
{isLoading ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
<p>Processing initial network data...</p>
</div>
}
size="small"
show={filteredList.length === 0}
>
{/* @ts-ignore */}
<TimeTable
rows={filteredList}
tableHeight={panelHeight - 102}
referenceLines={referenceLines}
renderPopup
onRowClick={showDetailsModal}
sortBy="time"
sortAscending
onJump={(row: any) => {
devTools.update(INDEX_KEY, {
index: filteredList.indexOf(row),
});
player.jump(row.time);
}}
activeIndex={activeIndex}
</div>
) : (
<NoContent
title={
<div className="capitalize flex items-center gap-2">
<InfoCircleOutlined size={18} />
{t('No Data')}
</div>
}
size="small"
show={displayedItems.length === 0}
>
{tableCols}
</TimeTable>
{selectedWsChannel ? (
<WSPanel
socketMsgList={selectedWsChannel}
onClose={() => setSelectedWsChannel(null)}
/>
) : null}
</NoContent>
<div>
<TimeTable
rows={displayedItems}
tableHeight={panelHeight - 102 - (hasMoreItems ? 30 : 0)}
referenceLines={referenceLines}
renderPopup
onRowClick={showDetailsModal}
sortBy="time"
sortAscending
onJump={(row) => {
devTools.update(INDEX_KEY, {
index: displayedItems.indexOf(row),
});
player.jump(row.time);
}}
activeIndex={activeIndex}
>
{tableCols}
</TimeTable>
{hasMoreItems && (
<div
ref={loadingRef}
className="flex justify-center items-center text-xs text-gray-500"
>
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
Loading more data ({totalItems - displayedItems.length}{' '}
remaining)
</div>
</div>
)}
</div>
{selectedWsChannel ? (
<WSPanel
socketMsgList={selectedWsChannel}
onClose={() => setSelectedWsChannel(null)}
/>
) : null}
</NoContent>
)}
</BottomBlock.Content>
</BottomBlock>
);
@ -722,7 +858,6 @@ export const NetworkPanelComp = observer(
);
const WebNetworkPanel = observer(NetworkPanelCont);
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
export { WebNetworkPanel, MobileNetworkPanel };

View file

@ -0,0 +1,178 @@
export function mergeListsWithZoom<
T extends Record<string, any>,
Y extends Record<string, any>,
Z extends Record<string, any>,
>(
arr1: T[],
arr2: Y[],
arr3: Z[],
zoom?: { enabled: boolean; start: number; end: number },
): Array<T | Y | Z> {
// Early return for empty arrays
if (arr1.length === 0 && arr2.length === 0 && arr3.length === 0) {
return [];
}
// Optimized for common case - no zoom
if (!zoom?.enabled) {
return mergeThreeSortedArrays(arr1, arr2, arr3);
}
// Binary search for start indexes (faster than linear search for large arrays)
const index1 = binarySearchStartIndex(arr1, zoom.start);
const index2 = binarySearchStartIndex(arr2, zoom.start);
const index3 = binarySearchStartIndex(arr3, zoom.start);
// Merge arrays within zoom range
return mergeThreeSortedArraysWithinRange(
arr1,
arr2,
arr3,
index1,
index2,
index3,
zoom.start,
zoom.end,
);
}
function binarySearchStartIndex<T extends Record<string, any>>(
arr: T[],
threshold: number,
): number {
if (arr.length === 0) return 0;
let low = 0;
let high = arr.length - 1;
// Handle edge cases first for better performance
if (arr[high].time < threshold) return arr.length;
if (arr[low].time >= threshold) return 0;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
if (arr[mid].time < threshold) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return low;
}
function mergeThreeSortedArrays<
T extends Record<string, any>,
Y extends Record<string, any>,
Z extends Record<string, any>,
>(arr1: T[], arr2: Y[], arr3: Z[]): Array<T | Y | Z> {
const totalLength = arr1.length + arr2.length + arr3.length;
// prealloc array size
const result = new Array(totalLength);
let i = 0,
j = 0,
k = 0,
index = 0;
while (i < arr1.length || j < arr2.length || k < arr3.length) {
const val1 = i < arr1.length ? arr1[i].time : Infinity;
const val2 = j < arr2.length ? arr2[j].time : Infinity;
const val3 = k < arr3.length ? arr3[k].time : Infinity;
if (val1 <= val2 && val1 <= val3) {
result[index++] = arr1[i++];
} else if (val2 <= val1 && val2 <= val3) {
result[index++] = arr2[j++];
} else {
result[index++] = arr3[k++];
}
}
return result;
}
// same as above, just with zoom stuff
function mergeThreeSortedArraysWithinRange<
T extends Record<string, any>,
Y extends Record<string, any>,
Z extends Record<string, any>,
>(
arr1: T[],
arr2: Y[],
arr3: Z[],
startIdx1: number,
startIdx2: number,
startIdx3: number,
start: number,
end: number,
): Array<T | Y | Z> {
// we don't know beforehand how many items will be there
const result = [];
let i = startIdx1;
let j = startIdx2;
let k = startIdx3;
while (i < arr1.length || j < arr2.length || k < arr3.length) {
const val1 = i < arr1.length ? arr1[i].time : Infinity;
const val2 = j < arr2.length ? arr2[j].time : Infinity;
const val3 = k < arr3.length ? arr3[k].time : Infinity;
// Early termination: if all remaining values exceed end time
if (Math.min(val1, val2, val3) > end) {
break;
}
if (val1 <= val2 && val1 <= val3) {
if (val1 <= end) {
result.push(arr1[i]);
}
i++;
} else if (val2 <= val1 && val2 <= val3) {
if (val2 <= end) {
result.push(arr2[j]);
}
j++;
} else {
if (val3 <= end) {
result.push(arr3[k]);
}
k++;
}
}
return result;
}
export function processInChunks(
items: any[],
processFn: (item: any) => any,
chunkSize = 1000,
overscan = 0,
) {
return new Promise((resolve) => {
if (items.length === 0) {
resolve([]);
return;
}
let result: any[] = [];
let index = 0;
const processNextChunk = () => {
const chunk = items.slice(index, index + chunkSize + overscan);
result = result.concat(processFn(chunk));
index += chunkSize;
if (index < items.length) {
setTimeout(processNextChunk, 0);
} else {
resolve(result);
}
};
processNextChunk();
});
}

View file

@ -125,7 +125,7 @@ export function AutocompleteModal({
if (index === blocksAmount - 1 && blocksAmount > 1) {
str += ' and ';
}
str += `"${block.trim()}"`;
str += block.trim();
if (index < blocksAmount - 2) {
str += ', ';
}
@ -188,10 +188,10 @@ export function AutocompleteModal({
{query.length ? (
<div className="border-y border-y-gray-light py-2">
<div
className="whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
className="whitespace-nowrap truncate w-full rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
onClick={applyQuery}
>
{t('Apply')}&nbsp;{queryStr}
{t('Apply')}&nbsp;<span className='font-semibold'>{queryStr}</span>
</div>
</div>
) : null}

View file

@ -128,8 +128,10 @@ const FilterAutoComplete = observer(
};
const handleFocus = () => {
if (!initialFocus) {
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
}
setInitialFocus(true);
setOptions(topValues.map((i) => ({ value: i.value, label: i.value })));
};
return (

View file

@ -19,11 +19,13 @@ export default function MetaItem(props: Props) {
<TextEllipsis
text={label}
className="p-0"
maxWidth={'300px'}
popupProps={{ size: 'small', disabled: true }}
/>
<span className="bg-neutral-200 inline-block w-[1px] min-h-[17px]"></span>
<TextEllipsis
text={value}
maxWidth={'350px'}
className="p-0 text-neutral-500"
popupProps={{ size: 'small', disabled: true }}
/>

View file

@ -7,13 +7,15 @@ interface Props {
className?: string;
metaList: any[];
maxLength?: number;
onMetaClick?: (meta: { name: string, value: string }) => void;
horizontal?: boolean;
}
export default function SessionMetaList(props: Props) {
const { className = '', metaList, maxLength = 14 } = props;
const { className = '', metaList, maxLength = 14, horizontal = false } = props;
return (
<div className={cn('flex items-center flex-wrap gap-1', className)}>
<div className={cn('flex items-center gap-1', horizontal ? '' : 'flex-wrap', className)}>
{metaList.slice(0, maxLength).map(({ label, value }, index) => (
<React.Fragment key={index}>
<MetaItem label={label} value={`${value}`} />

View file

@ -5,6 +5,7 @@ import ListingVisibility from './components/ListingVisibility';
import DefaultPlaying from './components/DefaultPlaying';
import DefaultTimezone from './components/DefaultTimezone';
import CaptureRate from './components/CaptureRate';
import { useTranslation } from 'react-i18next';
function SessionSettings() {

View file

@ -0,0 +1,30 @@
import React from 'react';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { Switch } from 'UI';
import { useTranslation } from 'react-i18next';
function VirtualModeSettings() {
const { settingsStore } = useStore();
const { sessionSettings } = settingsStore;
const { virtualMode } = sessionSettings;
const { t } = useTranslation();
const updateSettings = (checked: boolean) => {
settingsStore.sessionSettings.updateKey('virtualMode', !virtualMode);
};
return (
<div>
<h3 className="text-lg">{t('Virtual Mode')}</h3>
<div className="my-1">
{t('Change this setting if you have issues with recordings containing Lightning Web Components (or similar custom HTML Element libraries).')}
</div>
<div className="mt-2">
<Switch onChange={updateSettings} checked={virtualMode} />
</div>
</div>
);
}
export default observer(VirtualModeSettings);

View file

@ -9,6 +9,7 @@ export const GLOBAL_HAS_NO_RECORDINGS = '__$global-hasNoRecordings$__';
export const SITE_ID_STORAGE_KEY = '__$user-siteId$__';
export const GETTING_STARTED = '__$user-gettingStarted$__';
export const MOUSE_TRAIL = '__$session-mouseTrail$__';
export const VIRTUAL_MODE_KEY = '__$session-virtualMode$__'
export const IFRAME = '__$session-iframe$__';
export const JWT_PARAM = '__$session-jwt-param$__';
export const MENU_COLLAPSED = '__$global-menuCollapsed$__';

View file

@ -503,7 +503,7 @@
"Returning users between": "Returning users between",
"Sessions": "Sessions",
"No recordings found.": "No recordings found.",
"Get new session": "Get new session",
"Get new image": "Get new image",
"The number of cards in one dashboard is limited to 30.": "The number of cards in one dashboard is limited to 30.",
"Add Card": "Add Card",
"Create Dashboard": "Create Dashboard",

View file

@ -503,7 +503,7 @@
"Returning users between": "Usuarios recurrentes entre",
"Sessions": "Sesiones",
"No recordings found.": "No se encontraron grabaciones.",
"Get new session": "Obtener nueva sesión",
"Get new image": "Obtener nueva sesión",
"The number of cards in one dashboard is limited to 30.": "El número de tarjetas en un panel está limitado a 30.",
"Add Card": "Agregar tarjeta",
"Create Dashboard": "Crear panel",

View file

@ -503,7 +503,7 @@
"Returning users between": "Utilisateurs récurrents entre",
"Sessions": "Sessions",
"No recordings found.": "Aucun enregistrement trouvé.",
"Get new session": "Obtenir une nouvelle session",
"Get new image": "Obtenir une nouvelle session",
"The number of cards in one dashboard is limited to 30.": "Le nombre de cartes dans un tableau de bord est limité à 30.",
"Add Card": "Ajouter une carte",
"Create Dashboard": "Créer un tableau de bord",

View file

@ -504,7 +504,7 @@
"Returning users between": "Возвращающиеся пользователи за период",
"Sessions": "Сессии",
"No recordings found.": "Записей не найдено.",
"Get new session": "Получить новую сессию",
"Get new image": "Получить новую сессию",
"The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.",
"Add Card": "Добавить карточку",
"Create Dashboard": "Создать дашборд",
@ -1498,5 +1498,8 @@
"More attribute": "Еще атрибут",
"More attributes": "Еще атрибуты",
"Account settings updated successfully": "Настройки аккаунта успешно обновлены",
"Include rage clicks": "Включить невыносимые клики"
}
"Include rage clicks": "Включить невыносимые клики",
"Interface Language": "Язык интерфейса",
"Select the language in which OpenReplay will appear.": "Выберите язык, на котором будет отображаться OpenReplay.",
"Language": "Язык"
}

View file

@ -503,7 +503,7 @@
"Returning users between": "回访用户区间",
"Sessions": "会话",
"No recordings found.": "未找到录制。",
"Get new session": "获取新会话",
"Get new image": "获取新会话",
"The number of cards in one dashboard is limited to 30.": "一个仪表板最多可包含30个卡片。",
"Add Card": "添加卡片",
"Create Dashboard": "创建仪表板",

View file

@ -1,11 +1,13 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { makeAutoObservable, runInAction, reaction } from 'mobx';
import { dashboardService, metricService } from 'App/services';
import { toast } from 'react-toastify';
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
import Period, { LAST_24_HOURS } from 'Types/app/period';
import { getRE } from 'App/utils';
import Filter from './types/filter';
import Widget from './types/widget';
import Dashboard from './types/dashboard';
import { calculateGranularities } from '@/components/Dashboard/components/WidgetDateRange/RangeGranularity';
import { CUSTOM_RANGE } from '@/dateRange';
interface DashboardFilter {
query?: string;
@ -36,7 +38,7 @@ export default class DashboardStore {
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
selectedDensity: number = 7; // depends on default drilldown, 7 points here!!!;
selectedDensity: number = 7;
comparisonPeriods: Record<string, any> = {};
@ -83,10 +85,29 @@ export default class DashboardStore {
makeAutoObservable(this);
this.resetDrillDownFilter();
this.createDensity(this.period.getDuration());
reaction(
() => this.period,
(period) => {
this.createDensity(period.getDuration());
},
);
}
setDensity = (density: any) => {
this.selectedDensity = parseInt(density, 10);
resetDensity = () => {
this.createDensity(this.period.getDuration());
};
createDensity = (duration: number) => {
const densityOpts = calculateGranularities(duration);
const defaultOption = densityOpts[densityOpts.length - 2];
this.setDensity(defaultOption.key);
};
setDensity = (density: number) => {
this.selectedDensity = density;
};
get sortedDashboards() {
@ -446,7 +467,7 @@ export default class DashboardStore {
this.isSaving = true;
try {
try {
const response = await dashboardService.addWidget(dashboard, metricIds);
await dashboardService.addWidget(dashboard, metricIds);
toast.success('Card added to dashboard.');
} catch {
toast.error('Card could not be added.');
@ -456,6 +477,17 @@ export default class DashboardStore {
}
}
resetPeriod = () => {
if (this.period) {
const range = this.period.rangeName;
if (range !== CUSTOM_RANGE) {
this.period = Period({ rangeName: this.period.rangeName });
} else {
this.period = Period({ rangeName: LAST_24_HOURS });
}
}
};
setPeriod(period: any) {
this.period = Period({
start: period.start,

View file

@ -1,6 +1,5 @@
import { makeAutoObservable } from 'mobx';
import { issueReportsService } from 'App/services';
import { makePersistable } from '.store/mobx-persist-store-virtual-858ce4d906/package';
import ReportedIssue from '../types/session/assignment';
export default class IssueReportingStore {

View file

@ -4,7 +4,6 @@ import {
SITE_ID_STORAGE_KEY,
} from 'App/constants/storageKeys';
import { projectsService } from 'App/services';
import { toast } from '.store/react-toastify-virtual-9dd0f3eae1/package';
import GDPR from './types/gdpr';
import Project from './types/project';

View file

@ -390,10 +390,11 @@ class SearchStore {
// TODO
}
async fetchSessions(
fetchSessions = async (
force: boolean = false,
bookmarked: boolean = false,
): Promise<void> {
): Promise<void> => {
console.log(this.searchInProgress)
if (this.searchInProgress) return;
const filter = this.instance.toSearch();

View file

@ -16,7 +16,6 @@ import { LAST_7_DAYS } from 'Types/app/period';
import { filterMap } from 'App/mstore/searchStore';
import { getDateRangeFromValue } from 'App/dateRange';
import { searchStore, searchStoreLive } from './index';
import { checkEventWithFilters } from '@/components/Session_/Player/Controls/checkEventWithFilters';
const range = getDateRangeFromValue(LAST_7_DAYS);
const defaultDateFilters = {
@ -341,14 +340,7 @@ export default class SessionStore {
const eventsData: Record<string, any[]> = {};
try {
const evData = await sessionService.getSessionEvents(sessionId);
Object.assign(eventsData, {
...evData,
events: evData.events.map((e) => ({
...e,
isHighlighted: checkEventWithFilters(e, searchStore.instance.filters)
}))
});
Object.assign(eventsData, evData);
} catch (e) {
console.error('Failed to fetch events', e);
}

View file

@ -6,6 +6,7 @@ import {
SHOWN_TIMEZONE,
DURATION_FILTER,
MOUSE_TRAIL,
VIRTUAL_MODE_KEY,
} from 'App/constants/storageKeys';
import { DateTime, Settings } from 'luxon';
@ -71,27 +72,19 @@ export const generateGMTZones = (): Timezone[] => {
export default class SessionSettings {
defaultTimezones = [...generateGMTZones()];
skipToIssue: boolean = localStorage.getItem(SKIP_TO_ISSUE) === 'true';
timezone: Timezone;
durationFilter: any = JSON.parse(
localStorage.getItem(DURATION_FILTER) ||
JSON.stringify(defaultDurationFilter),
);
captureRate: string = '0';
conditionalCapture: boolean = false;
captureConditions: { name: string; captureRate: number; filters: any[] }[] =
[];
mouseTrail: boolean = localStorage.getItem(MOUSE_TRAIL) !== 'false';
shownTimezone: 'user' | 'local';
virtualMode: boolean = localStorage.getItem(VIRTUAL_MODE_KEY) === 'true';
usingLocal: boolean = false;
constructor() {

View file

@ -163,6 +163,7 @@ export default class Widget {
fromJson(json: any, period?: any) {
json.config = json.config || {};
runInAction(() => {
this.dashboardId = json.dashboardId;
this.metricId = json.metricId;
this.widgetId = json.widgetId;
this.metricValue = this.metricValueFromArray(

View file

@ -53,8 +53,6 @@ export const blockValues = [
export default class UiPlayerStore {
fullscreen = false;
showOnlySearchEvents = false;
showSearchEventsSwitchButton = false;
bottomBlock = 0;
@ -147,12 +145,4 @@ export default class UiPlayerStore {
setZoomTab = (tab: 'overview' | 'journey' | 'issues' | 'errors') => {
this.zoomTab = tab;
};
setShowOnlySearchEvents = (show: boolean) => {
this.showOnlySearchEvents = show;
};
setSearchEventsSwitchButton = (show: boolean) => {
this.showSearchEventsSwitchButton = show;
};
}

View file

@ -80,7 +80,6 @@ export default class MessageLoader {
let artificialStartTime = Infinity;
let startTimeSet = false;
msgs.forEach((msg, i) => {
if (msg.tp === MType.Redux || msg.tp === MType.ReduxDeprecated) {
if ('actionTime' in msg && msg.actionTime) {

View file

@ -1,7 +1,7 @@
// @ts-ignore
import { Decoder } from 'syncod';
import logger from 'App/logger';
import { VIRTUAL_MODE_KEY } from '@/constants/storageKeys';
import type { Store, ILog, SessionFilesInfo } from 'Player';
import TabSessionManager, { TabState } from 'Player/web/TabManager';
import ActiveTabManager from 'Player/web/managers/ActiveTabManager';
@ -69,6 +69,7 @@ export interface State extends ScreenState {
tabChangeEvents: TabChangeEvent[];
closedTabs: string[];
sessionStart: number;
vModeBadge: boolean;
}
export const visualChanges = [
@ -99,6 +100,7 @@ export default class MessageManager {
closedTabs: [],
sessionStart: 0,
tabNames: {},
vModeBadge: false,
};
private clickManager: ListWalker<MouseClick> = new ListWalker();
@ -126,7 +128,6 @@ export default class MessageManager {
private tabsAmount = 0;
private tabChangeEvents: TabChangeEvent[] = [];
private activeTab = '';
constructor(
@ -142,8 +143,19 @@ export default class MessageManager {
this.activityManager = new ActivityManager(
this.session.duration.milliseconds,
); // only if not-live
const vMode = localStorage.getItem(VIRTUAL_MODE_KEY);
if (vMode === 'true') {
this.setVirtualMode(true);
}
}
private virtualMode = false;
public setVirtualMode = (virtualMode: boolean) => {
this.virtualMode = virtualMode;
Object.values(this.tabs).forEach((tab) => tab.setVirtualMode(virtualMode));
};
public getListsFullState = () => {
const fullState: Record<string, any> = {};
for (const tab in Object.keys(this.tabs)) {
@ -394,6 +406,9 @@ export default class MessageManager {
this.sessionStart,
this.initialLists,
);
if (this.virtualMode) {
this.tabs[msg.tabId].setVirtualMode(this.virtualMode);
}
}
const lastMessageTime = Math.max(msg.time, this.lastMessageTime);

View file

@ -99,6 +99,7 @@ export default class TabSessionManager {
tabStates: { [tabId: string]: TabState };
tabNames: { [tabId: string]: string };
location?: string;
vModeBadge?: boolean;
}>,
private readonly screen: Screen,
private readonly id: string,
@ -116,6 +117,13 @@ export default class TabSessionManager {
screen,
this.session.isMobile,
this.setCSSLoading,
() => {
setTimeout(() => {
this.state.update({
vModeBadge: true,
})
}, 0)
}
);
this.lists = new Lists(initialLists);
initialLists?.event?.forEach((e: Record<string, string>) => {
@ -126,6 +134,10 @@ export default class TabSessionManager {
});
}
public setVirtualMode = (virtualMode: boolean) => {
this.pagesManager.setVirtualMode(virtualMode);
};
setSession = (session: any) => {
this.session = session;
};

View file

@ -21,15 +21,10 @@ export default class WebPlayer extends Player {
inspectorMode: false,
mobsFetched: false,
};
private inspectorController: InspectorController;
protected screen: Screen;
protected readonly messageManager: MessageManager;
protected readonly messageLoader: MessageLoader;
private targetMarker: TargetMarker;
constructor(
@ -104,6 +99,10 @@ export default class WebPlayer extends Player {
window.__OPENREPLAY_DEV_TOOLS__.player = this;
}
enableVMode = () => {
this.messageManager.setVirtualMode(true);
}
preloadFirstFile(data: Uint8Array, fileKey?: string) {
void this.messageLoader.preloadFirstFile(data, fileKey);
}

View file

@ -140,11 +140,16 @@ class SimpleHeatmap {
ctx.drawImage(this.circle, p[0] - this.r, p[1] - this.r);
});
const colored = ctx.getImageData(0, 0, this.width, this.height);
this.colorize(colored.data, this.grad);
ctx.putImageData(colored, 0, 0);
return this;
try {
const colored = ctx.getImageData(0, 0, this.width, this.height);
this.colorize(colored.data, this.grad);
ctx.putImageData(colored, 0, 0);
} catch (e) {
// usually happens if session is corrupted ?
console.error('Error while colorizing heatmap:', e);
} finally {
return this;
}
}
private colorize(

View file

@ -44,47 +44,34 @@ const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/;
export default class DOMManager extends ListWalker<Message> {
private readonly vTexts: Map<number, VText> = new Map(); // map vs object here?
private readonly vElements: Map<number, VElement> = new Map();
private readonly olVRoots: Map<number, OnloadVRoot> = new Map();
/** required to keep track of iframes, frameId : vnodeId */
private readonly iframeRoots: Record<number, number> = {};
private shadowRootParentMap: Map<number, number> = new Map();
/** Constructed StyleSheets https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
* as well as <style> tag owned StyleSheets
*/
private olStyleSheets: Map<number, OnloadStyleSheet> = new Map();
/** @depreacted since tracker 4.0.2 Mapping by nodeID */
private olStyleSheetsDeprecated: Map<number, OnloadStyleSheet> = new Map();
private upperBodyId: number = -1;
private nodeScrollManagers: Map<number, ListWalker<SetNodeScroll>> =
new Map();
private stylesManager: StylesManager;
private focusManager: FocusManager = new FocusManager(this.vElements);
private selectionManager: SelectionManager;
private readonly screen: Screen;
private readonly isMobile: boolean;
private readonly stringDict: Record<number, string>;
private readonly globalDict: {
get: (key: string) => string | undefined;
all: () => Record<string, string>;
};
public readonly time: number;
private virtualMode = false;
private hasSlots = false
private showVModeBadge?: () => void;
constructor(params: {
screen: Screen;
@ -96,6 +83,8 @@ export default class DOMManager extends ListWalker<Message> {
get: (key: string) => string | undefined;
all: () => Record<string, string>;
};
virtualMode?: boolean;
showVModeBadge?: () => void;
}) {
super();
this.screen = params.screen;
@ -105,6 +94,8 @@ export default class DOMManager extends ListWalker<Message> {
this.globalDict = params.globalDict;
this.selectionManager = new SelectionManager(this.vElements, params.screen);
this.stylesManager = new StylesManager(params.screen, params.setCssLoading);
this.virtualMode = params.virtualMode || false;
this.showVModeBadge = params.showVModeBadge;
setupWindowLogging(this.vTexts, this.vElements, this.olVRoots);
}
@ -307,6 +298,9 @@ export default class DOMManager extends ListWalker<Message> {
this.insertNode(msg);
this.removeBodyScroll(msg.id, vElem);
this.removeAutocomplete(vElem);
if (msg.tag === 'SLOT') {
this.hasSlots = true;
}
return;
}
case MType.MoveNode: {
@ -450,14 +444,17 @@ export default class DOMManager extends ListWalker<Message> {
return;
}
// shadow DOM for a custom element + SALESFORCE (<slot>)
const isCustomElement = vElem.tagName.includes('-') || vElem.tagName === 'SLOT';
const isNotActualIframe = !["IFRAME", "FRAME"].includes(vElem.tagName.toUpperCase());
const isLikelyShadowRoot = isCustomElement && isNotActualIframe;
const isCustomElement =
vElem.tagName.includes('-') || vElem.tagName === 'SLOT';
if (isLikelyShadowRoot) {
// Store the mapping but don't create the actual shadow root
this.shadowRootParentMap.set(msg.id, msg.frameID);
return;
if (isCustomElement) {
if (this.virtualMode) {
// Store the mapping but don't create the actual shadow root
this.shadowRootParentMap.set(msg.id, msg.frameID);
return;
} else if (this.hasSlots) {
this.showVModeBadge?.();
}
}
// Real iframes
@ -473,7 +470,11 @@ export default class DOMManager extends ListWalker<Message> {
case MType.AdoptedSsInsertRule: {
const styleSheet = this.olStyleSheets.get(msg.sheetID);
if (!styleSheet) {
logger.warn('No stylesheet was created for ', msg, this.olStyleSheets);
logger.warn(
'No stylesheet was created for ',
msg,
this.olStyleSheets,
);
return;
}
insertRule(styleSheet, msg);

View file

@ -22,6 +22,7 @@ export default class PagesManager extends ListWalker<DOMManager> {
private screen: Screen,
private isMobile: boolean,
private setCssLoading: (flag: boolean) => void,
private showVModeBadge: () => void,
) {
super();
}
@ -30,6 +31,10 @@ export default class PagesManager extends ListWalker<DOMManager> {
Assumed that messages added in a correct time sequence.
*/
falseOrder = false;
virtualMode = false;
setVirtualMode = (virtualMode: boolean) => {
this.virtualMode = virtualMode;
};
appendMessage(m: Message): void {
if ([MType.StringDict, MType.StringDictGlobal].includes(m.tp)) {
@ -62,6 +67,8 @@ export default class PagesManager extends ListWalker<DOMManager> {
get: (key: string) => this.globalDictionary.get(key),
all: () => Object.fromEntries(this.globalDictionary),
},
virtualMode: this.virtualMode,
showVModeBadge: this.showVModeBadge,
}),
);
this.falseOrder = false;

View file

@ -51,7 +51,6 @@ interface IEvent {
path: string;
label: string;
};
isHighlighted?: boolean;
}
interface ConsoleEvent extends IEvent {
@ -119,8 +118,6 @@ class Event {
messageId: IEvent['messageId'];
isHighlighted: IEvent['isHighlighted'];
constructor(event: IEvent) {
Object.assign(this, {
time: event.time,
@ -128,7 +125,6 @@ class Event {
key: event.key,
tabId: event.tabId,
messageId: event.messageId,
isHighlighted: event.isHighlighted,
target: {
path: event.target?.path || event.targetPath,
label: event.target?.label,
@ -190,7 +186,6 @@ export class Click extends Event {
this.count = evt.count;
this.hesitation = evt.hesitation;
this.selector = evt.selector;
this.isHighlighted = evt.isHighlighted;
if (isClickRage) {
this.type = CLICKRAGE;
}

View file

@ -38,7 +38,6 @@ export function debounceCall(func, wait) {
};
}
export function randomInt(a, b) {
const min = (b ? a : 0) - 0.5;
const max = b || a || Number.MAX_SAFE_INTEGER;

View file

@ -54,6 +54,25 @@ server {
add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization,Content-Encoding';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
}
location /integrations/ {
rewrite ^/integrations/(.*) /$1 break;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-For $real_ip;
proxy_set_header X-Forwarded-Host $real_ip;
proxy_set_header X-Real-IP $real_ip;
proxy_set_header Host $host;
proxy_pass http://integrations-openreplay:8080;
proxy_read_timeout 300;
proxy_connect_timeout 120;
proxy_send_timeout 300;
# CORS Headers
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'POST,PATCH,OPTIONS,DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization,Content-Encoding,X-Openreplay-Batch';
add_header 'Access-Control-Expose-Headers' 'Content-Length';
}
location /api/ {
rewrite ^/api/(.*) /$1 break;

View file

@ -18,4 +18,4 @@ version: 0.1.7
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.22.5"
AppVersion: "v1.22.7"

View file

@ -1,7 +1,6 @@
apiVersion: v2
name: db
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
@ -11,14 +10,12 @@ description: A Helm chart for Kubernetes
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.22.0"
AppVersion: "v1.22.1"

View file

@ -18,4 +18,4 @@ version: 0.1.10
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
AppVersion: "v1.22.27"
AppVersion: "v1.22.42"

View file

@ -106,7 +106,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
user_city LowCardinality(String),
user_state LowCardinality(String),
platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
platform Enum8('web'=1,'mobile'=2) DEFAULT 'web',
datetime DateTime,
timezone LowCardinality(Nullable(String)),
duration UInt32,

View file

@ -106,7 +106,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
user_city LowCardinality(String),
user_state LowCardinality(String),
platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
platform Enum8('web'=1,'mobile'=2) DEFAULT 'web',
datetime DateTime,
timezone LowCardinality(Nullable(String)),
duration UInt32,