Compare commits

..

1 commit

Author SHA1 Message Date
Shekar Siri
41ec24fa31 fix(auth): remove unnecessary captcha token validation
The token validation checks were redundant as the validation is already
handled by the captcha wrapper component. This change simplifies the
password reset flow while maintaining security.
2025-03-21 10:54:07 +01:00
192 changed files with 2251 additions and 4880 deletions

View file

@ -1,122 +0,0 @@
# This action will push the assist changes to aws
on:
workflow_dispatch:
inputs:
skip_security_checks:
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
required: false
default: "false"
push:
branches:
- dev
paths:
- "ee/assist-server/**"
name: Build and Deploy Assist-Server EE
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
# We need to diff with old commit
# to see which workers got changed.
fetch-depth: 2
- uses: ./.github/composite-actions/update-keys
with:
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
assist_key: ${{ secrets.ASSIST_KEY }}
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
license_key: ${{ secrets.EE_LICENSE_KEY }}
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
pg_password: ${{ secrets.EE_PG_PASSWORD }}
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
name: Update Keys
- name: Docker login
run: |
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
- uses: azure/k8s-set-context@v1
with:
method: kubeconfig
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
id: setcontext
- name: Building and Pushing Assist-Server image
id: build-image
env:
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
ENVIRONMENT: staging
run: |
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
cd assist-server
PUSH_IMAGE=0 bash -x ./build.sh ee
[[ "x$skip_security_checks" == "xtrue" ]] || {
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
images=("assist-server")
for image in ${images[*]};do
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
done
err_code=$?
[[ $err_code -ne 0 ]] && {
exit $err_code
}
} && {
echo "Skipping Security Checks"
}
images=("assist-server")
for image in ${images[*]};do
docker push $DOCKER_REPO/$image:$IMAGE_TAG
done
- name: Creating old image input
run: |
#
# Create yaml with existing image tags
#
kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\
tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt
echo > /tmp/image_override.yaml
for line in `cat /tmp/image_tag.txt`;
do
image_array=($(echo "$line" | tr ':' '\n'))
cat <<EOF >> /tmp/image_override.yaml
${image_array[0]}:
image:
# We've to strip off the -ee, as helm will append it.
tag: `echo ${image_array[1]} | cut -d '-' -f 1`
EOF
done
- name: Deploy to kubernetes
run: |
pwd
cd scripts/helmcharts/
# Update changed image tag
sed -i "/assist-server/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
cat /tmp/image_override.yaml
# Deploy command
mkdir -p /tmp/charts
mv openreplay/charts/{ingress-nginx,assist-server,quickwit,connector} /tmp/charts/
rm -rf openreplay/charts/*
mv /tmp/charts/* openreplay/charts/
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
env:
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
# We're not passing -ee flag, because helm will add that.
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
ENVIRONMENT: staging

View file

@ -1,189 +0,0 @@
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
on:
workflow_dispatch:
inputs:
services:
description: 'Comma separated names of services to build(in small letters).'
required: true
default: 'chalice,frontend'
tag:
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
name: Build patches from tag, rewrite commit HEAD to older timestamp, and Push the tag
jobs:
deploy:
name: Build Patch from old tag
runs-on: ubuntu-latest
env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 4
ref: ${{ github.event.inputs.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
- name: Create backup tag with timestamp
run: |
set -e # Exit immediately if a command exits with a non-zero status
TIMESTAMP=$(date +%Y%m%d%H%M%S)
BACKUP_TAG="${{ github.event.inputs.tag }}-backup-${TIMESTAMP}"
echo "BACKUP_TAG=${BACKUP_TAG}" >> $GITHUB_ENV
echo "INPUT_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
git tag $BACKUP_TAG || { echo "Failed to create backup tag"; exit 1; }
git push origin $BACKUP_TAG || { echo "Failed to push backup tag"; exit 1; }
echo "Created backup tag: $BACKUP_TAG"
# Get the oldest commit date from the last 3 commits in raw format
OLDEST_COMMIT_TIMESTAMP=$(git log -3 --pretty=format:"%at" | tail -1)
echo "Oldest commit timestamp: $OLDEST_COMMIT_TIMESTAMP"
# Add 1 second to the timestamp
NEW_TIMESTAMP=$((OLDEST_COMMIT_TIMESTAMP + 1))
echo "NEW_TIMESTAMP=$NEW_TIMESTAMP" >> $GITHUB_ENV
- name: Setup yq
uses: mikefarah/yq@master
# Configure AWS credentials for the first registry
- name: Configure AWS credentials for RELEASE_ARM_REGISTRY
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_DEPOT_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_DEPOT_SECRET_KEY }}
aws-region: ${{ secrets.AWS_DEPOT_DEFAULT_REGION }}
- name: Login to Amazon ECR for RELEASE_ARM_REGISTRY
id: login-ecr-arm
run: |
aws ecr get-login-password --region ${{ secrets.AWS_DEPOT_DEFAULT_REGION }} | docker login --username AWS --password-stdin ${{ secrets.RELEASE_ARM_REGISTRY }}
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
- name: Get HEAD Commit ID
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
- name: Define Branch Name
run: echo "BRANCH_NAME=${{inputs.branch}}" >> $GITHUB_ENV
- name: Build
id: build-image
env:
DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
MSAAS_REPO_FOLDER: /tmp/msaas
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
}
function clone_msaas() {
[ -d $MSAAS_REPO_FOLDER ] || {
git clone -b $INPUT_TAG --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 $INPUT_TAG
git log -1
cd $MSAAS_REPO_FOLDER
bash git-init.sh
git checkout
}
}
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
}
# 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"
done
- name: Change commit timestamp
run: |
# Convert the timestamp to a date format git can understand
NEW_DATE=$(perl -le 'print scalar gmtime($ARGV[0])." +0000"' $NEW_TIMESTAMP)
echo "Setting commit date to: $NEW_DATE"
# Amend the commit with the new date
GIT_COMMITTER_DATE="$NEW_DATE" git commit --amend --no-edit --date="$NEW_DATE"
# Verify the change
git log -1 --pretty=format:"Commit now dated: %cD"
# git tag and push
git tag $INPUT_TAG -f
git push origin $INPUT_TAG -f
# - name: Debug Job
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
# env:
# DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
# DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
# MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
# MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
# MSAAS_REPO_FOLDER: /tmp/msaas
# with:
# limit-access-to-actor: true

View file

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

View file

@ -1,42 +1,35 @@
on: on:
pull_request: workflow_dispatch:
types: [closed] description: "This workflow will build for patches for latest tag, and will Always use commit from main branch."
branches: inputs:
- main services:
name: Release tag update --force 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
jobs: jobs:
deploy: deploy:
name: Build Patch from main name: Build Patch from main
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || github.event.inputs.services == 'true' }} env:
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 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 - name: Set Remote with GITHUB_TOKEN
run: | run: |
git config --unset http.https://github.com/.extraheader 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 remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git
- name: Push main branch to tag - name: Push main branch to tag
run: | run: |
git fetch --tags
git checkout main git checkout main
echo "Updating tag ${{ env.LATEST_TAG }} to point to latest commit on main" git push origin HEAD:refs/tags/$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n 1) --force
git push origin HEAD:refs/tags/${{ env.LATEST_TAG }} --force # - name: Debug Job
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
# with:
# limit-access-to-actor: true

View file

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

View file

@ -338,14 +338,14 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
SELECT details.error_id as error_id, SELECT details.error_id as error_id,
name, message, users, total, name, message, users, total,
sessions, last_occurrence, first_occurrence, chart sessions, last_occurrence, first_occurrence, chart
FROM (SELECT error_id, FROM (SELECT JSONExtractString(toString(`$properties`), 'error_id') AS error_id,
JSONExtractString(toString(`$properties`), 'name') AS name, JSONExtractString(toString(`$properties`), 'name') AS name,
JSONExtractString(toString(`$properties`), 'message') AS message, JSONExtractString(toString(`$properties`), 'message') AS message,
COUNT(DISTINCT user_id) AS users, COUNT(DISTINCT user_id) AS users,
COUNT(DISTINCT events.session_id) AS sessions, COUNT(DISTINCT events.session_id) AS sessions,
MAX(created_at) AS max_datetime, MAX(created_at) AS max_datetime,
MIN(created_at) AS min_datetime, MIN(created_at) AS min_datetime,
COUNT(DISTINCT error_id) COUNT(DISTINCT JSONExtractString(toString(`$properties`), 'error_id'))
OVER() AS total OVER() AS total
FROM {MAIN_EVENTS_TABLE} AS events FROM {MAIN_EVENTS_TABLE} AS events
INNER JOIN (SELECT session_id, coalesce(user_id,toString(user_uuid)) AS user_id INNER JOIN (SELECT session_id, coalesce(user_id,toString(user_uuid)) AS user_id
@ -357,7 +357,7 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
GROUP BY error_id, name, message GROUP BY error_id, name, message
ORDER BY {sort} {order} ORDER BY {sort} {order}
LIMIT %(errors_limit)s OFFSET %(errors_offset)s) AS details LIMIT %(errors_limit)s OFFSET %(errors_offset)s) AS details
INNER JOIN (SELECT error_id, INNER JOIN (SELECT JSONExtractString(toString(`$properties`), 'error_id') AS error_id,
toUnixTimestamp(MAX(created_at))*1000 AS last_occurrence, toUnixTimestamp(MAX(created_at))*1000 AS last_occurrence,
toUnixTimestamp(MIN(created_at))*1000 AS first_occurrence toUnixTimestamp(MIN(created_at))*1000 AS first_occurrence
FROM {MAIN_EVENTS_TABLE} FROM {MAIN_EVENTS_TABLE}
@ -366,7 +366,7 @@ def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, us
GROUP BY error_id) AS time_details GROUP BY error_id) AS time_details
ON details.error_id=time_details.error_id ON details.error_id=time_details.error_id
INNER JOIN (SELECT error_id, groupArray([timestamp, count]) AS chart INNER JOIN (SELECT error_id, groupArray([timestamp, count]) AS chart
FROM (SELECT error_id, FROM (SELECT JSONExtractString(toString(`$properties`), 'error_id') AS error_id,
gs.generate_series AS timestamp, gs.generate_series AS timestamp,
COUNT(DISTINCT session_id) AS count COUNT(DISTINCT session_id) AS count
FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS gs FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS gs

View file

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

View file

@ -85,9 +85,6 @@ def __complete_missing_steps(start_time, end_time, density, neutral, rows, time_
# compute avg_time_from_previous at the same level as sessions_count (this was removed in v1.22) # compute avg_time_from_previous at the same level as sessions_count (this was removed in v1.22)
# if start-point is selected, the selected event is ranked n°1 # if start-point is selected, the selected event is ranked n°1
def path_analysis(project_id: int, data: schemas.CardPathAnalysis): def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
if not data.hide_excess:
data.hide_excess = True
data.rows = 50
sub_events = [] sub_events = []
start_points_conditions = [] start_points_conditions = []
step_0_conditions = [] step_0_conditions = []

View file

@ -153,7 +153,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True, "isEvent": True,
"value": [], "value": [],
"operator": e.operator, "operator": e.operator,
"filters": e.filters "filters": []
}) })
for v in e.value: for v in e.value:
if v not in extra_conditions[e.operator].value: if v not in extra_conditions[e.operator].value:
@ -178,7 +178,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True, "isEvent": True,
"value": [], "value": [],
"operator": e.operator, "operator": e.operator,
"filters": e.filters "filters": []
}) })
for v in e.value: for v in e.value:
if v not in extra_conditions[e.operator].value: if v not in extra_conditions[e.operator].value:
@ -1108,12 +1108,8 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
is_any = sh.isAny_opreator(f.operator) is_any = sh.isAny_opreator(f.operator)
if is_any or len(f.value) == 0: if is_any or len(f.value) == 0:
continue continue
is_negative_operator = sh.is_negation_operator(f.operator)
f.value = helper.values_for_operator(value=f.value, op=f.operator) f.value = helper.values_for_operator(value=f.value, op=f.operator)
op = sh.get_sql_operator(f.operator) op = sh.get_sql_operator(f.operator)
r_op = ""
if is_negative_operator:
r_op = sh.reverse_sql_operator(op)
e_k_f = e_k + f"_fetch{j}" e_k_f = e_k + f"_fetch{j}"
full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)} full_args = {**full_args, **sh.multi_values(f.value, value_key=e_k_f)}
if f.type == schemas.FetchFilterType.FETCH_URL: if f.type == schemas.FetchFilterType.FETCH_URL:
@ -1122,12 +1118,6 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
)) ))
events_conditions[-1]["condition"].append(event_where[-1]) events_conditions[-1]["condition"].append(event_where[-1])
apply = True apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = sh.multi_conditions(
f"sub.`$properties`.url_path {r_op} %({e_k_f})s", f.value, value_key=e_k_f)
elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE: elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE:
event_where.append(json_condition( event_where.append(json_condition(
"main", "$properties", 'status', op, f.value, e_k_f, True, True "main", "$properties", 'status', op, f.value, e_k_f, True, True
@ -1140,13 +1130,6 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
)) ))
events_conditions[-1]["condition"].append(event_where[-1]) events_conditions[-1]["condition"].append(event_where[-1])
apply = True apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = sh.multi_conditions(
f"sub.`$properties`.method {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
elif f.type == schemas.FetchFilterType.FETCH_DURATION: elif f.type == schemas.FetchFilterType.FETCH_DURATION:
event_where.append( event_where.append(
sh.multi_conditions(f"main.`$duration_s` {f.operator} %({e_k_f})s/1000", f.value, sh.multi_conditions(f"main.`$duration_s` {f.operator} %({e_k_f})s/1000", f.value,
@ -1159,26 +1142,12 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
)) ))
events_conditions[-1]["condition"].append(event_where[-1]) events_conditions[-1]["condition"].append(event_where[-1])
apply = True apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = sh.multi_conditions(
f"sub.`$properties`.request_body {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
elif f.type == schemas.FetchFilterType.FETCH_RESPONSE_BODY: elif f.type == schemas.FetchFilterType.FETCH_RESPONSE_BODY:
event_where.append(json_condition( event_where.append(json_condition(
"main", "$properties", 'response_body', op, f.value, e_k_f "main", "$properties", 'response_body', op, f.value, e_k_f
)) ))
events_conditions[-1]["condition"].append(event_where[-1]) events_conditions[-1]["condition"].append(event_where[-1])
apply = True apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.`$event_name`='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = sh.multi_conditions(
f"sub.`$properties`.response_body {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
else: else:
logging.warning(f"undefined FETCH filter: {f.type}") logging.warning(f"undefined FETCH filter: {f.type}")
if not apply: if not apply:
@ -1426,30 +1395,17 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
if extra_conditions and len(extra_conditions) > 0: if extra_conditions and len(extra_conditions) > 0:
_extra_or_condition = [] _extra_or_condition = []
for i, c in enumerate(extra_conditions): for i, c in enumerate(extra_conditions):
if sh.isAny_opreator(c.operator) and c.type != schemas.EventType.REQUEST_DETAILS.value: if sh.isAny_opreator(c.operator):
continue continue
e_k = f"ec_value{i}" e_k = f"ec_value{i}"
op = sh.get_sql_operator(c.operator) op = sh.get_sql_operator(c.operator)
c.value = helper.values_for_operator(value=c.value, op=c.operator) c.value = helper.values_for_operator(value=c.value, op=c.operator)
full_args = {**full_args, full_args = {**full_args,
**sh.multi_values(c.value, value_key=e_k)} **sh.multi_values(c.value, value_key=e_k)}
if c.type in (schemas.EventType.LOCATION.value, schemas.EventType.REQUEST.value): if c.type == events.EventType.LOCATION.ui_type:
_extra_or_condition.append( _extra_or_condition.append(
sh.multi_conditions(f"extra_event.url_path {op} %({e_k})s", sh.multi_conditions(f"extra_event.url_path {op} %({e_k})s",
c.value, value_key=e_k)) c.value, value_key=e_k))
elif c.type == schemas.EventType.REQUEST_DETAILS.value:
for j, c_f in enumerate(c.filters):
if sh.isAny_opreator(c_f.operator) or len(c_f.value) == 0:
continue
e_k += f"_{j}"
op = sh.get_sql_operator(c_f.operator)
c_f.value = helper.values_for_operator(value=c_f.value, op=c_f.operator)
full_args = {**full_args,
**sh.multi_values(c_f.value, value_key=e_k)}
if c_f.type == schemas.FetchFilterType.FETCH_URL.value:
_extra_or_condition.append(
sh.multi_conditions(f"extra_event.url_path {op} %({e_k})s",
c_f.value, value_key=e_k))
else: else:
logging.warning(f"unsupported extra_event type:${c.type}") logging.warning(f"unsupported extra_event type:${c.type}")
if len(_extra_or_condition) > 0: if len(_extra_or_condition) > 0:

View file

@ -148,7 +148,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True, "isEvent": True,
"value": [], "value": [],
"operator": e.operator, "operator": e.operator,
"filters": e.filters "filters": []
}) })
for v in e.value: for v in e.value:
if v not in extra_conditions[e.operator].value: if v not in extra_conditions[e.operator].value:
@ -165,7 +165,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True, "isEvent": True,
"value": [], "value": [],
"operator": e.operator, "operator": e.operator,
"filters": e.filters "filters": []
}) })
for v in e.value: for v in e.value:
if v not in extra_conditions[e.operator].value: if v not in extra_conditions[e.operator].value:
@ -989,7 +989,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s", sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s",
c.value, value_key=e_k)) c.value, value_key=e_k))
else: else:
logger.warning(f"unsupported extra_event type: {c.type}") logger.warning(f"unsupported extra_event type:${c.type}")
if len(_extra_or_condition) > 0: if len(_extra_or_condition) > 0:
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")") extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
query_part = f"""\ query_part = f"""\

View file

@ -4,41 +4,37 @@ import schemas
def get_sql_operator(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator, schemas.MathOperator]): def get_sql_operator(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator, schemas.MathOperator]):
if isinstance(op, Enum):
op = op.value
return { return {
schemas.SearchEventOperator.IS.value: "=", schemas.SearchEventOperator.IS: "=",
schemas.SearchEventOperator.ON.value: "=", schemas.SearchEventOperator.ON: "=",
schemas.SearchEventOperator.ON_ANY.value: "IN", schemas.SearchEventOperator.ON_ANY: "IN",
schemas.SearchEventOperator.IS_NOT.value: "!=", schemas.SearchEventOperator.IS_NOT: "!=",
schemas.SearchEventOperator.NOT_ON.value: "!=", schemas.SearchEventOperator.NOT_ON: "!=",
schemas.SearchEventOperator.CONTAINS.value: "ILIKE", schemas.SearchEventOperator.CONTAINS: "ILIKE",
schemas.SearchEventOperator.NOT_CONTAINS.value: "NOT ILIKE", schemas.SearchEventOperator.NOT_CONTAINS: "NOT ILIKE",
schemas.SearchEventOperator.STARTS_WITH.value: "ILIKE", schemas.SearchEventOperator.STARTS_WITH: "ILIKE",
schemas.SearchEventOperator.ENDS_WITH.value: "ILIKE", schemas.SearchEventOperator.ENDS_WITH: "ILIKE",
# Selector operators: # Selector operators:
schemas.ClickEventExtraOperator.IS.value: "=", schemas.ClickEventExtraOperator.IS: "=",
schemas.ClickEventExtraOperator.IS_NOT.value: "!=", schemas.ClickEventExtraOperator.IS_NOT: "!=",
schemas.ClickEventExtraOperator.CONTAINS.value: "ILIKE", schemas.ClickEventExtraOperator.CONTAINS: "ILIKE",
schemas.ClickEventExtraOperator.NOT_CONTAINS.value: "NOT ILIKE", schemas.ClickEventExtraOperator.NOT_CONTAINS: "NOT ILIKE",
schemas.ClickEventExtraOperator.STARTS_WITH.value: "ILIKE", schemas.ClickEventExtraOperator.STARTS_WITH: "ILIKE",
schemas.ClickEventExtraOperator.ENDS_WITH.value: "ILIKE", schemas.ClickEventExtraOperator.ENDS_WITH: "ILIKE",
schemas.MathOperator.GREATER.value: ">", schemas.MathOperator.GREATER: ">",
schemas.MathOperator.GREATER_EQ.value: ">=", schemas.MathOperator.GREATER_EQ: ">=",
schemas.MathOperator.LESS.value: "<", schemas.MathOperator.LESS: "<",
schemas.MathOperator.LESS_EQ.value: "<=", schemas.MathOperator.LESS_EQ: "<=",
}.get(op, "=") }.get(op, "=")
def is_negation_operator(op: schemas.SearchEventOperator): def is_negation_operator(op: schemas.SearchEventOperator):
if isinstance(op, Enum): return op in [schemas.SearchEventOperator.IS_NOT,
op = op.value schemas.SearchEventOperator.NOT_ON,
return op in [schemas.SearchEventOperator.IS_NOT.value, schemas.SearchEventOperator.NOT_CONTAINS,
schemas.SearchEventOperator.NOT_ON.value, schemas.ClickEventExtraOperator.IS_NOT,
schemas.SearchEventOperator.NOT_CONTAINS.value, schemas.ClickEventExtraOperator.NOT_CONTAINS]
schemas.ClickEventExtraOperator.IS_NOT.value,
schemas.ClickEventExtraOperator.NOT_CONTAINS.value]
def reverse_sql_operator(op): def reverse_sql_operator(op):

View file

@ -960,6 +960,36 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
return self return self
# We don't need this as the UI is expecting filters to override the full series' filters
# @model_validator(mode="after")
# def __merge_out_filters_with_series(self):
# for f in self.filters:
# for s in self.series:
# found = False
#
# if f.is_event:
# sub = s.filter.events
# else:
# sub = s.filter.filters
#
# for e in sub:
# if f.type == e.type and f.operator == e.operator:
# found = True
# if f.is_event:
# # If extra event: append value
# for v in f.value:
# if v not in e.value:
# e.value.append(v)
# else:
# # If extra filter: override value
# e.value = f.value
# if not found:
# sub.append(f)
#
# self.filters = []
#
# return self
# UI is expecting filters to override the full series' filters # UI is expecting filters to override the full series' filters
@model_validator(mode="after") @model_validator(mode="after")
def __override_series_filters_with_outer_filters(self): def __override_series_filters_with_outer_filters(self):
@ -1030,16 +1060,6 @@ class CardTable(__CardSchema):
values["metricValue"] = [] values["metricValue"] = []
return values return values
@model_validator(mode="after")
def __enforce_AND_operator(self):
self.metric_of = MetricOfTable(self.metric_of)
if self.metric_of in (MetricOfTable.VISITED_URL, MetricOfTable.FETCH, \
MetricOfTable.VISITED_URL.value, MetricOfTable.FETCH.value):
for s in self.series:
if s.filter is not None:
s.filter.events_order = SearchEventOrder.AND
return self
@model_validator(mode="after") @model_validator(mode="after")
def __transform(self): def __transform(self):
self.metric_of = MetricOfTable(self.metric_of) self.metric_of = MetricOfTable(self.metric_of)
@ -1115,7 +1135,7 @@ class CardPathAnalysis(__CardSchema):
view_type: MetricOtherViewType = Field(...) view_type: MetricOtherViewType = Field(...)
metric_value: List[ProductAnalyticsSelectedEventType] = Field(default_factory=list) metric_value: List[ProductAnalyticsSelectedEventType] = Field(default_factory=list)
density: int = Field(default=4, ge=2, le=10) density: int = Field(default=4, ge=2, le=10)
rows: int = Field(default=5, ge=1, le=10) rows: int = Field(default=3, ge=1, le=10)
start_type: Literal["start", "end"] = Field(default="start") start_type: Literal["start", "end"] = Field(default="start")
start_point: List[PathAnalysisSubFilterSchema] = Field(default_factory=list) start_point: List[PathAnalysisSubFilterSchema] = Field(default_factory=list)

View file

@ -19,16 +19,14 @@ const EVENTS_DEFINITION = {
} }
}; };
EVENTS_DEFINITION.emit = { EVENTS_DEFINITION.emit = {
NEW_AGENT: "NEW_AGENT", NEW_AGENT: "NEW_AGENT",
NO_AGENTS: "NO_AGENT", NO_AGENTS: "NO_AGENT",
AGENT_DISCONNECT: "AGENT_DISCONNECTED", AGENT_DISCONNECT: "AGENT_DISCONNECTED",
AGENTS_CONNECTED: "AGENTS_CONNECTED", AGENTS_CONNECTED: "AGENTS_CONNECTED",
AGENTS_INFO_CONNECTED: "AGENTS_INFO_CONNECTED", NO_SESSIONS: "SESSION_DISCONNECTED",
NO_SESSIONS: "SESSION_DISCONNECTED", SESSION_ALREADY_CONNECTED: "SESSION_ALREADY_CONNECTED",
SESSION_ALREADY_CONNECTED: "SESSION_ALREADY_CONNECTED", SESSION_RECONNECTED: "SESSION_RECONNECTED",
SESSION_RECONNECTED: "SESSION_RECONNECTED", UPDATE_EVENT: EVENTS_DEFINITION.listen.UPDATE_EVENT
UPDATE_EVENT: EVENTS_DEFINITION.listen.UPDATE_EVENT,
WEBRTC_CONFIG: "WEBRTC_CONFIG",
}; };
const BASE_sessionInfo = { const BASE_sessionInfo = {

View file

@ -42,7 +42,7 @@ const findSessionSocketId = async (io, roomId, tabId) => {
}; };
async function getRoomData(io, roomID) { async function getRoomData(io, roomID) {
let tabsCount = 0, agentsCount = 0, tabIDs = [], agentIDs = [], config = null, agentInfos = []; let tabsCount = 0, agentsCount = 0, tabIDs = [], agentIDs = [];
const connected_sockets = await io.in(roomID).fetchSockets(); const connected_sockets = await io.in(roomID).fetchSockets();
if (connected_sockets.length > 0) { if (connected_sockets.length > 0) {
for (let socket of connected_sockets) { for (let socket of connected_sockets) {
@ -52,19 +52,13 @@ async function getRoomData(io, roomID) {
} else { } else {
agentsCount++; agentsCount++;
agentIDs.push(socket.id); agentIDs.push(socket.id);
agentInfos.push({ ...socket.handshake.query.agentInfo, socketId: socket.id });
if (socket.handshake.query.config !== undefined) {
config = socket.handshake.query.config;
}
} }
} }
} else { } else {
tabsCount = -1; tabsCount = -1;
agentsCount = -1; agentsCount = -1;
agentInfos = [];
agentIDs = [];
} }
return {tabsCount, agentsCount, tabIDs, agentIDs, config, agentInfos}; return {tabsCount, agentsCount, tabIDs, agentIDs};
} }
function processNewSocket(socket) { function processNewSocket(socket) {
@ -84,7 +78,7 @@ async function onConnect(socket) {
IncreaseOnlineConnections(socket.handshake.query.identity); IncreaseOnlineConnections(socket.handshake.query.identity);
const io = getServer(); const io = getServer();
const {tabsCount, agentsCount, tabIDs, agentInfos, agentIDs, config} = await getRoomData(io, socket.handshake.query.roomId); const {tabsCount, agentsCount, tabIDs, agentIDs} = await getRoomData(io, socket.handshake.query.roomId);
if (socket.handshake.query.identity === IDENTITIES.session) { if (socket.handshake.query.identity === IDENTITIES.session) {
// Check if session with the same tabID already connected, if so, refuse new connexion // Check if session with the same tabID already connected, if so, refuse new connexion
@ -106,9 +100,7 @@ async function onConnect(socket) {
// Inform all connected agents about reconnected session // Inform all connected agents about reconnected session
if (agentsCount > 0) { if (agentsCount > 0) {
logger.debug(`notifying new session about agent-existence`); logger.debug(`notifying new session about agent-existence`);
io.to(socket.id).emit(EVENTS_DEFINITION.emit.WEBRTC_CONFIG, config);
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agentIDs); io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agentIDs);
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_INFO_CONNECTED, agentInfos);
socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id); socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
} }
} else if (tabsCount <= 0) { } else if (tabsCount <= 0) {
@ -126,8 +118,7 @@ async function onConnect(socket) {
// Stats // Stats
startAssist(socket, socket.handshake.query.agentID); startAssist(socket, socket.handshake.query.agentID);
} }
io.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.WEBRTC_CONFIG, socket.handshake.query.config); socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
socket.to(socket.handshake.query.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, { ...socket.handshake.query.agentInfo });
} }
// Set disconnect handler // Set disconnect handler

View file

@ -2,12 +2,11 @@ package datasaver
import ( import (
"context" "context"
"encoding/json"
"openreplay/backend/pkg/db/types"
"openreplay/backend/internal/config/db" "openreplay/backend/internal/config/db"
"openreplay/backend/pkg/db/clickhouse" "openreplay/backend/pkg/db/clickhouse"
"openreplay/backend/pkg/db/postgres" "openreplay/backend/pkg/db/postgres"
"openreplay/backend/pkg/db/types"
"openreplay/backend/pkg/logger" "openreplay/backend/pkg/logger"
. "openreplay/backend/pkg/messages" . "openreplay/backend/pkg/messages"
queue "openreplay/backend/pkg/queue/types" queue "openreplay/backend/pkg/queue/types"
@ -51,6 +50,10 @@ func New(log logger.Logger, cfg *db.Config, pg *postgres.Conn, ch clickhouse.Con
} }
func (s *saverImpl) Handle(msg Message) { func (s *saverImpl) Handle(msg Message) {
if msg.TypeID() == MsgCustomEvent {
defer s.Handle(types.WrapCustomEvent(msg.(*CustomEvent)))
}
var ( var (
sessCtx = context.WithValue(context.Background(), "sessionID", msg.SessionID()) sessCtx = context.WithValue(context.Background(), "sessionID", msg.SessionID())
session *sessions.Session session *sessions.Session
@ -66,23 +69,6 @@ func (s *saverImpl) Handle(msg Message) {
return 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 IsMobileType(msg.TypeID()) {
if err := s.handleMobileMessage(sessCtx, session, msg); err != nil { if err := s.handleMobileMessage(sessCtx, session, msg); err != nil {
if !postgres.IsPkeyViolation(err) { if !postgres.IsPkeyViolation(err) {

View file

@ -86,8 +86,7 @@ def __generic_query(typename, value_length=None):
ORDER BY value""" ORDER BY value"""
if value_length is None or value_length > 2: if value_length is None or value_length > 2:
return f"""SELECT DISTINCT ON(value, type) value, type return f"""(SELECT DISTINCT value, type
FROM ((SELECT DISTINCT value, type
FROM {TABLE} FROM {TABLE}
WHERE WHERE
project_id = %(project_id)s project_id = %(project_id)s
@ -103,7 +102,7 @@ def __generic_query(typename, value_length=None):
AND type='{typename.upper()}' AND type='{typename.upper()}'
AND value ILIKE %(value)s AND value ILIKE %(value)s
ORDER BY value ORDER BY value
LIMIT 5)) AS raw;""" LIMIT 5);"""
return f"""SELECT DISTINCT value, type return f"""SELECT DISTINCT value, type
FROM {TABLE} FROM {TABLE}
WHERE WHERE
@ -258,7 +257,7 @@ def __search_metadata(project_id, value, key=None, source=None):
WHERE project_id = %(project_id)s WHERE project_id = %(project_id)s
AND {colname} ILIKE %(svalue)s LIMIT 5)""") AND {colname} ILIKE %(svalue)s LIMIT 5)""")
with ch_client.ClickHouseClient() as cur: with ch_client.ClickHouseClient() as cur:
query = cur.format(query=f"""SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE query = cur.format(query=f"""SELECT key, value, 'METADATA' AS TYPE
FROM({" UNION ALL ".join(sub_from)}) AS all_metas FROM({" UNION ALL ".join(sub_from)}) AS all_metas
LIMIT 5;""", parameters={"project_id": project_id, "value": helper.string_to_sql_like(value), LIMIT 5;""", parameters={"project_id": project_id, "value": helper.string_to_sql_like(value),
"svalue": 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) 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 = errors_helper.__get_basic_constraints_ch(time_constraint=False)
ch_basic_query.append("error_id = %(error_id)s") ch_basic_query.append("toString(`$properties`.error_id) = %(error_id)s")
with ch_client.ClickHouseClient() as ch: with ch_client.ClickHouseClient() as ch:
data["startDate24"] = TimeUTC.now(-1) data["startDate24"] = TimeUTC.now(-1)
@ -95,7 +95,7 @@ def get_details(project_id, error_id, user_id, **data):
"error_id": error_id} "error_id": error_id}
main_ch_query = f"""\ main_ch_query = f"""\
WITH pre_processed AS (SELECT error_id, WITH pre_processed AS (SELECT toString(`$properties`.error_id) AS error_id,
toString(`$properties`.name) AS name, toString(`$properties`.name) AS name,
toString(`$properties`.message) AS message, toString(`$properties`.message) AS message,
session_id, session_id,
@ -183,7 +183,7 @@ def get_details(project_id, error_id, user_id, **data):
AND `$event_name` = 'ERROR' AND `$event_name` = 'ERROR'
AND events.created_at >= toDateTime(timestamp / 1000) AND events.created_at >= toDateTime(timestamp / 1000)
AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000) AND events.created_at < toDateTime((timestamp + %(step_size24)s) / 1000)
AND error_id = %(error_id)s AND toString(`$properties`.error_id) = %(error_id)s
GROUP BY timestamp GROUP BY timestamp
ORDER BY timestamp) AS chart_details ORDER BY timestamp) AS chart_details
) AS chart_details24 ON TRUE ) AS chart_details24 ON TRUE
@ -196,7 +196,7 @@ def get_details(project_id, error_id, user_id, **data):
AND `$event_name` = 'ERROR' AND `$event_name` = 'ERROR'
AND events.created_at >= toDateTime(timestamp / 1000) AND events.created_at >= toDateTime(timestamp / 1000)
AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000) AND events.created_at < toDateTime((timestamp + %(step_size30)s) / 1000)
AND error_id = %(error_id)s AND toString(`$properties`.error_id) = %(error_id)s
GROUP BY timestamp GROUP BY timestamp
ORDER BY timestamp) AS chart_details ORDER BY timestamp) AS chart_details
) AS chart_details30 ON TRUE;""" ) AS chart_details30 ON TRUE;"""

View file

@ -927,12 +927,12 @@ def authenticate_sso(email: str, internal_id: str):
aud=AUDIENCE, jwt_jti=j_r.jwt_refresh_jti), aud=AUDIENCE, jwt_jti=j_r.jwt_refresh_jti),
"refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int), "refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int),
"spotJwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'], "spotJwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'],
iat=j_r.spot_jwt_iat, aud=spot.AUDIENCE, for_spot=True), iat=j_r.spot_jwt_iat, aud=spot.AUDIENCE),
"spotRefreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'], "spotRefreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'],
tenant_id=r['tenantId'], tenant_id=r['tenantId'],
iat=j_r.spot_jwt_refresh_iat, iat=j_r.spot_jwt_refresh_iat,
aud=spot.AUDIENCE, aud=spot.AUDIENCE,
jwt_jti=j_r.spot_jwt_refresh_jti, for_spot=True), jwt_jti=j_r.spot_jwt_refresh_jti),
"spotRefreshTokenMaxAge": config("JWT_SPOT_REFRESH_EXPIRATION", cast=int) "spotRefreshTokenMaxAge": config("JWT_SPOT_REFRESH_EXPIRATION", cast=int)
} }
return response return response

View file

@ -1,16 +1,3 @@
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'; CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.22.0-ee';
SET allow_experimental_json_type = 1; SET allow_experimental_json_type = 1;
@ -164,7 +151,8 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, "$event_name", created_at, session_id) ORDER BY (project_id, "$event_name", created_at, session_id)
TTL _deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00'; TTL _timestamp + INTERVAL 1 MONTH ,
_deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00';
-- The list of events that should not be ingested, -- The list of events that should not be ingested,
-- according to a specific event_name and optional properties -- according to a specific event_name and optional properties

View file

@ -9,7 +9,8 @@ CREATE TABLE IF NOT EXISTS experimental.autocomplete
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp) PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, type, value); ORDER BY (project_id, type, value)
TTL _timestamp + INTERVAL 1 MONTH;
CREATE TABLE IF NOT EXISTS experimental.events CREATE TABLE IF NOT EXISTS experimental.events
( (
@ -86,7 +87,8 @@ CREATE TABLE IF NOT EXISTS experimental.events
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(datetime) PARTITION BY toYYYYMM(datetime)
ORDER BY (project_id, datetime, event_type, session_id, message_id); ORDER BY (project_id, datetime, event_type, session_id, message_id)
TTL datetime + INTERVAL 3 MONTH;
@ -106,7 +108,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_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_city LowCardinality(String),
user_state LowCardinality(String), user_state LowCardinality(String),
platform Enum8('web'=1,'mobile'=2) DEFAULT 'web', platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
datetime DateTime, datetime DateTime,
timezone LowCardinality(Nullable(String)), timezone LowCardinality(Nullable(String)),
duration UInt32, duration UInt32,
@ -138,6 +140,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMMDD(datetime) PARTITION BY toYYYYMMDD(datetime)
ORDER BY (project_id, datetime, session_id) ORDER BY (project_id, datetime, session_id)
TTL datetime + INTERVAL 3 MONTH
SETTINGS index_granularity = 512; SETTINGS index_granularity = 512;
CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
@ -149,7 +152,8 @@ CREATE TABLE IF NOT EXISTS experimental.user_favorite_sessions
sign Int8 sign Int8
) ENGINE = CollapsingMergeTree(sign) ) ENGINE = CollapsingMergeTree(sign)
PARTITION BY toYYYYMM(_timestamp) PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, session_id); ORDER BY (project_id, user_id, session_id)
TTL _timestamp + INTERVAL 3 MONTH;
CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
( (
@ -159,7 +163,8 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_sessions
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp) PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, session_id); ORDER BY (project_id, user_id, session_id)
TTL _timestamp + INTERVAL 3 MONTH;
CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
( (
@ -169,7 +174,8 @@ CREATE TABLE IF NOT EXISTS experimental.user_viewed_errors
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp) PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, user_id, error_id); ORDER BY (project_id, user_id, error_id)
TTL _timestamp + INTERVAL 3 MONTH;
CREATE TABLE IF NOT EXISTS experimental.issues CREATE TABLE IF NOT EXISTS experimental.issues
( (
@ -182,7 +188,8 @@ CREATE TABLE IF NOT EXISTS experimental.issues
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(_timestamp) PARTITION BY toYYYYMM(_timestamp)
ORDER BY (project_id, issue_id, type); ORDER BY (project_id, issue_id, type)
TTL _timestamp + INTERVAL 3 MONTH;
@ -285,7 +292,8 @@ CREATE TABLE IF NOT EXISTS experimental.sessions_feature_flags
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(datetime) PARTITION BY toYYYYMM(datetime)
ORDER BY (project_id, datetime, session_id, feature_flag_id, condition_id); ORDER BY (project_id, datetime, session_id, feature_flag_id, condition_id)
TTL datetime + INTERVAL 3 MONTH;
CREATE TABLE IF NOT EXISTS experimental.ios_events CREATE TABLE IF NOT EXISTS experimental.ios_events
( (
@ -321,7 +329,8 @@ CREATE TABLE IF NOT EXISTS experimental.ios_events
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
PARTITION BY toYYYYMM(datetime) PARTITION BY toYYYYMM(datetime)
ORDER BY (project_id, datetime, event_type, session_id, message_id); ORDER BY (project_id, datetime, event_type, session_id, message_id)
TTL datetime + INTERVAL 3 MONTH;
SET allow_experimental_json_type = 1; SET allow_experimental_json_type = 1;
@ -475,7 +484,8 @@ CREATE TABLE IF NOT EXISTS product_analytics.events
_timestamp DateTime DEFAULT now() _timestamp DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(_timestamp) ) ENGINE = ReplacingMergeTree(_timestamp)
ORDER BY (project_id, "$event_name", created_at, session_id) ORDER BY (project_id, "$event_name", created_at, session_id)
TTL _deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00'; TTL _timestamp + INTERVAL 1 MONTH ,
_deleted_at + INTERVAL 1 DAY DELETE WHERE _deleted_at != '1970-01-01 00:00:00';
-- The list of events that should not be ingested, -- The list of events that should not be ingested,
-- according to a specific event_name and optional properties -- according to a specific event_name and optional properties

View file

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

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import cn from 'classnames'; import cn from 'classnames';
import Counter from 'App/components/shared/SessionItem/Counter'; import Counter from 'App/components/shared/SessionItem/Counter';
import { useDraggable } from '@neodrag/react'; import Draggable from 'react-draggable';
import type { LocalStream } from 'Player'; import type { LocalStream } from 'Player';
import { PlayerContext } from 'App/components/Session/playerContext'; import { PlayerContext } from 'App/components/Session/playerContext';
import ChatControls from '../ChatControls/ChatControls'; import ChatControls from '../ChatControls/ChatControls';
@ -25,8 +25,6 @@ function ChatWindow({
isPrestart, isPrestart,
}: Props) { }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const dragRef = React.useRef<HTMLDivElement>(null);
useDraggable(dragRef, { bounds: 'body', defaultPosition: { x: 50, y: 200 } })
const { player } = React.useContext(PlayerContext); const { player } = React.useContext(PlayerContext);
const { toggleVideoLocalStream } = player.assistManager; const { toggleVideoLocalStream } = player.assistManager;
@ -41,7 +39,11 @@ function ChatWindow({
}, [localVideoEnabled]); }, [localVideoEnabled]);
return ( return (
<div ref={dragRef}> <Draggable
handle=".handle"
bounds="body"
defaultPosition={{ x: 50, y: 200 }}
>
<div <div
className={cn(stl.wrapper, 'fixed radius bg-white shadow-xl mt-16')} className={cn(stl.wrapper, 'fixed radius bg-white shadow-xl mt-16')}
style={{ width: '280px' }} style={{ width: '280px' }}
@ -100,7 +102,7 @@ function ChatWindow({
isPrestart={isPrestart} isPrestart={isPrestart}
/> />
</div> </div>
</div> </Draggable>
); );
} }

View file

@ -82,7 +82,7 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
{ stream: MediaStream; isAgent: boolean }[] | null { stream: MediaStream; isAgent: boolean }[] | null
>([]); >([]);
const [localStream, setLocalStream] = useState<LocalStream | null>(null); const [localStream, setLocalStream] = useState<LocalStream | null>(null);
const [callObject, setCallObject] = useState<{ end: () => void } | null | undefined>( const [callObject, setCallObject] = useState<{ end: () => void } | null>(
null, null,
); );
@ -135,7 +135,6 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
}, [peerConnectionStatus]); }, [peerConnectionStatus]);
const addIncomeStream = (stream: MediaStream, isAgent: boolean) => { const addIncomeStream = (stream: MediaStream, isAgent: boolean) => {
if (!stream.active) return;
setIncomeStream((oldState) => { setIncomeStream((oldState) => {
if (oldState === null) return [{ stream, isAgent }]; if (oldState === null) return [{ stream, isAgent }];
if ( if (
@ -150,8 +149,13 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
}); });
}; };
const removeIncomeStream = () => { const removeIncomeStream = (stream: MediaStream) => {
setIncomeStream([]); setIncomeStream((prevState) => {
if (!prevState) return [];
return prevState.filter(
(existingStream) => existingStream.stream.id !== stream.id,
);
});
}; };
function onReject() { function onReject() {
@ -177,12 +181,7 @@ function AssistActions({ userId, isCallActive, agentIds }: Props) {
() => { () => {
player.assistManager.ping(AssistActionsPing.call.end, agentId); player.assistManager.ping(AssistActionsPing.call.end, agentId);
lStream.stop.apply(lStream); lStream.stop.apply(lStream);
removeIncomeStream(); removeIncomeStream(lStream.stream);
},
() => {
player.assistManager.ping(AssistActionsPing.call.end, agentId);
lStream.stop.apply(lStream);
removeIncomeStream();
}, },
onReject, onReject,
onError, onError,

View file

@ -34,40 +34,43 @@ function VideoContainer({
} }
const iid = setInterval(() => { const iid = setInterval(() => {
const track = stream.getVideoTracks()[0]; const track = stream.getVideoTracks()[0];
const settings = track?.getSettings();
const isDummyVideoTrack = settings
? settings.width === 2 ||
settings.frameRate === 0 ||
(!settings.frameRate && !settings.width)
: true;
const shouldBeEnabled = track.enabled && !isDummyVideoTrack;
if (track) { if (isEnabled !== shouldBeEnabled) {
if (!track.enabled) { setEnabled(shouldBeEnabled);
setEnabled(false); setRemoteEnabled?.(shouldBeEnabled);
setRemoteEnabled?.(false);
} else {
setEnabled(true);
setRemoteEnabled?.(true);
}
} else {
setEnabled(false);
setRemoteEnabled?.(false);
} }
}, 500); }, 500);
return () => clearInterval(iid); return () => clearInterval(iid);
}, [stream]); }, [stream, isEnabled]);
return ( return (
<div <div
className="flex-1" className="flex-1"
style={{ style={{
display: isEnabled ? undefined : 'none',
width: isEnabled ? undefined : '0px!important', width: isEnabled ? undefined : '0px!important',
height: isEnabled ? undefined : '0px !important', height: isEnabled ? undefined : '0px!important',
border: '1px solid grey', border: '1px solid grey',
transform: local ? 'scaleX(-1)' : undefined, transform: local ? 'scaleX(-1)' : undefined,
display: isEnabled ? 'block' : 'none',
}} }}
> >
<video <video autoPlay ref={ref} muted={muted} style={{ height }} />
autoPlay {isAgent ? (
ref={ref} <div
muted={muted} style={{
style={{ height }} position: 'absolute',
/> }}
>
{t('Agent')}
</div>
) : null}
</div> </div>
); );
} }

View file

@ -24,7 +24,7 @@ function ModuleCard(props: Props) {
<Switch <Switch
size="small" size="small"
checked={!module.isEnabled} checked={!module.isEnabled}
title={!module.isEnabled ? 'Enabled' : 'Disabled'} title={module.isEnabled ? 'Enabled' : 'Disabled'}
onChange={() => props.onToggle(module)} onChange={() => props.onToggle(module)}
/> />
</div> </div>

View file

@ -40,12 +40,11 @@ function Modules() {
}; };
useEffect(() => { useEffect(() => {
const moduleList = list(t) list(t).forEach((module) => {
moduleList.forEach((module) => {
module.isEnabled = modules.includes(module.key); module.isEnabled = modules.includes(module.key);
}); });
setModulesState( setModulesState(
moduleList.filter( list(t).filter(
(module) => !module.hidden && (!module.enterprise || isEnterprise), (module) => !module.hidden && (!module.enterprise || isEnterprise),
), ),
); );

View file

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

View file

@ -6,7 +6,6 @@ import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList'
import { useModal } from 'Components/ModalContext'; import { useModal } from 'Components/ModalContext';
import Widget from '@/mstore/types/widget'; import Widget from '@/mstore/types/widget';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FilterKey } from 'Types/filter/filterType';
interface Props { interface Props {
metric?: any; metric?: any;
@ -36,20 +35,20 @@ function SessionsBy(props: Props) {
...filtersMap[metric.metricOf], ...filtersMap[metric.metricOf],
value: [data.name], value: [data.name],
type: filtersMap[metric.metricOf].key, type: filtersMap[metric.metricOf].key,
filters: [], filters: filtersMap[metric.metricOf].filters?.map((f: any) => {
const {
key,
operatorOptions,
category,
icon,
label,
options,
...cleaned
} = f;
return { ...cleaned, type: f.key, value: [] };
}),
}; };
if (metric.metricOf === FilterKey.FETCH) {
baseFilter.filters = [
{
key: FilterKey.FETCH_URL,
operator: 'is',
value: [data.name],
type: FilterKey.FETCH_URL,
}
];
}
const { const {
key, key,
operatorOptions, operatorOptions,

View file

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

View file

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

View file

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

View file

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

View file

@ -68,7 +68,7 @@ function MetricsList({
}, [metricStore]); }, [metricStore]);
const isFiltered = metricStore.filter.query !== '' || metricStore.filter.type !== ''; const isFiltered = metricStore.filter.query !== '' || metricStore.filter.type !== 'all';
const searchImageDimensions = { width: 60, height: 'auto' }; const searchImageDimensions = { width: 60, height: 'auto' };
const defaultImageDimensions = { width: 600, height: 'auto' }; const defaultImageDimensions = { width: 600, height: 'auto' };

View file

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

View file

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

View file

@ -1,395 +1,376 @@
import React, {useEffect, useState} from 'react'; import React, { useEffect, useState } from 'react';
import {NoContent, Loader, Pagination} from 'UI'; import { NoContent, Loader, Pagination } from 'UI';
import {Button, Tag, Tooltip, Dropdown, message} from 'antd'; import { Button, Tag, Tooltip, Dropdown, message } from 'antd';
import {UndoOutlined, DownOutlined} from '@ant-design/icons'; import { UndoOutlined, DownOutlined } from '@ant-design/icons';
import cn from 'classnames'; import cn from 'classnames';
import {useStore} from 'App/mstore'; import { useStore } from 'App/mstore';
import SessionItem from 'Shared/SessionItem'; import SessionItem from 'Shared/SessionItem';
import {observer} from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import {DateTime} from 'luxon'; import { DateTime } from 'luxon';
import {debounce, numberWithCommas} from 'App/utils'; import { debounce, numberWithCommas } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted'; import useIsMounted from 'App/hooks/useIsMounted';
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import {HEATMAP, USER_PATH, FUNNEL} from 'App/constants/card'; import { HEATMAP, USER_PATH, FUNNEL } from 'App/constants/card';
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface Props { interface Props {
className?: string; className?: string;
} }
function WidgetSessions(props: Props) { function WidgetSessions(props: Props) {
const {t} = useTranslation(); const { t } = useTranslation();
const listRef = React.useRef<HTMLDivElement>(null); const listRef = React.useRef<HTMLDivElement>(null);
const {className = ''} = props; const { className = '' } = props;
const [activeSeries, setActiveSeries] = useState('all'); const [activeSeries, setActiveSeries] = useState('all');
const [data, setData] = useState<any>([]); const [data, setData] = useState<any>([]);
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// all filtering done through series now // all filtering done through series now
const filteredSessions = getListSessionsBySeries(data, 'all'); const filteredSessions = getListSessionsBySeries(data, 'all');
const {dashboardStore, metricStore, sessionStore, customFieldStore} = const { dashboardStore, metricStore, sessionStore, customFieldStore } =
useStore(); useStore();
const focusedSeries = metricStore.focusedSeriesName; const focusedSeries = metricStore.focusedSeriesName;
const filter = dashboardStore.drillDownFilter; const filter = dashboardStore.drillDownFilter;
const widget = metricStore.instance; const widget = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat( const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat(
'LLL dd, yyyy HH:mm', 'LLL dd, yyyy HH:mm',
); );
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat( const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat(
'LLL dd, yyyy HH:mm', 'LLL dd, yyyy HH:mm',
); );
const [seriesOptions, setSeriesOptions] = useState([ const [seriesOptions, setSeriesOptions] = useState([
{label: t('All'), value: 'all'}, { label: t('All'), value: 'all' },
]); ]);
const hasFilters = const hasFilters =
filter.filters.length > 0 || filter.filters.length > 0 ||
filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.startTimestamp !== dashboardStore.drillDownPeriod.start ||
filter.endTimestamp !== dashboardStore.drillDownPeriod.end; filter.endTimestamp !== dashboardStore.drillDownPeriod.end;
const filterText = filter.filters.length > 0 ? filter.filters[0].value : ''; const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
const metaList = customFieldStore.list.map((i: any) => i.key); const metaList = customFieldStore.list.map((i: any) => i.key);
const seriesDropdownItems = seriesOptions.map((option) => ({ const seriesDropdownItems = seriesOptions.map((option) => ({
key: option.value, key: option.value,
label: ( label: (
<div onClick={() => setActiveSeries(option.value)}>{option.label}</div> <div onClick={() => setActiveSeries(option.value)}>{option.label}</div>
), ),
}));
useEffect(() => {
if (!widget.series) return;
const seriesOptions = widget.series.map((item: any) => ({
label: item.name,
value: item.seriesId ?? item.name,
})); }));
setSeriesOptions([{ label: t('All'), value: 'all' }, ...seriesOptions]);
}, [widget.series.length]);
useEffect(() => { const fetchSessions = (metricId: any, filter: any) => {
if (!widget.series) return; if (!isMounted()) return;
const seriesOptions = widget.series.map((item: any) => ({ setLoading(true);
label: item.name, delete filter.eventsOrderSupport;
value: item.seriesId ?? item.name, if (widget.metricType === FUNNEL) {
})); if (filter.series[0].filter.filters.length === 0) {
setSeriesOptions([{label: t('All'), value: 'all'}, ...seriesOptions]); setLoading(false);
}, [widget.series.length]); return setData([]);
}
}
const fetchSessions = (metricId: any, filter: any) => { widget
if (!isMounted()) return; .fetchSessions(metricId, filter)
.then((res: any) => {
if (widget.metricType === FUNNEL) { setData(res);
if (filter.series[0].filter.filters.length === 0) { if (metricStore.drillDown) {
setLoading(false); setTimeout(() => {
return setData([]); message.info(t('Sessions Refreshed!'));
} listRef.current?.scrollIntoView({ behavior: 'smooth' });
metricStore.setDrillDown(false);
}, 0);
} }
})
.finally(() => {
setLoading(false);
});
};
const fetchClickmapSessions = (customFilters: Record<string, any>) => {
sessionStore.getSessions(customFilters).then((data) => {
setData([{ ...data, seriesId: 1, seriesName: 'Clicks' }]);
});
};
const debounceRequest: any = React.useCallback(
debounce(fetchSessions, 1000),
[],
);
const debounceClickMapSearch = React.useCallback(
debounce(fetchClickmapSessions, 1000),
[],
);
const depsString = JSON.stringify(widget.series);
setLoading(true); const loadData = () => {
const filterCopy = {...filter}; if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
delete filterCopy.eventsOrderSupport; const clickFilter = {
value: [metricStore.clickMapSearch],
try { type: 'CLICK',
// Handle filters properly with null checks operator: 'onSelector',
if (filterCopy.filters && filterCopy.filters.length > 0) { isEvent: true,
// Ensure the nested path exists before pushing // @ts-ignore
if (filterCopy.series?.[0]?.filter) { filters: [],
if (!filterCopy.series[0].filter.filters) { };
filterCopy.series[0].filter.filters = []; const timeRange = {
} rangeValue: dashboardStore.drillDownPeriod.rangeValue,
filterCopy.series[0].filter.filters.push(...filterCopy.filters); startDate: dashboardStore.drillDownPeriod.start,
} endDate: dashboardStore.drillDownPeriod.end,
filterCopy.filters = []; };
} const customFilter = {
} catch (e) { ...filter,
// do nothing ...timeRange,
filters: [...sessionStore.userFilter.filters, clickFilter],
};
debounceClickMapSearch(customFilter);
} else {
const hasStartPoint =
!!widget.startPoint && widget.metricType === USER_PATH;
const onlyFocused = focusedSeries
? widget.series.filter((s) => s.name === focusedSeries)
: widget.series;
const activeSeries = metricStore.disabledSeries.length
? onlyFocused.filter(
(s) => !metricStore.disabledSeries.includes(s.name),
)
: onlyFocused;
const seriesJson = activeSeries.map((s) => s.toJson());
if (hasStartPoint) {
seriesJson[0].filter.filters.push(widget.startPoint.toJson());
}
if (widget.metricType === USER_PATH) {
if (
seriesJson[0].filter.filters[0].value[0] === '' &&
widget.data.nodes
) {
seriesJson[0].filter.filters[0].value = widget.data.nodes[0].name;
} else if (
seriesJson[0].filter.filters[0].value[0] === '' &&
!widget.data.nodes?.length
) {
// no point requesting if we don't have starting point picked by api
return;
} }
widget }
.fetchSessions(metricId, filterCopy) debounceRequest(widget.metricId, {
.then((res: any) => { ...filter,
setData(res); series: seriesJson,
if (metricStore.drillDown) { page: metricStore.sessionsPage,
setTimeout(() => { limit: metricStore.sessionsPageSize,
message.info(t('Sessions Refreshed!')); });
listRef.current?.scrollIntoView({behavior: 'smooth'}); }
metricStore.setDrillDown(false); };
}, 0); useEffect(() => {
} metricStore.updateKey('sessionsPage', 1);
}) loadData();
.finally(() => { }, [
setLoading(false); filter.startTimestamp,
}); filter.endTimestamp,
}; filter.filters,
const fetchClickmapSessions = (customFilters: Record<string, any>) => { depsString,
sessionStore.getSessions(customFilters).then((data) => { metricStore.clickMapSearch,
setData([{...data, seriesId: 1, seriesName: 'Clicks'}]); focusedSeries,
}); widget.startPoint,
}; widget.data.nodes,
const debounceRequest: any = React.useCallback( metricStore.disabledSeries.length,
debounce(fetchSessions, 1000), ]);
[], useEffect(loadData, [metricStore.sessionsPage]);
); useEffect(() => {
const debounceClickMapSearch = React.useCallback( if (activeSeries === 'all') {
debounce(fetchClickmapSessions, 1000), metricStore.setFocusedSeriesName(null);
[], } else {
); metricStore.setFocusedSeriesName(
seriesOptions.find((option) => option.value === activeSeries)?.label,
false,
);
}
}, [activeSeries]);
useEffect(() => {
if (focusedSeries) {
setActiveSeries(
seriesOptions.find((option) => option.label === focusedSeries)?.value ||
'all',
);
} else {
setActiveSeries('all');
}
}, [focusedSeries]);
const depsString = JSON.stringify(widget.series); const clearFilters = () => {
metricStore.updateKey('sessionsPage', 1);
dashboardStore.resetDrillDownFilter();
};
const loadData = () => { return (
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) { <div
const clickFilter = { className={cn(
value: [metricStore.clickMapSearch], className,
type: 'CLICK', 'bg-white p-3 pb-0 rounded-xl shadow-sm border mt-3',
operator: 'onSelector', )}
isEvent: true, >
// @ts-ignore <div className="flex items-center justify-between">
filters: [], <div>
}; <div className="flex items-baseline gap-2">
const timeRange = { <h2 className="text-xl">
rangeValue: dashboardStore.drillDownPeriod.rangeValue, {metricStore.clickMapSearch ? t('Clicks') : t('Sessions')}
startDate: dashboardStore.drillDownPeriod.start, </h2>
endDate: dashboardStore.drillDownPeriod.end, <div className="ml-2 color-gray-medium">
}; {metricStore.clickMapLabel
const customFilter = { ? `on "${metricStore.clickMapLabel}" `
...filter, : null}
...timeRange, {t('between')}{' '}
filters: [...sessionStore.userFilter.filters, clickFilter], <span className="font-medium color-gray-darkest">
};
debounceClickMapSearch(customFilter);
} else {
const hasStartPoint =
!!widget.startPoint && widget.metricType === USER_PATH;
const onlyFocused = focusedSeries
? widget.series.filter((s) => s.name === focusedSeries)
: widget.series;
const activeSeries = metricStore.disabledSeries.length
? onlyFocused.filter(
(s) => !metricStore.disabledSeries.includes(s.name),
)
: onlyFocused;
const seriesJson = activeSeries.map((s) => s.toJson());
if (hasStartPoint) {
seriesJson[0].filter.filters.push(widget.startPoint.toJson());
}
if (widget.metricType === USER_PATH) {
if (
seriesJson[0].filter.filters[0].value[0] === '' &&
widget.data.nodes?.length
) {
seriesJson[0].filter.filters[0].value = widget.data.nodes[0].name;
} else if (
seriesJson[0].filter.filters[0].value[0] === '' &&
!widget.data.nodes?.length
) {
// no point requesting if we don't have starting point picked by api
return;
}
}
debounceRequest(widget.metricId, {
...filter,
series: seriesJson,
page: metricStore.sessionsPage,
limit: metricStore.sessionsPageSize,
});
}
};
useEffect(() => {
metricStore.updateKey('sessionsPage', 1);
loadData();
}, [
filter.startTimestamp,
filter.endTimestamp,
filter.filters,
depsString,
metricStore.clickMapSearch,
focusedSeries,
widget.startPoint,
widget.data.nodes,
metricStore.disabledSeries.length,
]);
useEffect(loadData, [metricStore.sessionsPage]);
useEffect(() => {
if (activeSeries === 'all') {
metricStore.setFocusedSeriesName(null);
} else {
metricStore.setFocusedSeriesName(
seriesOptions.find((option) => option.value === activeSeries)?.label,
false,
);
}
}, [activeSeries]);
useEffect(() => {
if (focusedSeries) {
setActiveSeries(
seriesOptions.find((option) => option.label === focusedSeries)?.value ||
'all',
);
} else {
setActiveSeries('all');
}
}, [focusedSeries]);
const clearFilters = () => {
metricStore.updateKey('sessionsPage', 1);
dashboardStore.resetDrillDownFilter();
};
return (
<div
className={cn(
className,
'bg-white p-3 pb-0 rounded-xl shadow-sm border mt-3',
)}
>
<div className="flex items-center justify-between">
<div>
<div className="flex items-baseline gap-2">
<h2 className="text-xl">
{metricStore.clickMapSearch ? t('Clicks') : t('Sessions')}
</h2>
<div className="ml-2 color-gray-medium">
{metricStore.clickMapLabel
? `on "${metricStore.clickMapLabel}" `
: null}
{t('between')}{' '}
<span className="font-medium color-gray-darkest">
{startTime} {startTime}
</span>{' '} </span>{' '}
{t('and')}{' '} {t('and')}{' '}
<span className="font-medium color-gray-darkest"> <span className="font-medium color-gray-darkest">
{endTime} {endTime}
</span>{' '} </span>{' '}
</div> </div>
{hasFilters && ( {hasFilters && (
<Tooltip title={t('Clear Drilldown')} placement="top"> <Tooltip title={t('Clear Drilldown')} placement="top">
<Button type="text" size="small" onClick={clearFilters}> <Button type="text" size="small" onClick={clearFilters}>
<UndoOutlined/> <UndoOutlined />
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
</div> </div>
{hasFilters && widget.metricType === 'table' && ( {hasFilters && widget.metricType === 'table' && (
<div className="py-2"> <div className="py-2">
<Tag <Tag
closable closable
onClose={clearFilters} onClose={clearFilters}
className="truncate max-w-44 rounded-lg" className="truncate max-w-44 rounded-lg"
> >
{filterText} {filterText}
</Tag> </Tag>
</div> </div>
)} )}
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && ( {widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
<div className="flex items-center ml-6"> <div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium"> <span className="mr-2 color-gray-medium">
{t('Filter by Series')} {t('Filter by Series')}
</span> </span>
<Dropdown <Dropdown
menu={{ menu={{
items: seriesDropdownItems, items: seriesDropdownItems,
selectable: true, selectable: true,
selectedKeys: [activeSeries], selectedKeys: [activeSeries],
}} }}
trigger={['click']} trigger={['click']}
> >
<Button type="text" size="small"> <Button type="text" size="small">
{seriesOptions.find((option) => option.value === activeSeries) {seriesOptions.find((option) => option.value === activeSeries)
?.label || t('Select Series')} ?.label || t('Select Series')}
<DownOutlined/> <DownOutlined />
</Button> </Button>
</Dropdown> </Dropdown>
</div>
)}
</div>
</div> </div>
)}
</div>
</div>
<div className="mt-3"> <div className="mt-3">
<Loader loading={loading}> <Loader loading={loading}>
<NoContent <NoContent
title={ title={
<div className="flex items-center justify-center flex-col"> <div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_SESSIONS} size={60}/> <AnimatedSVG name={ICONS.NO_SESSIONS} size={60} />
<div className="mt-4"/> <div className="mt-4" />
<div className="text-center"> <div className="text-center">
{t('No relevant sessions found for the selected time period')} {t('No relevant sessions found for the selected time period')}
</div> </div>
</div> </div>
} }
show={filteredSessions.sessions.length === 0} show={filteredSessions.sessions.length === 0}
> >
{filteredSessions.sessions.map((session: any) => ( {filteredSessions.sessions.map((session: any) => (
<React.Fragment key={session.sessionId}> <React.Fragment key={session.sessionId}>
<SessionItem <SessionItem
disableUser disableUser
session={session} session={session}
metaList={metaList} metaList={metaList}
/> />
<div className="border-b"/> <div className="border-b" />
</React.Fragment> </React.Fragment>
))} ))}
<div <div
className="flex items-center justify-between p-5" className="flex items-center justify-between p-5"
ref={listRef} ref={listRef}
> >
<div> <div>
{t('Showing')}{' '} {t('Showing')}{' '}
<span className="font-medium"> <span className="font-medium">
{(metricStore.sessionsPage - 1) * {(metricStore.sessionsPage - 1) *
metricStore.sessionsPageSize + metricStore.sessionsPageSize +
1} 1}
</span>{' '} </span>{' '}
{t('to')}{' '} {t('to')}{' '}
<span className="font-medium"> <span className="font-medium">
{(metricStore.sessionsPage - 1) * {(metricStore.sessionsPage - 1) *
metricStore.sessionsPageSize + metricStore.sessionsPageSize +
filteredSessions.sessions.length} filteredSessions.sessions.length}
</span>{' '} </span>{' '}
{t('of')}{' '} {t('of')}{' '}
<span className="font-medium"> <span className="font-medium">
{numberWithCommas(filteredSessions.total)} {numberWithCommas(filteredSessions.total)}
</span>{' '} </span>{' '}
{t('sessions.')} {t('sessions.')}
</div> </div>
<Pagination <Pagination
page={metricStore.sessionsPage} page={metricStore.sessionsPage}
total={filteredSessions.total} total={filteredSessions.total}
onPageChange={(page: any) => onPageChange={(page: any) =>
metricStore.updateKey('sessionsPage', page) metricStore.updateKey('sessionsPage', page)
} }
limit={metricStore.sessionsPageSize} limit={metricStore.sessionsPageSize}
debounceRequest={500} debounceRequest={500}
/> />
</div>
</NoContent>
</Loader>
</div> </div>
</div> </NoContent>
); </Loader>
</div>
</div>
);
} }
const getListSessionsBySeries = (data: any, seriesId: any) => { const getListSessionsBySeries = (data: any, seriesId: any) => {
const arr = data.reduce( const arr = data.reduce(
(arr: any, element: any) => { (arr: any, element: any) => {
if (seriesId === 'all') { if (seriesId === 'all') {
const sessionIds = arr.sessions.map((i: any) => i.sessionId); const sessionIds = arr.sessions.map((i: any) => i.sessionId);
const sessions = element.sessions.filter( const sessions = element.sessions.filter(
(i: any) => !sessionIds.includes(i.sessionId), (i: any) => !sessionIds.includes(i.sessionId),
); );
arr.sessions.push(...sessions); arr.sessions.push(...sessions);
} else if (element.seriesId === seriesId) { } else if (element.seriesId === seriesId) {
const sessionIds = arr.sessions.map((i: any) => i.sessionId); const sessionIds = arr.sessions.map((i: any) => i.sessionId);
const sessions = element.sessions.filter( const sessions = element.sessions.filter(
(i: any) => !sessionIds.includes(i.sessionId), (i: any) => !sessionIds.includes(i.sessionId),
); );
const duplicates = element.sessions.length - sessions.length; const duplicates = element.sessions.length - sessions.length;
arr.sessions.push(...sessions); arr.sessions.push(...sessions);
arr.total = element.total - duplicates; arr.total = element.total - duplicates;
} }
return arr; return arr;
}, },
{sessions: []}, { sessions: [] },
); );
arr.total = arr.total =
seriesId === 'all' seriesId === 'all'
? Math.max(...data.map((i: any) => i.total)) ? Math.max(...data.map((i: any) => i.total))
: data.find((i: any) => i.seriesId === seriesId).total; : data.find((i: any) => i.seriesId === seriesId).total;
return arr; return arr;
}; };
export default observer(WidgetSessions); export default observer(WidgetSessions);

View file

@ -92,9 +92,6 @@ function WidgetView({
filter: { filters: selectedCard.filters }, filter: { filters: selectedCard.filters },
}), }),
]; ];
} else if (selectedCard.cardType === TABLE) {
cardData.series = [new FilterSeries()];
cardData.series[0].filter.eventsOrder = 'and';
} }
if (selectedCard.cardType === FUNNEL) { if (selectedCard.cardType === FUNNEL) {
cardData.series = [new FilterSeries()]; cardData.series = [new FilterSeries()];

View file

@ -83,7 +83,6 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
}); });
const onChartClick = () => { const onChartClick = () => {
dashboardStore.setDrillDownPeriod(dashboardStore.period);
// if (!isWidget || isPredefined) return; // if (!isWidget || isPredefined) return;
props.history.push( props.history.push(
withSiteId( withSiteId(

View file

@ -14,7 +14,7 @@ interface SSOLoginProps {
const SSOLogin = ({ authDetails, enforceSSO = false }: SSOLoginProps) => { const SSOLogin = ({ authDetails, enforceSSO = false }: SSOLoginProps) => {
const { userStore } = useStore(); const { userStore } = useStore();
const { t } = useTranslation(); const { t } = useTranslation();
const { isSSOSupported } = userStore; const { isEnterprise } = userStore;
const getSSOLink = () => const getSSOLink = () =>
window !== window.top window !== window.top
@ -23,7 +23,7 @@ const SSOLogin = ({ authDetails, enforceSSO = false }: SSOLoginProps) => {
const ssoLink = getSSOLink(); const ssoLink = getSSOLink();
const ssoButtonText = `${t('Login with SSO')} ${authDetails.ssoProvider ? `(${authDetails.ssoProvider})` : '' const ssoButtonText = `${t('Login with SSO')} ${authDetails.ssoProvider ? `(${authDetails.ssoProvider})` : ''
}`; }`;
if (enforceSSO) { if (enforceSSO) {
return ( return (
@ -47,7 +47,7 @@ const SSOLogin = ({ authDetails, enforceSSO = false }: SSOLoginProps) => {
<Tooltip <Tooltip
title={ title={
<div className="text-center"> <div className="text-center">
{isSSOSupported ? ( {isEnterprise ? (
<span> <span>
{t('SSO has not been configured.')} {t('SSO has not been configured.')}
<br /> <br />

View file

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import { Redirect, Route, RouteComponentProps, Switch } from 'react-router'; import { Redirect, Route, RouteComponentProps, Switch } from 'react-router';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { OB_TABS, onboarding as onboardingRoute, withSiteId } from 'App/routes'; import { OB_TABS, onboarding as onboardingRoute, withSiteId } from 'App/routes';
import { Icon } from 'UI';
import IdentifyUsersTab from './components/IdentifyUsersTab'; import IdentifyUsersTab from './components/IdentifyUsersTab';
import InstallOpenReplayTab from './components/InstallOpenReplayTab'; import InstallOpenReplayTab from './components/InstallOpenReplayTab';
import IntegrationsTab from './components/IntegrationsTab'; import IntegrationsTab from './components/IntegrationsTab';
import ManageUsersTab from './components/ManageUsersTab'; import ManageUsersTab from './components/ManageUsersTab';
import SideMenu from './components/SideMenu'; import SideMenu from './components/SideMenu';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Smartphone, AppWindow } from 'lucide-react';
interface Props { interface Props {
match: { match: {
@ -31,7 +33,7 @@ function Onboarding(props: Props) {
{ {
label: ( label: (
<div className="font-semibold flex gap-2 items-center"> <div className="font-semibold flex gap-2 items-center">
<AppWindow size={16} /> <Icon name="browser/browser" size={16} />
&nbsp;{t('Web')} &nbsp;{t('Web')}
</div> </div>
), ),
@ -40,7 +42,7 @@ function Onboarding(props: Props) {
{ {
label: ( label: (
<div className="font-semibold flex gap-2 items-center"> <div className="font-semibold flex gap-2 items-center">
<Smartphone size={16} /> <Icon name="mobile" size={16} />
&nbsp;{t('Mobile')} &nbsp;{t('Mobile')}
</div> </div>
), ),

View file

@ -130,20 +130,18 @@ function IdentifyUsersTab(props: Props) {
'To identify users through metadata, you will have to explicitly specify your user metadata so it can be injected during sessions. Follow the below steps', 'To identify users through metadata, you will have to explicitly specify your user metadata so it can be injected during sessions. Follow the below steps',
)} )}
</p> </p>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-start">
<CircleNumber text="1" /> <CircleNumber text="1" />
<MetadataList /> <MetadataList />
</div> </div>
<div className="my-6" /> <div className="my-6" />
<div className="flex items-start"> <div className="flex items-start">
<div> <CircleNumber text="2" />
<CircleNumber text="2" /> <div className="pt-1 w-full">
<span className="font-bold"> <span className="font-bold">
{t('Inject metadata when recording sessions')} {t('Inject metadata when recording sessions')}
</span> </span>
</div>
<div className="pt-1 w-full">
<div className="my-2"> <div className="my-2">
{t('Use the')}&nbsp; {t('Use the')}&nbsp;
<span className="highlight-blue">setMetadata</span>{' '} <span className="highlight-blue">setMetadata</span>{' '}

View file

@ -8,7 +8,6 @@ import MobileOnboardingTabs from '../OnboardingTabs/OnboardingMobileTabs';
import ProjectFormButton from '../ProjectFormButton'; import ProjectFormButton from '../ProjectFormButton';
import withOnboarding, { WithOnboardingProps } from '../withOnboarding'; import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CircleHelp } from 'lucide-react'
interface Props extends WithOnboardingProps { interface Props extends WithOnboardingProps {
platforms: Array<{ platforms: Array<{
@ -46,8 +45,8 @@ function InstallOpenReplayTab(props: Props) {
</div> </div>
<a href={"https://docs.openreplay.com/en/sdk/using-or/"} target="_blank"> <a href={"https://docs.openreplay.com/en/sdk/using-or/"} target="_blank">
<Button size={"small"} type={"text"} className="ml-2 flex items-center gap-2"> <Button size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
<CircleHelp size={14} /> <Icon name={"question-circle"} />
<div>{t('See Documentation')}</div> <div className={"text-main"}>{t('See Documentation')}</div>
</Button> </Button>
</a> </a>
</h1> </h1>

View file

@ -55,14 +55,16 @@ function MetadataList() {
<Button type="default" onClick={() => openModal()}> <Button type="default" onClick={() => openModal()}>
{t('Add Metadata')} {t('Add Metadata')}
</Button> </Button>
{fields.map((f, index) => ( <div className="flex ml-2">
<TagBadge {fields.map((f, index) => (
key={index} <TagBadge
text={f.key} key={index}
onRemove={() => removeMetadata(f)} text={f.key}
outline onRemove={() => removeMetadata(f)}
/> outline
))} />
))}
</div>
</div> </div>
); );
} }

View file

@ -1,32 +0,0 @@
import React from 'react'
import DocCard from "App/components/shared/DocCard";
import { useTranslation } from 'react-i18next';
import { Mail } from 'lucide-react'
import { CopyButton } from "UI";
export function CollabCard({ showUserModal }: { showUserModal: () => void }) {
const { t } = useTranslation();
return (
<DocCard title={t('Need help from team member?')}>
<div className={'text-main cursor-pointer flex items-center gap-2'} onClick={showUserModal}>
<Mail size={14} />
<span>
{t('Invite and Collaborate')}
</span>
</div>
</DocCard>
)
}
export function ProjectKeyCard({ projectKey }: { projectKey: string }) {
const { t } = useTranslation();
return (
<DocCard title={t('Project Key')}>
<div className="p-2 rounded bg-white flex justify-between items-center">
<div className={'font-mono'}>{projectKey}</div>
<CopyButton content={projectKey} className={'capitalize font-medium text-neutral-400'} />
</div>
</DocCard>
)
}

View file

@ -4,7 +4,6 @@ import DocCard from 'Shared/DocCard/DocCard';
import { useModal } from 'App/components/Modal'; import { useModal } from 'App/components/Modal';
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm'; import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs'; import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs';
import { CollabCard, ProjectKeyCard } from "./Callouts";
import MobileInstallDocs from './InstallDocs/MobileInstallDocs'; import MobileInstallDocs from './InstallDocs/MobileInstallDocs';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -40,9 +39,18 @@ function MobileTrackingCodeModal(props: Props) {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<CollabCard showUserModal={showUserModal} /> <DocCard title={t('Need help from team member?')}>
<a className="link" onClick={showUserModal}>
{t('Invite and Collaborate')}
</a>
</DocCard>
<ProjectKeyCard projectKey={site.projectKey} /> <DocCard title={t('Project Key')}>
<div className="p-2 rounded bg-white flex justify-between items-center">
{site.projectKey}
<CopyButton content={site.projectKey} />
</div>
</DocCard>
</div> </div>
</div> </div>
); );
@ -54,9 +62,18 @@ function MobileTrackingCodeModal(props: Props) {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<CollabCard showUserModal={showUserModal} /> <DocCard title={t('Need help from team member?')}>
<a className="link" onClick={showUserModal}>
{t('Invite and Collaborate')}
</a>
</DocCard>
<ProjectKeyCard projectKey={site.projectKey} /> <DocCard title={t('Project Key')}>
<div className="p-2 rounded bg-white flex justify-between items-center">
{site.projectKey}
<CopyButton content={site.projectKey} />
</div>
</DocCard>
</div> </div>
</div> </div>
); );

View file

@ -3,7 +3,6 @@ import { Tabs, Icon, CopyButton } from 'UI';
import DocCard from 'Shared/DocCard/DocCard'; import DocCard from 'Shared/DocCard/DocCard';
import { useModal } from 'App/components/Modal'; import { useModal } from 'App/components/Modal';
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm'; import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
import { CollabCard, ProjectKeyCard } from "./Callouts";
import InstallDocs from './InstallDocs'; import InstallDocs from './InstallDocs';
import ProjectCodeSnippet from './ProjectCodeSnippet'; import ProjectCodeSnippet from './ProjectCodeSnippet';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -38,9 +37,20 @@ function TrackingCodeModal(props: Props) {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<CollabCard showUserModal={showUserModal} /> <DocCard title="Need help from team member?">
<a className="link" onClick={showUserModal}>
<ProjectKeyCard projectKey={site.projectKey} /> {t('Invite and Collaborate')}
</a>
</DocCard>
<DocCard title="Project Key">
<div className="rounded bg-white px-2 py-1 flex items-center justify-between">
<span>{site.projectKey}</span>
<CopyButton
content={site.projectKey}
className="capitalize"
/>
</div>
</DocCard>
<DocCard title="Other ways to install"> <DocCard title="Other ways to install">
<a <a
className="link flex items-center" className="link flex items-center"
@ -67,9 +77,18 @@ function TrackingCodeModal(props: Props) {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<CollabCard showUserModal={showUserModal} /> <DocCard title="Need help from team member?">
<a className="link" onClick={showUserModal}>
{t('Invite and Collaborate')}
</a>
</DocCard>
<ProjectKeyCard projectKey={site.projectKey} /> <DocCard title="Project Key">
<div className="p-2 rounded bg-white flex justify-between items-center">
{site.projectKey}
<CopyButton content={site.projectKey} />
</div>
</DocCard>
</div> </div>
</div> </div>
); );

View file

@ -41,7 +41,7 @@ function SideMenu(props: Props) {
<Menu <Menu
mode="inline" mode="inline"
onClick={handleClick} onClick={handleClick}
style={{ border: 'none' }} style={{ marginTop: '8px', border: 'none' }}
selectedKeys={activeTab ? [activeTab] : []} selectedKeys={activeTab ? [activeTab] : []}
> >
<Menu.Item <Menu.Item

View file

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

View file

@ -91,7 +91,7 @@ function PlayerBlockHeader(props: Props) {
)} )}
</div> </div>
</div> </div>
<div className="relative border-l" style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }}> <div className="relative border-l" style={{ minWidth: '270px' }}>
<Tabs <Tabs
tabs={TABS} tabs={TABS}
active={activeTab} active={activeTab}

View file

@ -61,7 +61,7 @@ function PlayerContent({
className="w-full" className="w-full"
style={ style={
activeTab && !fullscreen activeTab && !fullscreen
? { maxWidth: `calc(100% - ${activeTab === 'EXPORT' ? '360px' : '270px'})` } ? { maxWidth: 'calc(100% - 270px)' }
: undefined : undefined
} }
> >

View file

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

View file

@ -114,17 +114,19 @@ function PlayerBlockHeader(props: any) {
)} )}
{_metaList.length > 0 && ( {_metaList.length > 0 && (
<SessionMetaList <div className="h-full flex items-center px-2 gap-1">
horizontal <SessionMetaList
metaList={_metaList} className=""
maxLength={2} metaList={_metaList}
/> maxLength={2}
/>
</div>
)} )}
</div> </div>
</div> </div>
<div <div
className="px-2 relative border-l border-l-gray-lighter" className="px-2 relative border-l border-l-gray-lighter"
style={{ minWidth: activeTab === 'EXPORT' ? '360px' : '270px' }} style={{ minWidth: '270px' }}
> >
<Tabs <Tabs
tabs={TABS} tabs={TABS}

View file

@ -65,7 +65,7 @@ function PlayerContent({
className="w-full" className="w-full"
style={ style={
activeTab && !fullscreen activeTab && !fullscreen
? { maxWidth: `calc(100% - ${activeTab === 'EXPORT' ? '360px' : '270px'})` } ? { maxWidth: 'calc(100% - 270px)' }
: undefined : undefined
} }
> >

View file

@ -182,7 +182,6 @@ function Player(props: IProps) {
setActiveTab={(tab: string) => setActiveTab={(tab: string) =>
activeTab === tab ? props.setActiveTab('') : props.setActiveTab(tab) activeTab === tab ? props.setActiveTab('') : props.setActiveTab(tab)
} }
activeTab={activeTab}
speedDown={playerContext.player.speedDown} speedDown={playerContext.player.speedDown}
speedUp={playerContext.player.speedUp} speedUp={playerContext.player.speedUp}
jump={playerContext.player.jump} jump={playerContext.player.jump}

View file

@ -7,16 +7,13 @@ import { Icon } from 'UI';
function LogsButton({ function LogsButton({
integrated, integrated,
onClick, onClick,
shorten,
}: { }: {
integrated: string[]; integrated: string[];
onClick: () => void; onClick: () => void;
shorten?: boolean;
}) { }) {
return ( return (
<ControlButton <ControlButton
label={shorten ? null : "Traces"} label="Traces"
customKey="traces"
customTags={ customTags={
<Avatar.Group> <Avatar.Group>
{integrated.map((name) => ( {integrated.map((name) => (

View file

@ -38,8 +38,8 @@ function WebPlayer(props: any) {
uxtestingStore, uxtestingStore,
uiPlayerStore, uiPlayerStore,
integrationsStore, integrationsStore,
userStore,
} = useStore(); } = useStore();
const devTools = sessionStore.devTools
const session = sessionStore.current; const session = sessionStore.current;
const { prefetched } = sessionStore; const { prefetched } = sessionStore;
const startedAt = sessionStore.current.startedAt || 0; const startedAt = sessionStore.current.startedAt || 0;
@ -57,17 +57,14 @@ function WebPlayer(props: any) {
const [fullView, setFullView] = useState(false); const [fullView, setFullView] = useState(false);
React.useEffect(() => { React.useEffect(() => {
const handleActivation = () => { if (windowActive) {
if (!document.hidden) { const handleActivation = () => {
setWindowActive(true); if (!document.hidden) {
document.removeEventListener('visibilitychange', handleActivation); setWindowActive(true);
} document.removeEventListener('visibilitychange', handleActivation);
}; }
document.addEventListener('visibilitychange', handleActivation); };
document.addEventListener('visibilitychange', handleActivation);
return () => {
devTools.update('network', { activeTab: 'ALL' });
document.removeEventListener('visibilitychange', handleActivation);
} }
}, []); }, []);

View file

@ -169,6 +169,6 @@ function TabChange({ from, to, activeUrl, onClick }) {
</div> </div>
</div> </div>
); );
}; }
export default observer(EventGroupWrapper); export default observer(EventGroupWrapper);

View file

@ -4,17 +4,17 @@ import cn from 'classnames';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import React from 'react'; import React from 'react';
import { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from 'virtua';
import { Button } from 'antd'; import { Button } from 'antd'
import { PlayerContext } from 'App/components/Session/playerContext'; import { PlayerContext } from 'App/components/Session/playerContext';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { Icon } from 'UI'; import { Icon } from 'UI';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react'
import EventGroupWrapper from './EventGroupWrapper'; import EventGroupWrapper from './EventGroupWrapper';
import EventSearch from './EventSearch/EventSearch'; import EventSearch from './EventSearch/EventSearch';
import styles from './eventsBlock.module.css'; import styles from './eventsBlock.module.css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CloseOutlined } from "@ant-design/icons"; import { CloseOutlined } from ".store/@ant-design-icons-virtual-42686020c5/package";
import { Tooltip } from "antd"; import { Tooltip } from ".store/antd-virtual-9dbfadb7f6/package";
import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal"; import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal";
interface IProps { interface IProps {
@ -25,7 +25,7 @@ const MODES = {
SELECT: 'select', SELECT: 'select',
SEARCH: 'search', SEARCH: 'search',
EXPORT: 'export', EXPORT: 'export',
}; }
function EventsBlock(props: IProps) { function EventsBlock(props: IProps) {
const defaultFramework = getDefaultFramework(); const defaultFramework = getDefaultFramework();
@ -95,7 +95,7 @@ function EventsBlock(props: IProps) {
? e.time >= zoomStartTs && e.time <= zoomEndTs ? e.time >= zoomStartTs && e.time <= zoomEndTs
: false : false
: true, : true,
); );
}, [ }, [
filteredLength, filteredLength,
notesWithEvtsLength, notesWithEvtsLength,
@ -126,7 +126,6 @@ function EventsBlock(props: IProps) {
}, },
[usedEvents, time, endTime], [usedEvents, time, endTime],
); );
const currentTimeEventIndex = findLastFitting(time); const currentTimeEventIndex = findLastFitting(time);
const write = ({ const write = ({
@ -183,7 +182,6 @@ function EventsBlock(props: IProps) {
const isTabChange = 'type' in event && event.type === 'TABCHANGE'; const isTabChange = 'type' in event && event.type === 'TABCHANGE';
const isCurrent = index === currentTimeEventIndex; const isCurrent = index === currentTimeEventIndex;
const isPrev = index < currentTimeEventIndex; const isPrev = index < currentTimeEventIndex;
return ( return (
<EventGroupWrapper <EventGroupWrapper
query={query} query={query}
@ -251,14 +249,12 @@ function EventsBlock(props: IProps) {
onClick={() => setMode(MODES.SEARCH)} onClick={() => setMode(MODES.SEARCH)}
> >
<Search size={14} /> <Search size={14} />
<div> <div>{t('Search')}&nbsp;{usedEvents.length}&nbsp;{t('events')}</div>
{t('Search')}&nbsp;{usedEvents.length}&nbsp;{t('events')}
</div>
</Button> </Button>
<Tooltip title={t('Close Panel')} placement="bottom"> <Tooltip title={t('Close Panel')} placement='bottom' >
<Button <Button
className="ml-auto" className="ml-auto"
type="text" type='text'
onClick={() => { onClick={() => {
setActiveTab(''); setActiveTab('');
}} }}
@ -267,23 +263,19 @@ function EventsBlock(props: IProps) {
</Tooltip> </Tooltip>
</div> </div>
) : null} ) : null}
{mode === MODES.SEARCH ? ( {mode === MODES.SEARCH ?
<div className={'flex items-center gap-2'}> <div className={'flex items-center gap-2'}>
<EventSearch <EventSearch
onChange={write} onChange={write}
setActiveTab={setActiveTab} setActiveTab={setActiveTab}
value={query} value={query}
eventsText={ eventsText={
usedEvents.length usedEvents.length ? `${usedEvents.length} ${t('Events')}` : `0 ${t('Events')}`
? `${usedEvents.length} ${t('Events')}`
: `0 ${t('Events')}`
} }
/> />
<Button type={'text'} onClick={() => setMode(MODES.SELECT)}> <Button type={'text'} onClick={() => setMode(MODES.SELECT)}>{t('Cancel')}</Button>
{t('Cancel')}
</Button>
</div> </div>
) : null} : null}
</div> </div>
<div <div
className={cn('flex-1 pb-4', styles.eventsList)} className={cn('flex-1 pb-4', styles.eventsList)}

View file

@ -65,6 +65,7 @@ function GraphQL({ panelHeight }: { panelHeight: number }) {
const filterList = (list: any, value: string) => { const filterList = (list: any, value: string) => {
const filterRE = getRE(value, 'i'); const filterRE = getRE(value, 'i');
const { t } = useTranslation();
return value return value
? list.filter( ? list.filter(

View file

@ -4,7 +4,7 @@ import { Popover, Button } from 'antd';
import stl from './controlButton.module.css'; import stl from './controlButton.module.css';
interface IProps { interface IProps {
label: React.ReactNode; label: string;
icon?: string; icon?: string;
disabled?: boolean; disabled?: boolean;
onClick?: () => void; onClick?: () => void;
@ -18,7 +18,6 @@ interface IProps {
noIcon?: boolean; noIcon?: boolean;
popover?: React.ReactNode; popover?: React.ReactNode;
customTags?: React.ReactNode; customTags?: React.ReactNode;
customKey?: string;
} }
function ControlButton({ function ControlButton({
@ -29,28 +28,29 @@ function ControlButton({
active = false, active = false,
popover = undefined, popover = undefined,
customTags, customTags,
customKey,
}: IProps) { }: IProps) {
return ( return (
<Popover content={popover} open={popover ? undefined : false}> <Popover content={popover} open={popover ? undefined : false}>
<Button <Button
size="small" size="small"
onClick={onClick} onClick={onClick}
id={`control-button-${customKey ? customKey.toLowerCase() : label!.toString().toLowerCase()}`} id={`control-button-${label.toLowerCase()}`}
disabled={disabled} disabled={disabled}
> >
{customTags} {customTags}
{hasErrors && ( {hasErrors && (
<div className="w-2 h-2 rounded-full bg-red" /> <div className={stl.labels}>
<div className={stl.errorSymbol} />
</div>
)} )}
{label && <span <span
className={cn( className={cn(
'font-semibold hover:text-main', 'font-semibold hover:text-main',
active ? 'color-main' : 'color-gray-darkest', active ? 'color-main' : 'color-gray-darkest',
)} )}
> >
{label} {label}
</span>} </span>
</Button> </Button>
</Popover> </Popover>
); );

View file

@ -32,8 +32,6 @@ import {
} from 'App/mstore/uiPlayerStore'; } from 'App/mstore/uiPlayerStore';
import { Icon } from 'UI'; import { Icon } from 'UI';
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton'; import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
import { CodeOutlined, DashboardOutlined, ClusterOutlined } from '@ant-design/icons';
import { ArrowDownUp, ListCollapse, Merge, Waypoints } from 'lucide-react'
import ControlButton from './ControlButton'; import ControlButton from './ControlButton';
import Timeline from './Timeline'; import Timeline from './Timeline';
@ -54,23 +52,23 @@ export const SKIP_INTERVALS = {
function getStorageName(type: any) { function getStorageName(type: any) {
switch (type) { switch (type) {
case STORAGE_TYPES.REDUX: case STORAGE_TYPES.REDUX:
return { name: 'Redux', icon: <Icon name='integrations/redux' size={14} /> }; return 'Redux';
case STORAGE_TYPES.MOBX: case STORAGE_TYPES.MOBX:
return { name: 'Mobx', icon: <Icon name='integrations/mobx' size={14} /> }; return 'Mobx';
case STORAGE_TYPES.VUEX: case STORAGE_TYPES.VUEX:
return { name: 'Vuex', icon: <Icon name='integrations/vuejs' size={14} /> }; return 'Vuex';
case STORAGE_TYPES.NGRX: case STORAGE_TYPES.NGRX:
return { name: 'NgRx', icon: <Icon name='integrations/ngrx' size={14} /> }; return 'NgRx';
case STORAGE_TYPES.ZUSTAND: case STORAGE_TYPES.ZUSTAND:
return { name: 'Zustand', icon: <Icon name='integrations/zustand' size={14} /> }; return 'Zustand';
case STORAGE_TYPES.NONE: case STORAGE_TYPES.NONE:
return { name: 'State', icon: <ClusterOutlined size={14} /> }; return 'State';
default: default:
return { name: 'State', icon: <ClusterOutlined size={14} /> }; return 'State';
} }
} }
function Controls({ setActiveTab, activeTab }: any) { function Controls({ setActiveTab }: any) {
const { player, store } = React.useContext(PlayerContext); const { player, store } = React.useContext(PlayerContext);
const { const {
uxtestingStore, uxtestingStore,
@ -193,7 +191,6 @@ function Controls({ setActiveTab, activeTab }: any) {
bottomBlock={bottomBlock} bottomBlock={bottomBlock}
disabled={disabled} disabled={disabled}
events={events} events={events}
activeTab={activeTab}
/> />
)} )}
@ -215,7 +212,6 @@ interface IDevtoolsButtons {
bottomBlock: number; bottomBlock: number;
disabled: boolean; disabled: boolean;
events: any[]; events: any[];
activeTab?: string;
} }
const DevtoolsButtons = observer( const DevtoolsButtons = observer(
@ -225,7 +221,6 @@ const DevtoolsButtons = observer(
bottomBlock, bottomBlock,
disabled, disabled,
events, events,
activeTab,
}: IDevtoolsButtons) => { }: IDevtoolsButtons) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { aiSummaryStore, integrationsStore } = useStore(); const { aiSummaryStore, integrationsStore } = useStore();
@ -267,36 +262,6 @@ const DevtoolsButtons = observer(
const possibleAudio = events.filter((e) => e.name.includes('media/audio')); const possibleAudio = events.filter((e) => e.name.includes('media/audio'));
const integratedServices = const integratedServices =
integrationsStore.integrations.backendLogIntegrations; integrationsStore.integrations.backendLogIntegrations;
const showIcons = activeTab === 'EXPORT'
const labels = {
console: {
icon: <CodeOutlined size={14} />,
label: t('Console'),
},
performance: {
icon: <DashboardOutlined size={14} />,
label: t('Performance'),
},
network: {
icon: <ArrowDownUp size={14} strokeWidth={2} />,
label: t('Network'),
},
events: {
icon: <ListCollapse size={14} strokeWidth={2} />,
label: t('Events'),
},
state: {
icon: getStorageName(storageType).icon,
label: getStorageName(storageType).name,
},
graphql: {
icon: <Merge size={14} strokeWidth={2} />,
label: 'Graphql',
}
}
// @ts-ignore
const getLabel = (block: string) => labels[block][showIcons ? 'icon' : 'label']
return ( return (
<> <>
{isSaas ? <SummaryButton onClick={showSummary} /> : null} {isSaas ? <SummaryButton onClick={showSummary} /> : null}
@ -309,7 +274,6 @@ const DevtoolsButtons = observer(
</div> </div>
</div> </div>
} }
customKey="xray"
label="X-Ray" label="X-Ray"
onClick={() => toggleBottomTools(OVERVIEW)} onClick={() => toggleBottomTools(OVERVIEW)}
active={bottomBlock === OVERVIEW && !inspectorMode} active={bottomBlock === OVERVIEW && !inspectorMode}
@ -322,11 +286,10 @@ const DevtoolsButtons = observer(
<div>{t('Launch Console')}</div> <div>{t('Launch Console')}</div>
</div> </div>
} }
customKey="console"
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(CONSOLE)} onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode} active={bottomBlock === CONSOLE && !inspectorMode}
label={getLabel('console')} label={t('Console')}
hasErrors={logRedCount > 0 || showExceptions} hasErrors={logRedCount > 0 || showExceptions}
/> />
@ -337,11 +300,10 @@ const DevtoolsButtons = observer(
<div>{t('Launch Network')}</div> <div>{t('Launch Network')}</div>
</div> </div>
} }
customKey="network"
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(NETWORK)} onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode} active={bottomBlock === NETWORK && !inspectorMode}
label={getLabel('network')} label={t('Network')}
hasErrors={resourceRedCount > 0} hasErrors={resourceRedCount > 0}
/> />
@ -352,11 +314,10 @@ const DevtoolsButtons = observer(
<div>{t('Launch Performance')}</div> <div>{t('Launch Performance')}</div>
</div> </div>
} }
customKey="performance"
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(PERFORMANCE)} onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode} active={bottomBlock === PERFORMANCE && !inspectorMode}
label={getLabel('performance')} label="Performance"
/> />
{showGraphql && ( {showGraphql && (
@ -364,8 +325,7 @@ const DevtoolsButtons = observer(
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(GRAPHQL)} onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode} active={bottomBlock === GRAPHQL && !inspectorMode}
label={getLabel('graphql')} label="Graphql"
customKey="graphql"
/> />
)} )}
@ -377,11 +337,10 @@ const DevtoolsButtons = observer(
<div>{t('Launch State')}</div> <div>{t('Launch State')}</div>
</div> </div>
} }
customKey="state"
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(STORAGE)} onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode} active={bottomBlock === STORAGE && !inspectorMode}
label={getLabel('state')} label={getStorageName(storageType) as string}
/> />
)} )}
<ControlButton <ControlButton
@ -391,16 +350,14 @@ const DevtoolsButtons = observer(
<div>{t('Launch Events')}</div> <div>{t('Launch Events')}</div>
</div> </div>
} }
customKey="events"
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(STACKEVENTS)} onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode} active={bottomBlock === STACKEVENTS && !inspectorMode}
label={getLabel('events')} label={t('Events')}
hasErrors={stackRedCount > 0} hasErrors={stackRedCount > 0}
/> />
{showProfiler && ( {showProfiler && (
<ControlButton <ControlButton
customKey="profiler"
disabled={disableButtons} disabled={disableButtons}
onClick={() => toggleBottomTools(PROFILER)} onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode} active={bottomBlock === PROFILER && !inspectorMode}
@ -411,7 +368,6 @@ const DevtoolsButtons = observer(
<LogsButton <LogsButton
integrated={integratedServices.map((service) => service.name)} integrated={integratedServices.map((service) => service.name)}
onClick={() => toggleBottomTools(BACKENDLOGS)} onClick={() => toggleBottomTools(BACKENDLOGS)}
shorten={showIcons}
/> />
) : null} ) : null}
{possibleAudio.length ? ( {possibleAudio.length ? (

View file

@ -6,11 +6,9 @@ import {
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import stl from './timeline.module.css'; import stl from './timeline.module.css';
import { getTimelinePosition } from './getTimelinePosition'; import { getTimelinePosition } from './getTimelinePosition';
import { useStore } from '@/mstore';
function EventsList() { function EventsList() {
const { store } = useContext(PlayerContext); const { store } = useContext(PlayerContext);
const { uiPlayerStore } = useStore();
const { eventCount, endTime } = store.get(); const { eventCount, endTime } = store.get();
const { tabStates } = store.get(); const { tabStates } = store.get();
@ -19,6 +17,7 @@ function EventsList() {
() => Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [], () => Object.values(tabStates)[0]?.eventList.filter((e) => e.time) || [],
[eventCount], [eventCount],
); );
React.useEffect(() => { React.useEffect(() => {
const hasDuplicates = events.some( const hasDuplicates = events.some(
(e, i) => (e, i) =>

View file

@ -49,6 +49,7 @@
z-index: 2; z-index: 2;
} }
.event { .event {
position: absolute; position: absolute;
width: 2px; width: 2px;

View file

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

View file

@ -202,7 +202,7 @@ function UnitStepsModal({ onClose }: Props) {
<div className={'w-full'}> <div className={'w-full'}>
<CodeBlock <CodeBlock
width={340} width={340}
height={'calc(100vh - 174px)'} height={'calc(100vh - 146px)'}
extra={`${events.length} Events`} extra={`${events.length} Events`}
copy copy
code={eventStr} code={eventStr}

View file

@ -34,46 +34,38 @@ const WarnBadge = React.memo(
currentLocation, currentLocation,
version, version,
siteId, siteId,
virtualElsFailed,
onVMode,
}: { }: {
currentLocation: string; currentLocation: string;
version: string; version: string;
siteId: string; siteId: string;
virtualElsFailed: boolean;
onVMode: () => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const localhostWarnSiteKey = localhostWarn(siteId); const localhostWarnSiteKey = localhostWarn(siteId);
const defaultLocalhostWarn = const defaultLocalhostWarn =
localStorage.getItem(localhostWarnSiteKey) !== '1'; localStorage.getItem(localhostWarnSiteKey) !== '1';
const localhostWarnActive = Boolean( const localhostWarnActive =
currentLocation && currentLocation &&
defaultLocalhostWarn && 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 trackerVersion = window.env.TRACKER_VERSION ?? undefined;
const trackerVerDiff = compareVersions(version, trackerVersion); const trackerVerDiff = compareVersions(version, trackerVersion);
const trackerWarnActive = trackerVerDiff !== VersionComparison.Same; const trackerWarnActive = trackerVerDiff !== VersionComparison.Same;
const [warnings, setWarnings] = React.useState<[localhostWarn: boolean, trackerWarn: boolean, virtualElsFailWarn: boolean]>([localhostWarnActive, trackerWarnActive, virtualElsFailed]) const [showLocalhostWarn, setLocalhostWarn] =
React.useState(localhostWarnActive);
const [showTrackerWarn, setTrackerWarn] = React.useState(trackerWarnActive);
React.useEffect(() => { const closeWarning = (type: 1 | 2) => {
setWarnings([localhostWarnActive, trackerWarnActive, virtualElsFailed])
}, [localhostWarnActive, trackerWarnActive, virtualElsFailed])
const closeWarning = (type: 0 | 1 | 2) => {
if (type === 1) { if (type === 1) {
localStorage.setItem(localhostWarnSiteKey, '1'); localStorage.setItem(localhostWarnSiteKey, '1');
setLocalhostWarn(false);
}
if (type === 2) {
setTrackerWarn(false);
} }
setWarnings((prev) => {
const newWarnings = [...prev];
newWarnings[type] = false;
return newWarnings;
});
}; };
if (!warnings.some(el => el === true)) return null; if (!showLocalhostWarn && !showTrackerWarn) return null;
return ( return (
<div <div
@ -87,7 +79,7 @@ const WarnBadge = React.memo(
fontWeight: 500, fontWeight: 500,
}} }}
> >
{warnings[0] ? ( {showLocalhostWarn ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between"> <div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
<div> <div>
<span>{t('Some assets may load incorrectly on localhost.')}</span> <span>{t('Some assets may load incorrectly on localhost.')}</span>
@ -109,7 +101,7 @@ const WarnBadge = React.memo(
</div> </div>
</div> </div>
) : null} ) : null}
{warnings[1] ? ( {showTrackerWarn ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between"> <div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between">
<div> <div>
<div> <div>
@ -133,21 +125,6 @@ const WarnBadge = React.memo(
</div> </div>
</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 <div
className="py-1 ml-3 cursor-pointer" className="py-1 ml-3 cursor-pointer"
onClick={() => closeWarning(2)} onClick={() => closeWarning(2)}

View file

@ -12,123 +12,60 @@ import {
getDateRangeFromValue, getDateRangeFromValue,
getDateRangeLabel, getDateRangeLabel,
} from 'App/dateRange'; } from 'App/dateRange';
import { DateTime, Interval, Settings } from 'luxon'; import { DateTime, Interval } from 'luxon';
import styles from './dateRangePopup.module.css'; import styles from './dateRangePopup.module.css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
function DateRangePopup(props: any) { function DateRangePopup(props: any) {
const { t } = useTranslation(); const { t } = useTranslation();
const [displayDates, setDisplayDates] = React.useState<[Date, Date]>([new Date(), new Date()]);
const [range, setRange] = React.useState( const [range, setRange] = React.useState(
props.selectedDateRange || props.selectedDateRange ||
Interval.fromDateTimes(DateTime.now(), DateTime.now()), Interval.fromDateTimes(DateTime.now(), DateTime.now()),
); );
const [value, setValue] = React.useState<string | null>(null); const [value, setValue] = React.useState<string | null>(null);
React.useEffect(() => { const selectCustomRange = (range) => {
if (props.selectedDateRange) { let newRange;
const start = new Date( if (props.singleDay) {
props.selectedDateRange.start.year, newRange = Interval.fromDateTimes(
props.selectedDateRange.start.month - 1, // JS months are 0-based DateTime.fromJSDate(range),
props.selectedDateRange.start.day DateTime.fromJSDate(range),
); );
const end = new Date( } else {
props.selectedDateRange.end.year, newRange = Interval.fromDateTimes(
props.selectedDateRange.end.month - 1, DateTime.fromJSDate(range[0]),
props.selectedDateRange.end.day DateTime.fromJSDate(range[1]),
); );
setDisplayDates([start, end]); }
} setRange(newRange);
}, [props.selectedDateRange]);
const createNaiveTime = (dateTime: DateTime) => {
if (!dateTime) return null;
return DateTime.fromObject({
hour: dateTime.hour,
minute: dateTime.minute
});
};
const selectCustomRange = (newDates: [Date, Date]) => {
if (!newDates || !newDates[0] || !newDates[1]) return;
setDisplayDates(newDates);
const selectedTzStart = DateTime.fromObject({
year: newDates[0].getFullYear(),
month: newDates[0].getMonth() + 1,
day: newDates[0].getDate(),
hour: 0,
minute: 0
}).setZone(Settings.defaultZone);
const selectedTzEnd = DateTime.fromObject({
year: newDates[1].getFullYear(),
month: newDates[1].getMonth() + 1,
day: newDates[1].getDate(),
hour: 23,
minute: 59
}).setZone(Settings.defaultZone);
const updatedRange = Interval.fromDateTimes(selectedTzStart, selectedTzEnd);
setRange(updatedRange);
setValue(CUSTOM_RANGE); setValue(CUSTOM_RANGE);
}; };
const setRangeTimeStart = (naiveTime: DateTime) => { const setRangeTimeStart = (value: DateTime) => {
if (!range.end || !naiveTime) return; if (!range.end || value > range.end) {
return;
const newStart = range.start.set({ }
hour: naiveTime.hour, const newRange = range.start.set({
minute: naiveTime.minute hour: value.hour,
minute: value.minute,
}); });
setRange(Interval.fromDateTimes(newRange, range.end));
if (newStart > range.end) return;
setRange(Interval.fromDateTimes(newStart, range.end));
setValue(CUSTOM_RANGE); setValue(CUSTOM_RANGE);
}; };
const setRangeTimeEnd = (naiveTime: DateTime) => { const setRangeTimeEnd = (value: DateTime) => {
if (!range.start || !naiveTime) return; if (!range.start || (value && value < range.start)) {
return;
const newEnd = range.end.set({ }
hour: naiveTime.hour, const newRange = range.end.set({ hour: value.hour, minute: value.minute });
minute: naiveTime.minute setRange(Interval.fromDateTimes(range.start, newRange));
});
if (newEnd < range.start) return;
setRange(Interval.fromDateTimes(range.start, newEnd));
setValue(CUSTOM_RANGE); setValue(CUSTOM_RANGE);
}; };
const selectValue = (value: string) => { const selectValue = (value: string) => {
const newRange = getDateRangeFromValue(value); const range = getDateRangeFromValue(value);
setRange(range);
if (!newRange.start || !newRange.end) {
setRange(Interval.fromDateTimes(DateTime.now(), DateTime.now()));
setDisplayDates([new Date(), new Date()]);
setValue(null);
return;
}
const zonedStart = newRange.start.setZone(Settings.defaultZone);
const zonedEnd = newRange.end.setZone(Settings.defaultZone);
setRange(Interval.fromDateTimes(zonedStart, zonedEnd));
const start = new Date(
zonedStart.year,
zonedStart.month - 1,
zonedStart.day
);
const end = new Date(
zonedEnd.year,
zonedEnd.month - 1,
zonedEnd.day
);
setDisplayDates([start, end]);
setValue(value); setValue(value);
}; };
@ -140,9 +77,9 @@ function DateRangePopup(props: any) {
const isUSLocale = const isUSLocale =
navigator.language === 'en-US' || navigator.language.startsWith('en-US'); navigator.language === 'en-US' || navigator.language.startsWith('en-US');
const naiveStartTime = createNaiveTime(range.start); const rangeForDisplay = props.singleDay
const naiveEndTime = createNaiveTime(range.end); ? range.start.ts
: [range.start!.startOf('day').ts, range.end!.startOf('day').ts];
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={`${styles.body} h-fit`}> <div className={`${styles.body} h-fit`}>
@ -166,7 +103,7 @@ function DateRangePopup(props: any) {
shouldCloseCalendar={() => false} shouldCloseCalendar={() => false}
isOpen isOpen
maxDate={new Date()} maxDate={new Date()}
value={displayDates} value={rangeForDisplay}
calendarProps={{ calendarProps={{
tileDisabled: props.isTileDisabled, tileDisabled: props.isTileDisabled,
selectRange: !props.singleDay, selectRange: !props.singleDay,
@ -185,7 +122,7 @@ function DateRangePopup(props: any) {
<span>{range.start.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span> <span>{range.start.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span>
<TimePicker <TimePicker
format={isUSLocale ? 'hh:mm a' : 'HH:mm'} format={isUSLocale ? 'hh:mm a' : 'HH:mm'}
value={naiveStartTime} value={range.start}
onChange={setRangeTimeStart} onChange={setRangeTimeStart}
needConfirm={false} needConfirm={false}
showNow={false} showNow={false}
@ -195,7 +132,7 @@ function DateRangePopup(props: any) {
<span>{range.end.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span> <span>{range.end.toFormat(isUSLocale ? 'MM/dd' : 'dd/MM')} </span>
<TimePicker <TimePicker
format={isUSLocale ? 'hh:mm a' : 'HH:mm'} format={isUSLocale ? 'hh:mm a' : 'HH:mm'}
value={naiveEndTime} value={range.end}
onChange={setRangeTimeEnd} onChange={setRangeTimeEnd}
needConfirm={false} needConfirm={false}
showNow={false} showNow={false}

View file

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

View file

@ -1,178 +0,0 @@
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

@ -18,7 +18,7 @@ function DocCard(props: Props) {
} = props; } = props;
return ( return (
<div className={cn('p-5 bg-gray-lightest mb-4 rounded-lg', className)}> <div className={cn('p-5 bg-gray-lightest mb-4 rounded', className)}>
<div className="font-medium mb-2 flex items-center"> <div className="font-medium mb-2 flex items-center">
{props.icon && ( {props.icon && (
<div <div

View file

@ -5,7 +5,6 @@ import cn from 'classnames';
import { Loader } from 'UI'; import { Loader } from 'UI';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { VList } from 'virtua';
function TruncatedText({ function TruncatedText({
text, text,
@ -125,7 +124,7 @@ export function AutocompleteModal({
if (index === blocksAmount - 1 && blocksAmount > 1) { if (index === blocksAmount - 1 && blocksAmount > 1) {
str += ' and '; str += ' and ';
} }
str += block.trim(); str += `"${block.trim()}"`;
if (index < blocksAmount - 2) { if (index < blocksAmount - 2) {
str += ', '; str += ', ';
} }
@ -171,27 +170,25 @@ export function AutocompleteModal({
<> <>
<div <div
className="flex flex-col gap-2 overflow-y-auto py-2 overflow-x-hidden text-ellipsis" className="flex flex-col gap-2 overflow-y-auto py-2 overflow-x-hidden text-ellipsis"
style={{ height: Math.min(sortedOptions.length * 32, 240) }} style={{ maxHeight: 200 }}
> >
<VList count={sortedOptions.length} itemSize={18}> {sortedOptions.map((item) => (
{sortedOptions.map((item) => ( <div
<div key={item.value}
key={item.value} onClick={() => onSelectOption(item)}
onClick={() => onSelectOption(item)} className="cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2"
className="cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2" >
> <Checkbox checked={isSelected(item)} /> {item.label}
<Checkbox checked={isSelected(item)} /> {item.label} </div>
</div> ))}
))}
</VList>
</div> </div>
{query.length ? ( {query.length ? (
<div className="border-y border-y-gray-light py-2"> <div className="border-y border-y-gray-light py-2">
<div <div
className="whitespace-nowrap truncate w-full rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1" className="whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
onClick={applyQuery} onClick={applyQuery}
> >
{t('Apply')}&nbsp;<span className='font-semibold'>{queryStr}</span> {t('Apply')}&nbsp;{queryStr}
</div> </div>
</div> </div>
) : null} ) : null}

View file

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

View file

@ -9,10 +9,8 @@ function LiveSessionSearch() {
const appliedFilter = searchStoreLive.instance; const appliedFilter = searchStoreLive.instance;
useEffect(() => { useEffect(() => {
if (projectsStore.activeSiteId) { void searchStoreLive.fetchSessions();
void searchStoreLive.fetchSessions(true); }, []);
}
}, [projectsStore.activeSiteId])
const onAddFilter = (filter: any) => { const onAddFilter = (filter: any) => {
filter.autoOpen = true; filter.autoOpen = true;

View file

@ -53,6 +53,9 @@ function SessionFilters() {
onBeforeLoad: async () => { onBeforeLoad: async () => {
await reloadTags(); await reloadTags();
}, },
onLoaded: () => {
debounceFetch = debounce(() => searchStore.fetchSessions(), 500);
}
}); });
const onAddFilter = (filter: any) => { const onAddFilter = (filter: any) => {

View file

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

View file

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

View file

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

View file

@ -1,30 +0,0 @@
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

@ -5,15 +5,6 @@ import { Tooltip } from 'antd';
import cn from 'classnames'; import cn from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface Props {
code?: string;
extra?: string;
language?: string;
copy?: boolean;
width?: string;
height?: string;
}
export default function CodeBlock({ export default function CodeBlock({
code = '', code = '',
extra = '', extra = '',
@ -21,7 +12,7 @@ export default function CodeBlock({
copy = false, copy = false,
width = undefined, width = undefined,
height = undefined, height = undefined,
}: Props) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {

View file

@ -31,7 +31,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {
{icon && ( {icon && (
<Icon <Icon
name={icon} name={icon}
className="absolute top-0 bottom-0 my-auto ml-4 z-10" className="absolute top-0 bottom-0 my-auto ml-4"
size="14" size="14"
/> />
)} )}

View file

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

View file

@ -49,8 +49,13 @@ const useSessionSearchQueryHandler = ({
searchStore.applyFilter(filter, true); searchStore.applyFilter(filter, true);
} }
// Important: Mark URL as parsed BEFORE fetching
// This prevents the initial fetch when the URL is parsed
searchStore.setUrlParsed(); searchStore.setUrlParsed();
onLoaded?.();
// Then fetch sessions - this is the only place that should fetch initially
await searchStore.fetchSessions();
onLoaded();
} catch (error) { } catch (error) {
console.error('Error applying filter from query:', error); console.error('Error applying filter from query:', error);
searchStore.setUrlParsed(); searchStore.setUrlParsed();

View file

@ -255,7 +255,7 @@ function SideMenu(props: Props) {
<Tag <Tag
color="cyan" color="cyan"
bordered={false} bordered={false}
className="text-xs ml-2" className="text-xs"
> >
{t('Beta')} {t('Beta')}
</Tag> </Tag>

View file

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

View file

@ -503,7 +503,7 @@
"Returning users between": "Usuarios recurrentes entre", "Returning users between": "Usuarios recurrentes entre",
"Sessions": "Sesiones", "Sessions": "Sesiones",
"No recordings found.": "No se encontraron grabaciones.", "No recordings found.": "No se encontraron grabaciones.",
"Get new image": "Obtener nueva sesión", "Get new session": "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.", "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", "Add Card": "Agregar tarjeta",
"Create Dashboard": "Crear panel", "Create Dashboard": "Crear panel",

View file

@ -503,7 +503,7 @@
"Returning users between": "Utilisateurs récurrents entre", "Returning users between": "Utilisateurs récurrents entre",
"Sessions": "Sessions", "Sessions": "Sessions",
"No recordings found.": "Aucun enregistrement trouvé.", "No recordings found.": "Aucun enregistrement trouvé.",
"Get new image": "Obtenir une nouvelle session", "Get new session": "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.", "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", "Add Card": "Ajouter une carte",
"Create Dashboard": "Créer un tableau de bord", "Create Dashboard": "Créer un tableau de bord",

View file

@ -504,7 +504,7 @@
"Returning users between": "Возвращающиеся пользователи за период", "Returning users between": "Возвращающиеся пользователи за период",
"Sessions": "Сессии", "Sessions": "Сессии",
"No recordings found.": "Записей не найдено.", "No recordings found.": "Записей не найдено.",
"Get new image": "Получить новую сессию", "Get new session": "Получить новую сессию",
"The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.", "The number of cards in one dashboard is limited to 30.": "Количество карточек в одном дашборде ограничено 30.",
"Add Card": "Добавить карточку", "Add Card": "Добавить карточку",
"Create Dashboard": "Создать дашборд", "Create Dashboard": "Создать дашборд",
@ -1498,8 +1498,5 @@
"More attribute": "Еще атрибут", "More attribute": "Еще атрибут",
"More attributes": "Еще атрибуты", "More attributes": "Еще атрибуты",
"Account settings updated successfully": "Настройки аккаунта успешно обновлены", "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": "回访用户区间", "Returning users between": "回访用户区间",
"Sessions": "会话", "Sessions": "会话",
"No recordings found.": "未找到录制。", "No recordings found.": "未找到录制。",
"Get new image": "获取新会话", "Get new session": "获取新会话",
"The number of cards in one dashboard is limited to 30.": "一个仪表板最多可包含30个卡片。", "The number of cards in one dashboard is limited to 30.": "一个仪表板最多可包含30个卡片。",
"Add Card": "添加卡片", "Add Card": "添加卡片",
"Create Dashboard": "创建仪表板", "Create Dashboard": "创建仪表板",

View file

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

View file

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

View file

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

View file

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

View file

@ -220,7 +220,6 @@ class SearchStoreLive {
updateFilter = (index: number, search: Partial<IFilter>) => { updateFilter = (index: number, search: Partial<IFilter>) => {
const newFilters = this.instance.filters.map((_filter: any, i: any) => { const newFilters = this.instance.filters.map((_filter: any, i: any) => {
if (i === index) { if (i === index) {
search.value = checkFilterValue(search.value);
return search; return search;
} }
return _filter; return _filter;

View file

@ -15,7 +15,9 @@ import { loadFile } from 'App/player/web/network/loadFiles';
import { LAST_7_DAYS } from 'Types/app/period'; import { LAST_7_DAYS } from 'Types/app/period';
import { filterMap } from 'App/mstore/searchStore'; import { filterMap } from 'App/mstore/searchStore';
import { getDateRangeFromValue } from 'App/dateRange'; import { getDateRangeFromValue } from 'App/dateRange';
import { clean as cleanParams } from '../api_client';
import { searchStore, searchStoreLive } from './index'; import { searchStore, searchStoreLive } from './index';
const range = getDateRangeFromValue(LAST_7_DAYS); const range = getDateRangeFromValue(LAST_7_DAYS);
const defaultDateFilters = { const defaultDateFilters = {

View file

@ -157,7 +157,7 @@ export default class FilterItem {
const json = { const json = {
type: isMetadata ? FilterKey.METADATA : this.key, type: isMetadata ? FilterKey.METADATA : this.key,
isEvent: Boolean(this.isEvent), isEvent: Boolean(this.isEvent),
value: this.value?.map((i: any) => (i ? i.toString() : '')) || [], value: this.value.map((i: any) => (i ? i.toString() : '')),
operator: this.operator, operator: this.operator,
source: isMetadata ? this.key.replace(/^_/, '') : this.source, source: isMetadata ? this.key.replace(/^_/, '') : this.source,
sourceOperator: this.sourceOperator, sourceOperator: this.sourceOperator,

View file

@ -7,7 +7,6 @@ import Filter, { IFilter } from 'App/mstore/types/filter';
import FilterItem from 'App/mstore/types/filterItem'; import FilterItem from 'App/mstore/types/filterItem';
import { makeAutoObservable, observable } from 'mobx'; import { makeAutoObservable, observable } from 'mobx';
import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period'; import { LAST_24_HOURS, LAST_30_DAYS, LAST_7_DAYS } from 'Types/app/period';
import { roundToNextMinutes } from '@/utils';
// @ts-ignore // @ts-ignore
const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS; const rangeValue = DATE_RANGE_VALUES.LAST_24_HOURS;
@ -178,7 +177,6 @@ export default class Search {
js.rangeValue, js.rangeValue,
js.startDate, js.startDate,
js.endDate, js.endDate,
15,
); );
js.startDate = startDate; js.startDate = startDate;
js.endDate = endDate; js.endDate = endDate;
@ -192,11 +190,12 @@ export default class Search {
rangeName: string, rangeName: string,
customStartDate: number, customStartDate: number,
customEndDate: number, customEndDate: number,
roundMinutes?: number, ): {
): { startDate: number; endDate: number } { startDate: number;
endDate: number;
} {
let endDate = new Date().getTime(); let endDate = new Date().getTime();
let startDate: number; let startDate: number;
const minutes = roundMinutes || 15;
switch (rangeName) { switch (rangeName) {
case LAST_7_DAYS: case LAST_7_DAYS:
@ -207,7 +206,9 @@ export default class Search {
break; break;
case CUSTOM_RANGE: case CUSTOM_RANGE:
if (!customStartDate || !customEndDate) { if (!customStartDate || !customEndDate) {
throw new Error('Start date and end date must be provided for CUSTOM_RANGE.'); throw new Error(
'Start date and end date must be provided for CUSTOM_RANGE.',
);
} }
startDate = customStartDate; startDate = customStartDate;
endDate = customEndDate; endDate = customEndDate;
@ -217,12 +218,10 @@ export default class Search {
startDate = endDate - 24 * 60 * 60 * 1000; startDate = endDate - 24 * 60 * 60 * 1000;
} }
if (rangeName !== CUSTOM_RANGE) { return {
startDate = roundToNextMinutes(startDate, minutes); startDate,
endDate = roundToNextMinutes(endDate, minutes); endDate,
} };
return { startDate, endDate };
} }
fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) { fromJS({ eventsOrder, filters, events, custom, ...filterData }: any) {

View file

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

View file

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

View file

@ -114,14 +114,12 @@ class UserStore {
get isEnterprise() { get isEnterprise() {
return ( return (
this.account?.edition === 'ee' || this.account?.edition === 'ee' ||
this.authStore.authDetails?.edition === 'ee' this.account?.edition === 'msaas' ||
this.authStore.authDetails?.edition === 'ee' ||
this.authStore.authDetails?.edition === 'msaas'
); );
} }
get isSSOSupported() {
return this.isEnterprise || this.account?.edition === 'msaas' || this.authStore.authDetails?.edition === 'msaas';
}
get isLoggedIn() { get isLoggedIn() {
return Boolean(this.jwt); return Boolean(this.jwt);
} }

View file

@ -124,9 +124,13 @@ export default class ListWalker<T extends Timed> {
* Assumed that the current message is already handled so * Assumed that the current message is already handled so
* if pointer doesn't change <null> is returned. * if pointer doesn't change <null> is returned.
*/ */
moveGetLast(t: number, index?: number, force?: boolean, debug?: boolean): T | null { moveGetLast(t: number, index?: number): T | null {
const key: string = index ? '_index' : 'time'; let key: string = 'time'; // TODO
const val = index ? index : t; let val = t;
if (index) {
key = '_index';
val = index;
}
let changed = false; let changed = false;
// @ts-ignore // @ts-ignore
@ -139,10 +143,7 @@ export default class ListWalker<T extends Timed> {
this.movePrev(); this.movePrev();
changed = true; changed = true;
} }
if (debug) { return changed ? this.list[this.p - 1] : null;
console.log(this.list[this.p - 1])
}
return changed || force ? this.list[this.p - 1] : null;
} }
prevTs = 0; prevTs = 0;

Some files were not shown because too many files have changed in this diff Show more