Compare commits

..

1 commit

Author SHA1 Message Date
GitHub Action
8761ece4c0 Increment frontend chart version 2025-05-05 15:47:58 +00:00
66 changed files with 546 additions and 1040 deletions

View file

@ -8,11 +8,7 @@ on:
required: true required: true
default: 'chalice,frontend' default: 'chalice,frontend'
tag: tag:
description: 'Tag to update.' description: 'Tag to build patches from.'
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
@ -77,7 +73,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=${{inputs.branch}}" >> $GITHUB_ENV run: echo "BRANCH_NAME=patch/main/${HEAD_COMMIT_ID}" >> $GITHUB_ENV
- name: Build - name: Build
id: build-image id: build-image

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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,'mobile'=2) DEFAULT 'web', platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
datetime DateTime, datetime DateTime,
timezone LowCardinality(Nullable(String)), timezone LowCardinality(Nullable(String)),
duration UInt32, duration UInt32,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -80,6 +80,7 @@ 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,7 +69,6 @@ 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 = [
@ -100,7 +99,6 @@ 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();
@ -128,6 +126,7 @@ export default class MessageManager {
private tabsAmount = 0; private tabsAmount = 0;
private tabChangeEvents: TabChangeEvent[] = []; private tabChangeEvents: TabChangeEvent[] = [];
private activeTab = ''; private activeTab = '';
constructor( constructor(
@ -143,19 +142,8 @@ 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)) {
@ -406,9 +394,6 @@ 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,7 +99,6 @@ 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,
@ -117,13 +116,6 @@ 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>) => {
@ -134,10 +126,6 @@ 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,10 +21,15 @@ 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(
@ -99,10 +104,6 @@ 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,16 +140,11 @@ 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);
}); });
try { const colored = ctx.getImageData(0, 0, this.width, this.height);
const colored = ctx.getImageData(0, 0, this.width, this.height); this.colorize(colored.data, this.grad);
this.colorize(colored.data, this.grad); ctx.putImageData(colored, 0, 0);
ctx.putImageData(colored, 0, 0);
} catch (e) { return this;
// usually happens if session is corrupted ?
console.error('Error while colorizing heatmap:', e);
} finally {
return this;
}
} }
private colorize( private colorize(

View file

@ -44,34 +44,47 @@ 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;
@ -83,8 +96,6 @@ 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;
@ -94,8 +105,6 @@ 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);
} }
@ -298,9 +307,6 @@ 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: {
@ -444,17 +450,14 @@ 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 = const isCustomElement = vElem.tagName.includes('-') || vElem.tagName === 'SLOT';
vElem.tagName.includes('-') || vElem.tagName === 'SLOT'; const isNotActualIframe = !["IFRAME", "FRAME"].includes(vElem.tagName.toUpperCase());
const isLikelyShadowRoot = isCustomElement && isNotActualIframe;
if (isCustomElement) { if (isLikelyShadowRoot) {
if (this.virtualMode) { // Store the mapping but don't create the actual shadow root
// Store the mapping but don't create the actual shadow root this.shadowRootParentMap.set(msg.id, msg.frameID);
this.shadowRootParentMap.set(msg.id, msg.frameID); return;
return;
} else if (this.hasSlots) {
this.showVModeBadge?.();
}
} }
// Real iframes // Real iframes
@ -470,11 +473,7 @@ 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( logger.warn('No stylesheet was created for ', msg, this.olStyleSheets);
'No stylesheet was created for ',
msg,
this.olStyleSheets,
);
return; return;
} }
insertRule(styleSheet, msg); insertRule(styleSheet, msg);

View file

@ -22,7 +22,6 @@ 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();
} }
@ -31,10 +30,6 @@ 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)) {
@ -67,8 +62,6 @@ 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

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

View file

@ -38,6 +38,7 @@ 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,25 +54,6 @@ 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.7" AppVersion: "v1.22.5"

View file

@ -1,6 +1,7 @@
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
@ -10,12 +11,14 @@ 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.1" AppVersion: "v1.22.0"

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.42" AppVersion: "v1.22.27"

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,'mobile'=2) DEFAULT 'web', platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
datetime DateTime, datetime DateTime,
timezone LowCardinality(Nullable(String)), timezone LowCardinality(Nullable(String)),
duration UInt32, duration UInt32,

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,'mobile'=2) DEFAULT 'web', platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
datetime DateTime, datetime DateTime,
timezone LowCardinality(Nullable(String)), timezone LowCardinality(Nullable(String)),
duration UInt32, duration UInt32,