Compare commits

...
Sign in to create a new pull request.

54 commits

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

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

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

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

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

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

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

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

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

* fix(build): proper platform

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

---------

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

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

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

---------

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

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

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

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

---------

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

* ui: fixes for litjs capture

* ui: introduce vmode for lwc light dom

* ui: fixup the mode toggle and remover

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

View file

@ -8,7 +8,11 @@ on:
required: true required: true
default: 'chalice,frontend' default: 'chalice,frontend'
tag: tag:
description: 'Tag to build patches from.' description: 'Tag to update.'
required: true
type: string
branch:
description: 'Branch to build patches from. Make sure the branch is uptodate with tag. Else itll cause missing commits.'
required: true required: true
type: string type: string
@ -73,7 +77,7 @@ jobs:
- 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
run: echo "BRANCH_NAME=patch/main/${HEAD_COMMIT_ID}" >> $GITHUB_ENV run: echo "BRANCH_NAME=${{inputs.branch}}" >> $GITHUB_ENV
- name: Build - name: Build
id: build-image id: build-image

View file

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

View file

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

View file

@ -85,7 +85,8 @@ 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 value, type return f"""SELECT DISTINCT ON(value,type) value, type
((SELECT DISTINCT value, type
FROM {TABLE} FROM {TABLE}
WHERE WHERE
project_id = %(project_id)s project_id = %(project_id)s
@ -101,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);""" LIMIT 5)) AS raw;"""
return f"""SELECT DISTINCT value, type return f"""SELECT DISTINCT value, type
FROM {TABLE} FROM {TABLE}
WHERE WHERE
@ -326,7 +327,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 key, value, 'METADATA' AS TYPE SELECT DISTINCT ON(key, value) 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

@ -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,10 +95,9 @@ 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) VALUES (%(username)s, %(token)s, %(user_id)s, %(url)s) RETURNING username, token, url;""",
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})
) )
@ -112,9 +111,10 @@ 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 FROM public.jira_cloud DELETE
WHERE user_id=%(user_id)s;""", FROM public.jira_cloud
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

@ -2,11 +2,12 @@ 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"
@ -50,10 +51,6 @@ 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
@ -69,6 +66,23 @@ 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,7 +86,8 @@ 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 value, type return f"""SELECT DISTINCT ON(value, type) value, type
FROM ((SELECT DISTINCT value, type
FROM {TABLE} FROM {TABLE}
WHERE WHERE
project_id = %(project_id)s project_id = %(project_id)s
@ -102,7 +103,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);""" LIMIT 5)) AS raw;"""
return f"""SELECT DISTINCT value, type return f"""SELECT DISTINCT value, type
FROM {TABLE} FROM {TABLE}
WHERE WHERE
@ -257,7 +258,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 key, value, 'METADATA' AS TYPE query = cur.format(query=f"""SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE
FROM({" UNION ALL ".join(sub_from)}) AS all_metas 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("toString(`$properties`.error_id) = %(error_id)s") ch_basic_query.append("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 toString(`$properties`.error_id) AS error_id, WITH pre_processed AS (SELECT 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 toString(`$properties`.error_id) = %(error_id)s AND 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 toString(`$properties`.error_id) = %(error_id)s AND 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

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

View file

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

View file

@ -1,5 +1,4 @@
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';
@ -10,7 +9,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 { debounce } from '@/utils'; import { debounceCall } from '@/utils';
const components: any = { const components: any = {
SessionPure: lazy(() => import('Components/Session/Session')), SessionPure: lazy(() => import('Components/Session/Session')),
@ -88,7 +87,6 @@ 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();
@ -99,7 +97,6 @@ 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();
@ -124,13 +121,9 @@ function PrivateRoutes() {
} }
}, [siteId]); }, [siteId]);
React.useEffect(() => {
debounceSearch = debounce(() => searchStore.fetchSessions(), 250);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (!searchStore.urlParsed) return; if (!searchStore.urlParsed) return;
debounceSearch(); debounceCall(() => searchStore.fetchSessions(true), 250)()
}, [searchStore.urlParsed, searchStore.instance.filters, searchStore.instance.eventsOrder]); }, [searchStore.urlParsed, searchStore.instance.filters, searchStore.instance.eventsOrder]);
return ( return (

View file

@ -6,6 +6,7 @@ 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';
@ -35,6 +36,7 @@ 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

@ -23,6 +23,7 @@ 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 session')} {t('Get new image')}
</Button> </Button>
</div> </div>
); );

View file

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

View file

@ -117,8 +117,6 @@ 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

@ -181,9 +181,10 @@ 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 } ? { ...metricParams, density }
: { ...params, ...timestmaps, ..._metric.toJson() }; : { ...params, ...timestmaps, ..._metric.toJson(), density };
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;
function calculateGranularities(periodDurationMs: number) { export 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

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

View file

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

View file

@ -114,13 +114,11 @@ function PlayerBlockHeader(props: any) {
)} )}
{_metaList.length > 0 && ( {_metaList.length > 0 && (
<div className="h-full flex items-center px-2 gap-1"> <SessionMetaList
<SessionMetaList horizontal
className="" metaList={_metaList}
metaList={_metaList} maxLength={2}
maxLength={2} />
/>
</div>
)} )}
</div> </div>
</div> </div>

View file

@ -13,8 +13,8 @@ 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 ".store/@ant-design-icons-virtual-42686020c5/package"; import { CloseOutlined } from "@ant-design/icons";
import { Tooltip } from ".store/antd-virtual-9dbfadb7f6/package"; import { Tooltip } from "antd";
import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal"; import { getDefaultFramework, frameworkIcons } from "../UnitStepsModal";
interface IProps { interface IProps {

View file

@ -38,6 +38,7 @@ 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;
@ -45,7 +46,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 { store } = React.useContext(PlayerContext); const { player, 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);
@ -127,6 +128,13 @@ function SubHeader(props) {
}); });
}; };
const showVModeBadge = store.get().vModeBadge;
const onVMode = () => {
settingsStore.sessionSettings.updateKey('virtualMode', true);
player.enableVMode?.();
location.reload();
}
return ( return (
<> <>
<div <div
@ -143,6 +151,8 @@ 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

@ -34,38 +34,46 @@ 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 = const localhostWarnActive = Boolean(
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 [showLocalhostWarn, setLocalhostWarn] = const [warnings, setWarnings] = React.useState<[localhostWarn: boolean, trackerWarn: boolean, virtualElsFailWarn: boolean]>([localhostWarnActive, trackerWarnActive, virtualElsFailed])
React.useState(localhostWarnActive);
const [showTrackerWarn, setTrackerWarn] = React.useState(trackerWarnActive);
const closeWarning = (type: 1 | 2) => { React.useEffect(() => {
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 (!showLocalhostWarn && !showTrackerWarn) return null; if (!warnings.some(el => el === true)) return null;
return ( return (
<div <div
@ -79,7 +87,7 @@ const WarnBadge = React.memo(
fontWeight: 500, fontWeight: 500,
}} }}
> >
{showLocalhostWarn ? ( {warnings[0] ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between"> <div 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>
@ -101,7 +109,7 @@ const WarnBadge = React.memo(
</div> </div>
</div> </div>
) : null} ) : null}
{showTrackerWarn ? ( {warnings[1] ? (
<div className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between"> <div 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>
@ -125,6 +133,21 @@ 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

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

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

View file

@ -125,7 +125,7 @@ export function AutocompleteModal({
if (index === blocksAmount - 1 && blocksAmount > 1) { 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 += ', ';
} }
@ -188,10 +188,10 @@ export function AutocompleteModal({
{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-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1" className="whitespace-nowrap truncate w-full rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1"
onClick={applyQuery} onClick={applyQuery}
> >
{t('Apply')}&nbsp;{queryStr} {t('Apply')}&nbsp;<span className='font-semibold'>{queryStr}</span>
</div> </div>
</div> </div>
) : null} ) : null}

View file

@ -128,8 +128,10 @@ 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

@ -19,11 +19,13 @@ 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,13 +7,15 @@ 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 } = props; const { className = '', metaList, maxLength = 14, horizontal = false } = props;
return ( return (
<div className={cn('flex items-center flex-wrap gap-1', className)}> <div className={cn('flex items-center gap-1', horizontal ? '' : 'flex-wrap', className)}>
{metaList.slice(0, maxLength).map(({ label, value }, index) => ( {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,6 +5,7 @@ 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

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

View file

@ -9,6 +9,7 @@ export const GLOBAL_HAS_NO_RECORDINGS = '__$global-hasNoRecordings$__';
export const SITE_ID_STORAGE_KEY = '__$user-siteId$__'; export const 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

@ -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 session": "Get new session", "Get new image": "Get new image",
"The number of cards in one dashboard is limited to 30.": "The number of cards in one dashboard is limited to 30.", "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 session": "Obtener nueva sesión", "Get new image": "Obtener nueva sesión",
"The number of cards in one dashboard is limited to 30.": "El número de tarjetas en un panel está limitado a 30.", "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 session": "Obtenir une nouvelle session", "Get new image": "Obtenir une nouvelle session",
"The number of cards in one dashboard is limited to 30.": "Le nombre de cartes dans un tableau de bord est limité à 30.", "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 session": "Получить новую сессию", "Get new image": "Получить новую сессию",
"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,5 +1498,8 @@
"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 session": "获取新会话", "Get new image": "获取新会话",
"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,11 +1,13 @@
import { makeAutoObservable, runInAction } from 'mobx'; import { makeAutoObservable, runInAction, reaction } 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, LAST_7_DAYS } from 'Types/app/period'; import Period, { LAST_24_HOURS } 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,7 +38,7 @@ export default class DashboardStore {
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_24_HOURS }); drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
selectedDensity: number = 7; // depends on default drilldown, 7 points here!!!; selectedDensity: number = 7;
comparisonPeriods: Record<string, any> = {}; comparisonPeriods: Record<string, any> = {};
@ -83,10 +85,29 @@ 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());
},
);
} }
setDensity = (density: any) => { resetDensity = () => {
this.selectedDensity = parseInt(density, 10); this.createDensity(this.period.getDuration());
};
createDensity = (duration: number) => {
const densityOpts = calculateGranularities(duration);
const defaultOption = densityOpts[densityOpts.length - 2];
this.setDensity(defaultOption.key);
};
setDensity = (density: number) => {
this.selectedDensity = density;
}; };
get sortedDashboards() { get sortedDashboards() {
@ -446,7 +467,7 @@ export default class DashboardStore {
this.isSaving = true; this.isSaving = true;
try { try {
try { try {
const response = await dashboardService.addWidget(dashboard, metricIds); 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.');
@ -456,6 +477,17 @@ export default class DashboardStore {
} }
} }
resetPeriod = () => {
if (this.period) {
const range = this.period.rangeName;
if (range !== CUSTOM_RANGE) {
this.period = Period({ rangeName: this.period.rangeName });
} else {
this.period = Period({ rangeName: LAST_24_HOURS });
}
}
};
setPeriod(period: any) { setPeriod(period: any) {
this.period = Period({ this.period = Period({
start: period.start, start: period.start,

View file

@ -1,6 +1,5 @@
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,7 +4,6 @@ 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,10 +390,11 @@ class SearchStore {
// TODO // TODO
} }
async fetchSessions( fetchSessions = async (
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

@ -6,6 +6,7 @@ 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';
@ -71,27 +72,19 @@ 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,6 +163,7 @@ 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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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