Merge branch 'dev' of https://github.com/openreplay/openreplay into e2e_tests_frontend
This commit is contained in:
commit
304b438154
185 changed files with 6108 additions and 2784 deletions
2
.github/workflows/alerts-ee.yaml
vendored
2
.github/workflows/alerts-ee.yaml
vendored
|
|
@ -130,7 +130,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,alerts,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,alerts,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/alerts.yaml
vendored
2
.github/workflows/alerts.yaml
vendored
|
|
@ -130,7 +130,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,alerts,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,alerts,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
|
||||||
2
.github/workflows/api-ee.yaml
vendored
2
.github/workflows/api-ee.yaml
vendored
|
|
@ -127,7 +127,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,chalice,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,chalice,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/api.yaml
vendored
2
.github/workflows/api.yaml
vendored
|
|
@ -120,7 +120,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,chalice,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,chalice,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
|
||||||
2
.github/workflows/assist-ee.yaml
vendored
2
.github/workflows/assist-ee.yaml
vendored
|
|
@ -113,7 +113,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,assist,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,assist,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/assist-server-ee.yaml
vendored
2
.github/workflows/assist-server-ee.yaml
vendored
|
|
@ -111,7 +111,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,assist-server,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,assist-server,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/assist-stats.yaml
vendored
2
.github/workflows/assist-stats.yaml
vendored
|
|
@ -130,7 +130,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,assist-stats,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,assist-stats,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/assist.yaml
vendored
2
.github/workflows/assist.yaml
vendored
|
|
@ -112,7 +112,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,assist,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,assist,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/crons-ee.yaml
vendored
2
.github/workflows/crons-ee.yaml
vendored
|
|
@ -129,7 +129,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,utilities,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,utilities,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||||
|
|
|
||||||
2
.github/workflows/frontend-dev.yaml
vendored
2
.github/workflows/frontend-dev.yaml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
|
||||||
4
.github/workflows/frontend.yaml
vendored
4
.github/workflows/frontend.yaml
vendored
|
|
@ -89,7 +89,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
@ -138,7 +138,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
|
||||||
18
.github/workflows/patch-build.yaml
vendored
18
.github/workflows/patch-build.yaml
vendored
|
|
@ -20,12 +20,20 @@ jobs:
|
||||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Rebase with main branch, to make sure the code has latest main changes
|
- name: Rebase with main branch, to make sure the code has latest main changes
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
git pull --rebase origin main
|
git remote -v
|
||||||
|
git config --global user.email "action@github.com"
|
||||||
|
git config --global user.name "GitHub Action"
|
||||||
|
git config --global rebase.autoStash true
|
||||||
|
git fetch origin main:main
|
||||||
|
git rebase main
|
||||||
|
git log -3
|
||||||
|
|
||||||
- name: Downloading yq
|
- name: Downloading yq
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -48,6 +56,8 @@ jobs:
|
||||||
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||||
|
|
||||||
- uses: depot/setup-action@v1
|
- uses: depot/setup-action@v1
|
||||||
|
env:
|
||||||
|
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||||
- name: Get HEAD Commit ID
|
- name: Get HEAD Commit ID
|
||||||
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
- name: Define Branch Name
|
- name: Define Branch Name
|
||||||
|
|
@ -100,7 +110,7 @@ jobs:
|
||||||
else
|
else
|
||||||
cd $MSAAS_REPO_FOLDER/openreplay/$service
|
cd $MSAAS_REPO_FOLDER/openreplay/$service
|
||||||
fi
|
fi
|
||||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt
|
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash $BUILD_SCRIPT_NAME >> /tmp/managed_${service}.txt 2>&1 || { echo "Build failed for $service"; cat /tmp/managed_${service}.txt; exit 1; }
|
||||||
}
|
}
|
||||||
# Checking for backend images
|
# Checking for backend images
|
||||||
ls backend/cmd >> /tmp/backend.txt
|
ls backend/cmd >> /tmp/backend.txt
|
||||||
|
|
|
||||||
2
.github/workflows/sourcemaps-reader-ee.yaml
vendored
2
.github/workflows/sourcemaps-reader-ee.yaml
vendored
|
|
@ -119,7 +119,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
|
||||||
2
.github/workflows/sourcemaps-reader.yaml
vendored
2
.github/workflows/sourcemaps-reader.yaml
vendored
|
|
@ -118,7 +118,7 @@ jobs:
|
||||||
cat /tmp/image_override.yaml
|
cat /tmp/image_override.yaml
|
||||||
# Deploy command
|
# Deploy command
|
||||||
mkdir -p /tmp/charts
|
mkdir -p /tmp/charts
|
||||||
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit,connector} /tmp/charts/
|
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit,connector,assist-api} /tmp/charts/
|
||||||
rm -rf openreplay/charts/*
|
rm -rf openreplay/charts/*
|
||||||
mv /tmp/charts/* openreplay/charts/
|
mv /tmp/charts/* openreplay/charts/
|
||||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||||
|
|
|
||||||
4
.github/workflows/workers-ee.yaml
vendored
4
.github/workflows/workers-ee.yaml
vendored
|
|
@ -148,9 +148,7 @@ jobs:
|
||||||
set -x
|
set -x
|
||||||
echo > /tmp/image_override.yaml
|
echo > /tmp/image_override.yaml
|
||||||
mkdir /tmp/helmcharts
|
mkdir /tmp/helmcharts
|
||||||
mv openreplay/charts/ingress-nginx /tmp/helmcharts/
|
mv openreplay/charts/{ingress-nginx,quickwit,connector,assist-api} /tmp/helmcharts/
|
||||||
mv openreplay/charts/quickwit /tmp/helmcharts/
|
|
||||||
mv openreplay/charts/connector /tmp/helmcharts/
|
|
||||||
## Update images
|
## Update images
|
||||||
for image in $(cat /tmp/images_to_build.txt);
|
for image in $(cat /tmp/images_to_build.txt);
|
||||||
do
|
do
|
||||||
|
|
|
||||||
4
.github/workflows/workers.yaml
vendored
4
.github/workflows/workers.yaml
vendored
|
|
@ -141,9 +141,7 @@ jobs:
|
||||||
set -x
|
set -x
|
||||||
echo > /tmp/image_override.yaml
|
echo > /tmp/image_override.yaml
|
||||||
mkdir /tmp/helmcharts
|
mkdir /tmp/helmcharts
|
||||||
mv openreplay/charts/ingress-nginx /tmp/helmcharts/
|
mv openreplay/charts/{ingress-nginx,quickwit,connector,assist-api} /tmp/helmcharts/
|
||||||
mv openreplay/charts/quickwit /tmp/helmcharts/
|
|
||||||
mv openreplay/charts/connector /tmp/helmcharts/
|
|
||||||
## Update images
|
## Update images
|
||||||
for image in $(cat /tmp/images_to_build.txt);
|
for image in $(cat /tmp/images_to_build.txt);
|
||||||
do
|
do
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
from chalicelib.utils import ch_client
|
from chalicelib.utils import ch_client
|
||||||
from .events_pg import *
|
from .events_pg import *
|
||||||
|
from chalicelib.utils.exp_ch_helper import explode_dproperties, add_timestamp
|
||||||
|
|
||||||
def __explode_properties(rows):
|
|
||||||
for i in range(len(rows)):
|
|
||||||
rows[i] = {**rows[i], **rows[i]["$properties"]}
|
|
||||||
rows[i].pop("$properties")
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def get_customs_by_session_id(session_id, project_id):
|
def get_customs_by_session_id(session_id, project_id):
|
||||||
with ch_client.ClickHouseClient() as cur:
|
with ch_client.ClickHouseClient() as cur:
|
||||||
rows = cur.execute(""" \
|
rows = cur.execute(""" \
|
||||||
SELECT `$properties`,
|
SELECT `$properties`,
|
||||||
|
properties,
|
||||||
created_at,
|
created_at,
|
||||||
'CUSTOM' AS type
|
'CUSTOM' AS type
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
|
|
@ -21,8 +16,10 @@ def get_customs_by_session_id(session_id, project_id):
|
||||||
AND `$event_name`!='INCIDENT'
|
AND `$event_name`!='INCIDENT'
|
||||||
ORDER BY created_at;""",
|
ORDER BY created_at;""",
|
||||||
{"project_id": project_id, "session_id": session_id})
|
{"project_id": project_id, "session_id": session_id})
|
||||||
rows = __explode_properties(rows)
|
rows = helper.list_to_camel_case(rows, ignore_keys=["properties"])
|
||||||
return helper.list_to_camel_case(rows)
|
rows = explode_dproperties(rows)
|
||||||
|
rows = add_timestamp(rows)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def __merge_cells(rows, start, count, replacement):
|
def __merge_cells(rows, start, count, replacement):
|
||||||
|
|
@ -69,12 +66,13 @@ def get_by_session_id(session_id, project_id, group_clickrage=False, event_type:
|
||||||
parameters={"project_id": project_id, "session_id": session_id,
|
parameters={"project_id": project_id, "session_id": session_id,
|
||||||
"select_events": select_events})
|
"select_events": select_events})
|
||||||
rows = cur.execute(query)
|
rows = cur.execute(query)
|
||||||
rows = __explode_properties(rows)
|
rows = explode_dproperties(rows)
|
||||||
if group_clickrage and 'CLICK' in select_events:
|
if group_clickrage and 'CLICK' in select_events:
|
||||||
rows = __get_grouped_clickrage(rows=rows, session_id=session_id, project_id=project_id)
|
rows = __get_grouped_clickrage(rows=rows, session_id=session_id, project_id=project_id)
|
||||||
|
|
||||||
rows = helper.list_to_camel_case(rows)
|
rows = helper.list_to_camel_case(rows)
|
||||||
rows = sorted(rows, key=lambda k: k["createdAt"])
|
rows = sorted(rows, key=lambda k: k["createdAt"])
|
||||||
|
rows = add_timestamp(rows)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -91,7 +89,7 @@ def get_incidents_by_session_id(session_id, project_id):
|
||||||
ORDER BY created_at;""",
|
ORDER BY created_at;""",
|
||||||
parameters={"project_id": project_id, "session_id": session_id})
|
parameters={"project_id": project_id, "session_id": session_id})
|
||||||
rows = cur.execute(query)
|
rows = cur.execute(query)
|
||||||
rows = __explode_properties(rows)
|
rows = explode_dproperties(rows)
|
||||||
rows = helper.list_to_camel_case(rows)
|
rows = helper.list_to_camel_case(rows)
|
||||||
rows = sorted(rows, key=lambda k: k["createdAt"])
|
rows = sorted(rows, key=lambda k: k["createdAt"])
|
||||||
return rows
|
return rows
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ def supported_types():
|
||||||
query=autocomplete.__generic_query(
|
query=autocomplete.__generic_query(
|
||||||
typename=schemas.EventType.GRAPHQL)),
|
typename=schemas.EventType.GRAPHQL)),
|
||||||
schemas.EventType.STATE_ACTION: SupportedFilter(
|
schemas.EventType.STATE_ACTION: SupportedFilter(
|
||||||
get=autocomplete.__generic_autocomplete(schemas.EventType.STATEACTION),
|
get=autocomplete.__generic_autocomplete(schemas.EventType.STATE_ACTION),
|
||||||
query=autocomplete.__generic_query(
|
query=autocomplete.__generic_query(
|
||||||
typename=schemas.EventType.STATE_ACTION)),
|
typename=schemas.EventType.STATE_ACTION)),
|
||||||
schemas.EventType.TAG: SupportedFilter(get=_search_tags, query=None),
|
schemas.EventType.TAG: SupportedFilter(get=_search_tags, query=None),
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
"""SELECT username, token, url
|
"""SELECT username, token, url
|
||||||
FROM public.jira_cloud
|
FROM public.jira_cloud
|
||||||
WHERE user_id=%(user_id)s;""",
|
WHERE user_id = %(user_id)s;""",
|
||||||
{"user_id": self._user_id})
|
{"user_id": self._user_id})
|
||||||
)
|
)
|
||||||
data = helper.dict_to_camel_case(cur.fetchone())
|
data = helper.dict_to_camel_case(cur.fetchone())
|
||||||
|
|
@ -95,10 +95,9 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
def add(self, username, token, url, obfuscate=False):
|
def add(self, username, token, url, obfuscate=False):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify("""\
|
cur.mogrify(""" \
|
||||||
INSERT INTO public.jira_cloud(username, token, user_id,url)
|
INSERT INTO public.jira_cloud(username, token, user_id, url)
|
||||||
VALUES (%(username)s, %(token)s, %(user_id)s,%(url)s)
|
VALUES (%(username)s, %(token)s, %(user_id)s, %(url)s) RETURNING username, token, url;""",
|
||||||
RETURNING username, token, url;""",
|
|
||||||
{"user_id": self._user_id, "username": username,
|
{"user_id": self._user_id, "username": username,
|
||||||
"token": token, "url": url})
|
"token": token, "url": url})
|
||||||
)
|
)
|
||||||
|
|
@ -112,9 +111,10 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
def delete(self):
|
def delete(self):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
cur.mogrify("""\
|
cur.mogrify(""" \
|
||||||
DELETE FROM public.jira_cloud
|
DELETE
|
||||||
WHERE user_id=%(user_id)s;""",
|
FROM public.jira_cloud
|
||||||
|
WHERE user_id = %(user_id)s;""",
|
||||||
{"user_id": self._user_id})
|
{"user_id": self._user_id})
|
||||||
)
|
)
|
||||||
return {"state": "success"}
|
return {"state": "success"}
|
||||||
|
|
@ -125,7 +125,7 @@ class JIRAIntegration(base.BaseIntegration):
|
||||||
changes={
|
changes={
|
||||||
"username": data.username,
|
"username": data.username,
|
||||||
"token": data.token if len(data.token) > 0 and data.token.find("***") == -1 \
|
"token": data.token if len(data.token) > 0 and data.token.find("***") == -1 \
|
||||||
else self.integration.token,
|
else self.integration["token"],
|
||||||
"url": str(data.url)
|
"url": str(data.url)
|
||||||
},
|
},
|
||||||
obfuscate=True
|
obfuscate=True
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from chalicelib.utils import ch_client, helper
|
from chalicelib.utils import ch_client, helper
|
||||||
import datetime
|
import datetime
|
||||||
from .issues_pg import get_all_types
|
from chalicelib.utils.exp_ch_helper import explode_dproperties, add_timestamp
|
||||||
|
|
||||||
|
|
||||||
def get(project_id, issue_id):
|
def get(project_id, issue_id):
|
||||||
|
|
@ -21,7 +21,7 @@ def get(project_id, issue_id):
|
||||||
def get_by_session_id(session_id, project_id, issue_type=None):
|
def get_by_session_id(session_id, project_id, issue_type=None):
|
||||||
with ch_client.ClickHouseClient() as cur:
|
with ch_client.ClickHouseClient() as cur:
|
||||||
query = cur.format(query=f"""\
|
query = cur.format(query=f"""\
|
||||||
SELECT *
|
SELECT created_at, `$properties`
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
WHERE session_id = %(session_id)s
|
WHERE session_id = %(session_id)s
|
||||||
AND project_id= %(project_id)s
|
AND project_id= %(project_id)s
|
||||||
|
|
@ -29,8 +29,11 @@ def get_by_session_id(session_id, project_id, issue_type=None):
|
||||||
{"AND issue_type = %(type)s" if issue_type is not None else ""}
|
{"AND issue_type = %(type)s" if issue_type is not None else ""}
|
||||||
ORDER BY created_at;""",
|
ORDER BY created_at;""",
|
||||||
parameters={"session_id": session_id, "project_id": project_id, "type": issue_type})
|
parameters={"session_id": session_id, "project_id": project_id, "type": issue_type})
|
||||||
data = cur.execute(query)
|
rows = cur.execute(query)
|
||||||
return helper.list_to_camel_case(data)
|
rows = explode_dproperties(rows)
|
||||||
|
rows = helper.list_to_camel_case(rows)
|
||||||
|
rows = add_timestamp(rows)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
# To reduce the number of issues in the replay;
|
# To reduce the number of issues in the replay;
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,5 @@ def get_for_filters(project_id):
|
||||||
"name": k,
|
"name": k,
|
||||||
"displayName": metas[k],
|
"displayName": metas[k],
|
||||||
"possibleTypes": ["String"],
|
"possibleTypes": ["String"],
|
||||||
"autoCaptured": False,
|
"autoCaptured": False})
|
||||||
"icon": None})
|
|
||||||
return {"total": len(results), "list": results}
|
return {"total": len(results), "list": results}
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,8 @@ def get_sessions_by_card_id(project: schemas.ProjectContext, user_id, metric_id,
|
||||||
results = []
|
results = []
|
||||||
for s in data.series:
|
for s in data.series:
|
||||||
results.append({"seriesId": s.series_id, "seriesName": s.name,
|
results.append({"seriesId": s.series_id, "seriesName": s.name,
|
||||||
**sessions_search.search_sessions(data=s.filter, project=project, user_id=user_id)})
|
**sessions_search.search_sessions(data=s.filter, project=project, user_id=user_id,
|
||||||
|
metric_of=data.metric_of)})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
@ -187,7 +188,8 @@ def get_sessions(project: schemas.ProjectContext, user_id, data: schemas.CardSes
|
||||||
s.filter = schemas.SessionsSearchPayloadSchema(**s.filter.model_dump(by_alias=True))
|
s.filter = schemas.SessionsSearchPayloadSchema(**s.filter.model_dump(by_alias=True))
|
||||||
|
|
||||||
results.append({"seriesId": None, "seriesName": s.name,
|
results.append({"seriesId": None, "seriesName": s.name,
|
||||||
**sessions_search.search_sessions(data=s.filter, project=project, user_id=user_id)})
|
**sessions_search.search_sessions(data=s.filter, project=project, user_id=user_id,
|
||||||
|
metric_of=data.metric_of)})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,32 +28,32 @@ def search_events(project_id: int, q: Optional[str] = None):
|
||||||
def search_properties(project_id: int, property_name: Optional[str] = None, event_name: Optional[str] = None,
|
def search_properties(project_id: int, property_name: Optional[str] = None, event_name: Optional[str] = None,
|
||||||
q: Optional[str] = None):
|
q: Optional[str] = None):
|
||||||
with ClickHouseClient() as ch_client:
|
with ClickHouseClient() as ch_client:
|
||||||
select = "value"
|
select = "value, data_count"
|
||||||
|
grouping = ""
|
||||||
full_args = {"project_id": project_id, "limit": 20,
|
full_args = {"project_id": project_id, "limit": 20,
|
||||||
"event_name": event_name, "property_name": property_name, "q": q,
|
"event_name": event_name, "property_name": property_name,
|
||||||
"property_name_l": helper.string_to_sql_like(property_name),
|
|
||||||
"q_l": helper.string_to_sql_like(q)}
|
"q_l": helper.string_to_sql_like(q)}
|
||||||
|
|
||||||
constraints = ["project_id = %(project_id)s",
|
constraints = ["project_id = %(project_id)s",
|
||||||
"_timestamp >= now()-INTERVAL 1 MONTH"]
|
"_timestamp >= now()-INTERVAL 1 MONTH",
|
||||||
|
"property_name = %(property_name)s"]
|
||||||
if event_name:
|
if event_name:
|
||||||
constraints += ["event_name = %(event_name)s"]
|
constraints += ["event_name = %(event_name)s"]
|
||||||
|
else:
|
||||||
if property_name and q:
|
select = "value, sum(aepg.data_count) AS data_count"
|
||||||
constraints += ["property_name = %(property_name)s"]
|
grouping = "GROUP BY 1"
|
||||||
elif property_name:
|
|
||||||
select = "DISTINCT ON(property_name) property_name AS value"
|
|
||||||
constraints += ["property_name ILIKE %(property_name_l)s"]
|
|
||||||
|
|
||||||
if q:
|
if q:
|
||||||
constraints += ["value ILIKE %(q_l)s"]
|
constraints += ["value ILIKE %(q_l)s"]
|
||||||
|
|
||||||
query = ch_client.format(
|
query = ch_client.format(
|
||||||
f"""SELECT {select},data_count
|
f"""SELECT {select}
|
||||||
FROM product_analytics.autocomplete_event_properties_grouped
|
FROM product_analytics.autocomplete_event_properties_grouped AS aepg
|
||||||
WHERE {" AND ".join(constraints)}
|
WHERE {" AND ".join(constraints)}
|
||||||
ORDER BY data_count DESC
|
{grouping}
|
||||||
|
ORDER BY data_count DESC
|
||||||
LIMIT %(limit)s;""",
|
LIMIT %(limit)s;""",
|
||||||
parameters=full_args)
|
parameters=full_args)
|
||||||
rows = ch_client.execute(query)
|
rows = ch_client.execute(query)
|
||||||
|
|
||||||
return {"values": helper.list_to_camel_case(rows), "_src": 2}
|
return {"events": helper.list_to_camel_case(rows), "_src": 2}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,13 @@ from chalicelib.utils.ch_client import ClickHouseClient
|
||||||
from chalicelib.utils.exp_ch_helper import get_sub_condition, get_col_cast
|
from chalicelib.utils.exp_ch_helper import get_sub_condition, get_col_cast
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
PREDEFINED_EVENTS = {
|
PREDEFINED_EVENTS = [
|
||||||
"CLICK": "String",
|
"CLICK",
|
||||||
"INPUT": "String",
|
"INPUT",
|
||||||
"LOCATION": "String",
|
"LOCATION",
|
||||||
"ERROR": "String",
|
"ERROR",
|
||||||
"PERFORMANCE": "String",
|
"REQUEST"
|
||||||
"REQUEST": "String"
|
]
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_events(project_id: int, page: schemas.PaginatedSchema):
|
def get_events(project_id: int, page: schemas.PaginatedSchema):
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,14 @@ PREDEFINED_PROPERTIES = {
|
||||||
"message_id": "UInt64"
|
"message_id": "UInt64"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EVENT_DEFAULT_PROPERTIES = {
|
||||||
|
"CLICK": "label",
|
||||||
|
"INPUT": "label",
|
||||||
|
"LOCATION": "url_path",
|
||||||
|
"ERROR": "name",
|
||||||
|
"REQUEST": "url_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
|
def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
|
||||||
with ClickHouseClient() as ch_client:
|
with ClickHouseClient() as ch_client:
|
||||||
|
|
@ -104,7 +112,7 @@ def get_all_properties(project_id: int, page: schemas.PaginatedSchema):
|
||||||
return {"total": total, "list": properties}
|
return {"total": total, "list": properties}
|
||||||
|
|
||||||
|
|
||||||
def get_event_properties(project_id: int, event_name):
|
def get_event_properties(project_id: int, event_name: str, auto_captured: bool):
|
||||||
with ClickHouseClient() as ch_client:
|
with ClickHouseClient() as ch_client:
|
||||||
r = ch_client.format(
|
r = ch_client.format(
|
||||||
"""SELECT all_properties.property_name AS name,
|
"""SELECT all_properties.property_name AS name,
|
||||||
|
|
@ -115,9 +123,10 @@ def get_event_properties(project_id: int, event_name):
|
||||||
WHERE event_properties.project_id = %(project_id)s
|
WHERE event_properties.project_id = %(project_id)s
|
||||||
AND all_properties.project_id = %(project_id)s
|
AND all_properties.project_id = %(project_id)s
|
||||||
AND event_properties.event_name = %(event_name)s
|
AND event_properties.event_name = %(event_name)s
|
||||||
|
AND event_properties.auto_captured = %(auto_captured)s
|
||||||
GROUP BY ALL
|
GROUP BY ALL
|
||||||
ORDER BY 1;""",
|
ORDER BY 1;""",
|
||||||
parameters={"project_id": project_id, "event_name": event_name})
|
parameters={"project_id": project_id, "event_name": event_name, "auto_captured": auto_captured})
|
||||||
properties = ch_client.execute(r)
|
properties = ch_client.execute(r)
|
||||||
properties = helper.list_to_camel_case(properties)
|
properties = helper.list_to_camel_case(properties)
|
||||||
for i, p in enumerate(properties):
|
for i, p in enumerate(properties):
|
||||||
|
|
@ -127,6 +136,8 @@ def get_event_properties(project_id: int, event_name):
|
||||||
p["dataType"] = exp_ch_helper.simplify_clickhouse_type(PREDEFINED_PROPERTIES[p["name"]])
|
p["dataType"] = exp_ch_helper.simplify_clickhouse_type(PREDEFINED_PROPERTIES[p["name"]])
|
||||||
p["_foundInPredefinedList"] = True
|
p["_foundInPredefinedList"] = True
|
||||||
p["possibleTypes"] = list(set(exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])))
|
p["possibleTypes"] = list(set(exp_ch_helper.simplify_clickhouse_types(p["possibleTypes"])))
|
||||||
|
p["defaultProperty"] = auto_captured and event_name in EVENT_DEFAULT_PROPERTIES \
|
||||||
|
and p["name"] == EVENT_DEFAULT_PROPERTIES[event_name]
|
||||||
|
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
for e in data.events:
|
for e in data.events:
|
||||||
if e.type == schemas.EventType.LOCATION:
|
if e.type == schemas.EventType.LOCATION:
|
||||||
if e.operator not in extra_conditions:
|
if e.operator not in extra_conditions:
|
||||||
extra_conditions[e.operator] = schemas.SessionSearchEventSchema.model_validate({
|
extra_conditions[e.operator] = schemas.SessionSearchEventSchema(**{
|
||||||
"type": e.type,
|
"type": e.type,
|
||||||
"isEvent": True,
|
"isEvent": True,
|
||||||
"value": [],
|
"value": [],
|
||||||
|
|
@ -175,7 +175,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
for e in data.events:
|
for e in data.events:
|
||||||
if e.type == schemas.EventType.REQUEST_DETAILS:
|
if e.type == schemas.EventType.REQUEST_DETAILS:
|
||||||
if e.operator not in extra_conditions:
|
if e.operator not in extra_conditions:
|
||||||
extra_conditions[e.operator] = schemas.SessionSearchEventSchema.model_validate({
|
extra_conditions[e.operator] = schemas.SessionSearchEventSchema(**{
|
||||||
"type": e.type,
|
"type": e.type,
|
||||||
"isEvent": True,
|
"isEvent": True,
|
||||||
"value": [],
|
"value": [],
|
||||||
|
|
@ -240,8 +240,10 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
||||||
{main_col} AS name,
|
{main_col} AS name,
|
||||||
count(DISTINCT session_id) AS total,
|
count(DISTINCT session_id) AS total,
|
||||||
COALESCE(SUM(count(DISTINCT session_id)) OVER (), 0) AS total_count
|
any(total_count) as total_count
|
||||||
FROM (SELECT s.session_id AS session_id {extra_col}
|
FROM (SELECT s.session_id AS session_id,
|
||||||
|
count(DISTINCT s.session_id) OVER () AS total_count
|
||||||
|
{extra_col}
|
||||||
{query_part}) AS filtred_sessions
|
{query_part}) AS filtred_sessions
|
||||||
{extra_where}
|
{extra_where}
|
||||||
GROUP BY {main_col}
|
GROUP BY {main_col}
|
||||||
|
|
@ -251,8 +253,10 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
|
||||||
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
main_query = f"""SELECT COUNT(DISTINCT {main_col}) OVER () AS main_count,
|
||||||
{main_col} AS name,
|
{main_col} AS name,
|
||||||
count(DISTINCT user_id) AS total,
|
count(DISTINCT user_id) AS total,
|
||||||
COALESCE(SUM(count(DISTINCT user_id)) OVER (), 0) AS total_count
|
any(total_count) AS total_count
|
||||||
FROM (SELECT s.user_id AS user_id {extra_col}
|
FROM (SELECT s.user_id AS user_id,
|
||||||
|
count(DISTINCT s.user_id) OVER () AS total_count
|
||||||
|
{extra_col}
|
||||||
{query_part}
|
{query_part}
|
||||||
WHERE isNotNull(user_id)
|
WHERE isNotNull(user_id)
|
||||||
AND notEmpty(user_id)) AS filtred_sessions
|
AND notEmpty(user_id)) AS filtred_sessions
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,7 @@ def __parse_metadata(metadata_map):
|
||||||
# This function executes the query and return result
|
# This function executes the query and return result
|
||||||
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.ProjectContext,
|
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.ProjectContext,
|
||||||
user_id, errors_only=False, error_status=schemas.ErrorStatus.ALL,
|
user_id, errors_only=False, error_status=schemas.ErrorStatus.ALL,
|
||||||
count_only=False, issue=None, ids_only=False):
|
count_only=False, issue=None, ids_only=False, metric_of: schemas.MetricOfTable = None):
|
||||||
platform = project.platform
|
|
||||||
if data.bookmarked:
|
if data.bookmarked:
|
||||||
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project.project_id, user_id)
|
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project.project_id, user_id)
|
||||||
if data.startTimestamp is None:
|
if data.startTimestamp is None:
|
||||||
|
|
@ -75,18 +74,78 @@ def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.
|
||||||
'sessions': [],
|
'sessions': [],
|
||||||
'_src': 2
|
'_src': 2
|
||||||
}
|
}
|
||||||
|
# ---------------------- extra filter in order to only select sessions that has been used in the card-table
|
||||||
|
extra_event = None
|
||||||
|
# extra_deduplication = []
|
||||||
|
extra_conditions = None
|
||||||
|
if metric_of == schemas.MetricOfTable.VISITED_URL:
|
||||||
|
extra_event = f"""SELECT DISTINCT ev.session_id,
|
||||||
|
JSONExtractString(toString(ev.`$properties`), 'url_path') AS url_path
|
||||||
|
FROM {exp_ch_helper.get_main_events_table(data.startTimestamp)} AS ev
|
||||||
|
WHERE ev.created_at >= toDateTime(%(startDate)s / 1000)
|
||||||
|
AND ev.created_at <= toDateTime(%(endDate)s / 1000)
|
||||||
|
AND ev.project_id = %(project_id)s
|
||||||
|
AND ev.`$event_name` = 'LOCATION'"""
|
||||||
|
# extra_deduplication.append("url_path")
|
||||||
|
extra_conditions = {}
|
||||||
|
for e in data.events:
|
||||||
|
if e.type == schemas.EventType.LOCATION:
|
||||||
|
if e.operator not in extra_conditions:
|
||||||
|
extra_conditions[e.operator] = schemas.SessionSearchEventSchema(**{
|
||||||
|
"type": e.type,
|
||||||
|
"isEvent": True,
|
||||||
|
"value": [],
|
||||||
|
"operator": e.operator,
|
||||||
|
"filters": e.filters
|
||||||
|
})
|
||||||
|
for v in e.value:
|
||||||
|
if v not in extra_conditions[e.operator].value:
|
||||||
|
extra_conditions[e.operator].value.append(v)
|
||||||
|
extra_conditions = list(extra_conditions.values())
|
||||||
|
elif metric_of == schemas.MetricOfTable.FETCH:
|
||||||
|
extra_event = f"""SELECT DISTINCT ev.session_id
|
||||||
|
FROM {exp_ch_helper.get_main_events_table(data.startTimestamp)} AS ev
|
||||||
|
WHERE ev.created_at >= toDateTime(%(startDate)s / 1000)
|
||||||
|
AND ev.created_at <= toDateTime(%(endDate)s / 1000)
|
||||||
|
AND ev.project_id = %(project_id)s
|
||||||
|
AND ev.`$event_name` = 'REQUEST'"""
|
||||||
|
|
||||||
|
# extra_deduplication.append("url_path")
|
||||||
|
extra_conditions = {}
|
||||||
|
for e in data.events:
|
||||||
|
if e.type == schemas.EventType.REQUEST_DETAILS:
|
||||||
|
if e.operator not in extra_conditions:
|
||||||
|
extra_conditions[e.operator] = schemas.SessionSearchEventSchema(**{
|
||||||
|
"type": e.type,
|
||||||
|
"isEvent": True,
|
||||||
|
"value": [],
|
||||||
|
"operator": e.operator,
|
||||||
|
"filters": e.filters
|
||||||
|
})
|
||||||
|
for v in e.value:
|
||||||
|
if v not in extra_conditions[e.operator].value:
|
||||||
|
extra_conditions[e.operator].value.append(v)
|
||||||
|
extra_conditions = list(extra_conditions.values())
|
||||||
|
|
||||||
|
# elif metric_of == schemas.MetricOfTable.ISSUES and len(metric_value) > 0:
|
||||||
|
# data.filters.append(schemas.SessionSearchFilterSchema(value=metric_value, type=schemas.FilterType.ISSUE,
|
||||||
|
# operator=schemas.SearchEventOperator.IS))
|
||||||
|
# ----------------------
|
||||||
if project.platform == "web":
|
if project.platform == "web":
|
||||||
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=error_status,
|
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=error_status,
|
||||||
errors_only=errors_only,
|
errors_only=errors_only,
|
||||||
favorite_only=data.bookmarked, issue=issue,
|
favorite_only=data.bookmarked, issue=issue,
|
||||||
project_id=project.project_id,
|
project_id=project.project_id,
|
||||||
user_id=user_id, platform=platform)
|
user_id=user_id, platform=project.platform,
|
||||||
|
extra_event=extra_event,
|
||||||
|
# extra_deduplication=extra_deduplication,
|
||||||
|
extra_conditions=extra_conditions)
|
||||||
else:
|
else:
|
||||||
full_args, query_part = sessions_legacy_mobil.search_query_parts_ch(data=data, error_status=error_status,
|
full_args, query_part = sessions_legacy_mobil.search_query_parts_ch(data=data, error_status=error_status,
|
||||||
errors_only=errors_only,
|
errors_only=errors_only,
|
||||||
favorite_only=data.bookmarked, issue=issue,
|
favorite_only=data.bookmarked, issue=issue,
|
||||||
project_id=project.project_id,
|
project_id=project.project_id,
|
||||||
user_id=user_id, platform=platform)
|
user_id=user_id, platform=project.platform)
|
||||||
if data.sort == "startTs":
|
if data.sort == "startTs":
|
||||||
data.sort = "datetime"
|
data.sort = "datetime"
|
||||||
if data.limit is not None and data.page is not None:
|
if data.limit is not None and data.page is not None:
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ COALESCE((SELECT TRUE
|
||||||
# This function executes the query and return result
|
# This function executes the query and return result
|
||||||
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.ProjectContext,
|
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project: schemas.ProjectContext,
|
||||||
user_id, errors_only=False, error_status=schemas.ErrorStatus.ALL,
|
user_id, errors_only=False, error_status=schemas.ErrorStatus.ALL,
|
||||||
count_only=False, issue=None, ids_only=False):
|
count_only=False, issue=None, ids_only=False, metric_of: schemas.MetricOfTable = None):
|
||||||
platform = project.platform
|
platform = project.platform
|
||||||
if data.bookmarked:
|
if data.bookmarked:
|
||||||
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project.project_id, user_id)
|
data.startTimestamp, data.endTimestamp = sessions_favorite.get_start_end_timestamp(project.project_id, user_id)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
|
import struct
|
||||||
|
from decimal import Decimal
|
||||||
from typing import Union, Any
|
from typing import Union, Any
|
||||||
|
|
||||||
import schemas
|
import schemas
|
||||||
from chalicelib.utils import sql_helper as sh
|
from chalicelib.utils import sql_helper as sh
|
||||||
|
from chalicelib.utils.TimeUTC import TimeUTC
|
||||||
from schemas import SearchEventOperator
|
from schemas import SearchEventOperator
|
||||||
import math
|
|
||||||
import struct
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -233,3 +234,16 @@ def best_clickhouse_type(value):
|
||||||
return "Float64"
|
return "Float64"
|
||||||
|
|
||||||
raise TypeError(f"Unsupported type: {type(value).__name__}")
|
raise TypeError(f"Unsupported type: {type(value).__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
def explode_dproperties(rows):
|
||||||
|
for i in range(len(rows)):
|
||||||
|
rows[i] = {**rows[i], **rows[i]["$properties"]}
|
||||||
|
rows[i].pop("$properties")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def add_timestamp(rows):
|
||||||
|
for row in rows:
|
||||||
|
row["timestamp"] = TimeUTC.datetime_to_timestamp(row["createdAt"])
|
||||||
|
return rows
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ def random_string(length=36):
|
||||||
return "".join(random.choices(string.hexdigits, k=length))
|
return "".join(random.choices(string.hexdigits, k=length))
|
||||||
|
|
||||||
|
|
||||||
def list_to_camel_case(items: list[dict], flatten: bool = False) -> list[dict]:
|
def list_to_camel_case(items: list[dict], flatten: bool = False, ignore_keys=[]) -> list[dict]:
|
||||||
for i in range(len(items)):
|
for i in range(len(items)):
|
||||||
if flatten:
|
if flatten:
|
||||||
items[i] = flatten_nested_dicts(items[i])
|
items[i] = flatten_nested_dicts(items[i])
|
||||||
items[i] = dict_to_camel_case(items[i])
|
items[i] = dict_to_camel_case(items[i], ignore_keys=[])
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ from decouple import config
|
||||||
from fastapi import Depends, Body, BackgroundTasks
|
from fastapi import Depends, Body, BackgroundTasks
|
||||||
|
|
||||||
import schemas
|
import schemas
|
||||||
from chalicelib.core import events, projects, metadata, reset_password, log_tools, \
|
from chalicelib.core import projects, metadata, reset_password, log_tools, \
|
||||||
announcements, weekly_report, assist, mobile, tenants, boarding, notifications, webhook, users, saved_search, tags
|
announcements, weekly_report, assist, mobile, tenants, boarding, notifications, webhook, users, saved_search, tags
|
||||||
|
from chalicelib.core.events import events
|
||||||
from chalicelib.core.issues import issues
|
from chalicelib.core.issues import issues
|
||||||
from chalicelib.core.sourcemaps import sourcemaps
|
from chalicelib.core.sourcemaps import sourcemaps
|
||||||
from chalicelib.core.metrics import custom_metrics
|
from chalicelib.core.metrics import custom_metrics
|
||||||
|
|
|
||||||
|
|
@ -220,9 +220,9 @@ def get_card_chart(projectId: int, metric_id: int, data: schemas.CardSessionsSch
|
||||||
|
|
||||||
|
|
||||||
@app.post("/{projectId}/dashboards/{dashboardId}/cards/{metric_id}/chart", tags=["card"])
|
@app.post("/{projectId}/dashboards/{dashboardId}/cards/{metric_id}/chart", tags=["card"])
|
||||||
@app.post("/{projectId}/dashboards/{dashboardId}/cards/{metric_id}", tags=["card"])
|
# @app.post("/{projectId}/dashboards/{dashboardId}/cards/{metric_id}", tags=["card"])
|
||||||
def get_card_chart_for_dashboard(projectId: int, dashboardId: int, metric_id: int,
|
def get_card_chart_for_dashboard(projectId: int, dashboardId: int, metric_id: int,
|
||||||
data: schemas.CardSessionsSchema = Body(...),
|
data: schemas.SavedCardSchema = Body(...),
|
||||||
context: schemas.CurrentContext = Depends(OR_context)):
|
context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
data = custom_metrics.make_chart_from_card(
|
data = custom_metrics.make_chart_from_card(
|
||||||
project=context.project, user_id=context.user_id, metric_id=metric_id, data=data, for_dashboard=True
|
project=context.project, user_id=context.user_id, metric_id=metric_id, data=data, for_dashboard=True
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ public_app, app, app_apikey = get_routers()
|
||||||
@app.get('/{projectId}/filters', tags=["product_analytics"])
|
@app.get('/{projectId}/filters', tags=["product_analytics"])
|
||||||
def get_all_filters(projectId: int, filter_query: Annotated[schemas.PaginatedSchema, Query()],
|
def get_all_filters(projectId: int, filter_query: Annotated[schemas.PaginatedSchema, Query()],
|
||||||
context: schemas.CurrentContext = Depends(OR_context)):
|
context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
|
# TODO: fix total attribute to return the total count instead of the total number of pages
|
||||||
|
# TODO: no pagination, return everything
|
||||||
|
# TODO: remove icon
|
||||||
return {
|
return {
|
||||||
"data": {
|
"data": {
|
||||||
"events": events.get_events(project_id=projectId, page=filter_query),
|
"events": events.get_events(project_id=projectId, page=filter_query),
|
||||||
|
|
@ -31,11 +34,12 @@ def get_all_events(projectId: int, filter_query: Annotated[schemas.PaginatedSche
|
||||||
|
|
||||||
|
|
||||||
@app.get('/{projectId}/properties/search', tags=["product_analytics"])
|
@app.get('/{projectId}/properties/search', tags=["product_analytics"])
|
||||||
def get_event_properties(projectId: int, event_name: str = None,
|
def get_event_properties(projectId: int, en: str = Query(default=None, description="event name"),
|
||||||
|
ac: bool = Query(description="auto captured"),
|
||||||
context: schemas.CurrentContext = Depends(OR_context)):
|
context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
if not event_name or len(event_name) == 0:
|
if not en or len(en) == 0:
|
||||||
return {"data": []}
|
return {"data": []}
|
||||||
return {"data": properties.get_event_properties(project_id=projectId, event_name=event_name)}
|
return {"data": properties.get_event_properties(project_id=projectId, event_name=en, auto_captured=ac)}
|
||||||
|
|
||||||
|
|
||||||
@app.post('/{projectId}/events/search', tags=["product_analytics"])
|
@app.post('/{projectId}/events/search', tags=["product_analytics"])
|
||||||
|
|
@ -63,15 +67,12 @@ def autocomplete_events(projectId: int, q: Optional[str] = None,
|
||||||
|
|
||||||
|
|
||||||
@app.get('/{projectId}/properties/autocomplete', tags=["autocomplete"])
|
@app.get('/{projectId}/properties/autocomplete', tags=["autocomplete"])
|
||||||
def autocomplete_properties(projectId: int, propertyName: Optional[str] = None, eventName: Optional[str] = None,
|
def autocomplete_properties(projectId: int, propertyName: str, eventName: Optional[str] = None,
|
||||||
q: Optional[str] = None, context: schemas.CurrentContext = Depends(OR_context)):
|
q: Optional[str] = None, context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
if not propertyName and not eventName and not q:
|
# Specify propertyName to get top values of that property
|
||||||
return {"error": ["Specify eventName to get top properties",
|
# Specify eventName&propertyName to get top values of that property for the selected event
|
||||||
"Specify propertyName to get top values of that property",
|
|
||||||
"Specify eventName&propertyName to get top values of that property for the selected event"]}
|
|
||||||
return {"data": autocomplete.search_properties(project_id=projectId,
|
return {"data": autocomplete.search_properties(project_id=projectId,
|
||||||
event_name=None if not eventName \
|
event_name=None if not eventName \
|
||||||
or len(eventName) == 0 else eventName,
|
or len(eventName) == 0 else eventName,
|
||||||
property_name=None if not propertyName \
|
property_name=propertyName,
|
||||||
or len(propertyName) == 0 else propertyName,
|
|
||||||
q=None if not q or len(q) == 0 else q)}
|
q=None if not q or len(q) == 0 else q)}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ def schema_extra(schema: dict, _):
|
||||||
class BaseModel(_BaseModel):
|
class BaseModel(_BaseModel):
|
||||||
model_config = ConfigDict(alias_generator=attribute_to_camel_case,
|
model_config = ConfigDict(alias_generator=attribute_to_camel_case,
|
||||||
use_enum_values=True,
|
use_enum_values=True,
|
||||||
json_schema_extra=schema_extra)
|
json_schema_extra=schema_extra,
|
||||||
|
# extra='forbid'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Enum(_Enum):
|
class Enum(_Enum):
|
||||||
|
|
|
||||||
|
|
@ -1043,11 +1043,16 @@ class MetricOfPathAnalysis(str, Enum):
|
||||||
session_count = MetricOfTimeseries.SESSION_COUNT.value
|
session_count = MetricOfTimeseries.SESSION_COUNT.value
|
||||||
|
|
||||||
|
|
||||||
# class CardSessionsSchema(SessionsSearchPayloadSchema):
|
|
||||||
class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
|
class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
|
||||||
startTimestamp: int = Field(default=TimeUTC.now(-7))
|
startTimestamp: int = Field(default=TimeUTC.now(-7))
|
||||||
endTimestamp: int = Field(default=TimeUTC.now())
|
endTimestamp: int = Field(default=TimeUTC.now())
|
||||||
density: int = Field(default=7, ge=1, le=200)
|
density: int = Field(default=7, ge=1, le=200)
|
||||||
|
# we need metric_type&metric_of in the payload of sessions search
|
||||||
|
# because the API will retrun all sessions if the card is not identified
|
||||||
|
# example: table of requests contains only sessions that have a request,
|
||||||
|
# but drill-down doesn't take that into consideration
|
||||||
|
metric_type: MetricType = Field(...)
|
||||||
|
metric_of: Any
|
||||||
series: List[CardSeriesSchema] = Field(default_factory=list)
|
series: List[CardSeriesSchema] = Field(default_factory=list)
|
||||||
|
|
||||||
# events: List[SessionSearchEventSchema2] = Field(default_factory=list, doc_hidden=True)
|
# events: List[SessionSearchEventSchema2] = Field(default_factory=list, doc_hidden=True)
|
||||||
|
|
@ -1112,6 +1117,11 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class SavedCardSchema(CardSessionsSchema):
|
||||||
|
metric_type: Optional[MetricType] = Field(default=None)
|
||||||
|
metric_of: Optional[Any] = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class CardConfigSchema(BaseModel):
|
class CardConfigSchema(BaseModel):
|
||||||
col: Optional[int] = Field(default=None)
|
col: Optional[int] = Field(default=None)
|
||||||
row: Optional[int] = Field(default=2)
|
row: Optional[int] = Field(default=2)
|
||||||
|
|
@ -1125,8 +1135,6 @@ class __CardSchema(CardSessionsSchema):
|
||||||
thumbnail: Optional[str] = Field(default=None)
|
thumbnail: Optional[str] = Field(default=None)
|
||||||
metric_format: Optional[MetricFormatType] = Field(default=None)
|
metric_format: Optional[MetricFormatType] = Field(default=None)
|
||||||
view_type: Any
|
view_type: Any
|
||||||
metric_type: MetricType = Field(...)
|
|
||||||
metric_of: Any
|
|
||||||
metric_value: List[IssueType] = Field(default_factory=list)
|
metric_value: List[IssueType] = Field(default_factory=list)
|
||||||
# This is used to save the selected session for heatmaps
|
# This is used to save the selected session for heatmaps
|
||||||
session_id: Optional[int] = Field(default=None)
|
session_id: Optional[int] = Field(default=None)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ func main() {
|
||||||
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
|
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
|
||||||
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
||||||
messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange,
|
messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange,
|
||||||
messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger,
|
messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger, messages.MsgIncident,
|
||||||
// Mobile messages
|
// Mobile messages
|
||||||
messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID,
|
messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID,
|
||||||
messages.MsgMobileMetadata, messages.MsgMobileEvent, messages.MsgMobileNetworkCall,
|
messages.MsgMobileMetadata, messages.MsgMobileEvent, messages.MsgMobileNetworkCall,
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,11 @@ func (s *saverImpl) handleWebMessage(sessCtx context.Context, session *sessions.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.ch.InsertWebPerformanceTrackAggr(session, m)
|
return s.ch.InsertWebPerformanceTrackAggr(session, m)
|
||||||
|
case *messages.Incident:
|
||||||
|
if err := s.pg.InsertIncident(session, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.ch.InsertIncident(session, m)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ type Connector interface {
|
||||||
InsertIssue(session *sessions.Session, msg *messages.IssueEvent) error
|
InsertIssue(session *sessions.Session, msg *messages.IssueEvent) error
|
||||||
InsertWebInputDuration(session *sessions.Session, msg *messages.InputChange) error
|
InsertWebInputDuration(session *sessions.Session, msg *messages.InputChange) error
|
||||||
InsertMouseThrashing(session *sessions.Session, msg *messages.MouseThrashing) error
|
InsertMouseThrashing(session *sessions.Session, msg *messages.MouseThrashing) error
|
||||||
|
InsertIncident(session *sessions.Session, msg *messages.Incident) error
|
||||||
InsertMobileSession(session *sessions.Session) error
|
InsertMobileSession(session *sessions.Session) error
|
||||||
InsertMobileCustom(session *sessions.Session, msg *messages.MobileEvent) error
|
InsertMobileCustom(session *sessions.Session, msg *messages.MobileEvent) error
|
||||||
InsertMobileClick(session *sessions.Session, msg *messages.MobileClickEvent) error
|
InsertMobileClick(session *sessions.Session, msg *messages.MobileClickEvent) error
|
||||||
|
|
@ -106,15 +107,15 @@ func (c *connectorImpl) newBatch(name, query string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var batches = map[string]string{
|
var batches = map[string]string{
|
||||||
"sessions": "INSERT INTO experimental.sessions (session_id, project_id, user_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, user_state, user_city, datetime, duration, pages_count, events_count, errors_count, issue_score, referrer, issue_types, tracker_version, user_browser, user_browser_version, metadata_1, metadata_2, metadata_3, metadata_4, metadata_5, metadata_6, metadata_7, metadata_8, metadata_9, metadata_10, platform, timezone, utm_source, utm_medium, utm_campaign) VALUES (?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?)",
|
"sessions": "INSERT INTO experimental.sessions (session_id, project_id, user_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, user_state, user_city, datetime, duration, pages_count, events_count, errors_count, issue_score, referrer, issue_types, tracker_version, user_browser, user_browser_version, metadata_1, metadata_2, metadata_3, metadata_4, metadata_5, metadata_6, metadata_7, metadata_8, metadata_9, metadata_10, platform, timezone, utm_source, utm_medium, utm_campaign, screen_width, screen_height) VALUES (?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?)",
|
||||||
"autocompletes": "INSERT INTO experimental.autocomplete (project_id, type, value) VALUES (?, ?, SUBSTR(?, 1, 8000))",
|
"autocompletes": "INSERT INTO experimental.autocomplete (project_id, type, value) VALUES (?, ?, SUBSTR(?, 1, 8000))",
|
||||||
"pages": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"pages": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"clicks": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"clicks": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"inputs": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$duration_s", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"inputs": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$duration_s", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"errors": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", error_id, "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"errors": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", error_id, "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"performance": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"performance": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"requests": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$duration_s", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"requests": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$duration_s", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"custom": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"custom": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties", properties) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"graphql": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"graphql": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"issuesEvents": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", issue_type, issue_id, "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
"issuesEvents": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", issue_type, issue_id, "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"issues": "INSERT INTO experimental.issues (project_id, issue_id, type, context_string) VALUES (?, ?, ?, ?)",
|
"issues": "INSERT INTO experimental.issues (project_id, issue_id, type, context_string) VALUES (?, ?, ?, ?)",
|
||||||
|
|
@ -220,6 +221,8 @@ func (c *connectorImpl) InsertWebSession(session *sessions.Session) error {
|
||||||
session.UtmSource,
|
session.UtmSource,
|
||||||
session.UtmMedium,
|
session.UtmMedium,
|
||||||
session.UtmCampaign,
|
session.UtmCampaign,
|
||||||
|
session.ScreenWidth,
|
||||||
|
session.ScreenHeight,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
c.checkError("sessions", err)
|
c.checkError("sessions", err)
|
||||||
return fmt.Errorf("can't append to sessions batch: %s", err)
|
return fmt.Errorf("can't append to sessions batch: %s", err)
|
||||||
|
|
@ -725,7 +728,6 @@ func (c *connectorImpl) InsertRequest(session *sessions.Session, msg *messages.N
|
||||||
|
|
||||||
func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.CustomEvent) error {
|
func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.CustomEvent) error {
|
||||||
jsonString, err := json.Marshal(map[string]interface{}{
|
jsonString, err := json.Marshal(map[string]interface{}{
|
||||||
"payload": msg.Payload,
|
|
||||||
"user_device": session.UserDevice,
|
"user_device": session.UserDevice,
|
||||||
"user_device_type": session.UserDeviceType,
|
"user_device_type": session.UserDeviceType,
|
||||||
"page_title ": msg.PageTitle,
|
"page_title ": msg.PageTitle,
|
||||||
|
|
@ -733,6 +735,14 @@ func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.Cu
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't marshal custom event: %s", err)
|
return fmt.Errorf("can't marshal custom event: %s", err)
|
||||||
}
|
}
|
||||||
|
customPayload := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal([]byte(msg.Payload), &customPayload); err != nil {
|
||||||
|
log.Printf("can't unmarshal custom event payload into object: %s", err)
|
||||||
|
customPayload = map[string]interface{}{
|
||||||
|
"payload": msg.Payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
eventTime := datetime(msg.Timestamp)
|
eventTime := datetime(msg.Timestamp)
|
||||||
if err := c.batches["custom"].Append(
|
if err := c.batches["custom"].Append(
|
||||||
session.SessionID,
|
session.SessionID,
|
||||||
|
|
@ -752,7 +762,8 @@ func (c *connectorImpl) InsertCustom(session *sessions.Session, msg *messages.Cu
|
||||||
session.UserState,
|
session.UserState,
|
||||||
session.UserCity,
|
session.UserCity,
|
||||||
cropString(msg.Url),
|
cropString(msg.Url),
|
||||||
jsonString,
|
jsonString, // $properties
|
||||||
|
customPayload, // properties
|
||||||
); err != nil {
|
); err != nil {
|
||||||
c.checkError("custom", err)
|
c.checkError("custom", err)
|
||||||
return fmt.Errorf("can't append to custom batch: %s", err)
|
return fmt.Errorf("can't append to custom batch: %s", err)
|
||||||
|
|
@ -799,6 +810,45 @@ func (c *connectorImpl) InsertGraphQL(session *sessions.Session, msg *messages.G
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *connectorImpl) InsertIncident(session *sessions.Session, msg *messages.Incident) error {
|
||||||
|
jsonString, err := json.Marshal(map[string]interface{}{
|
||||||
|
"label": msg.Label,
|
||||||
|
"start_time": msg.StartTime,
|
||||||
|
"end_time": msg.EndTime,
|
||||||
|
"user_device": session.UserDevice,
|
||||||
|
"user_device_type": session.UserDeviceType,
|
||||||
|
"page_title ": msg.PageTitle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't marshal custom event: %s", err)
|
||||||
|
}
|
||||||
|
eventTime := datetime(msg.Timestamp)
|
||||||
|
if err := c.batches["custom"].Append(
|
||||||
|
session.SessionID,
|
||||||
|
uint16(session.ProjectID),
|
||||||
|
getUUID(msg),
|
||||||
|
"INCIDENT",
|
||||||
|
eventTime,
|
||||||
|
eventTime.Unix(),
|
||||||
|
session.UserUUID,
|
||||||
|
true,
|
||||||
|
session.Platform,
|
||||||
|
session.UserOSVersion,
|
||||||
|
session.UserOS,
|
||||||
|
session.UserBrowser,
|
||||||
|
session.Referrer,
|
||||||
|
session.UserCountry,
|
||||||
|
session.UserState,
|
||||||
|
session.UserCity,
|
||||||
|
cropString(msg.Url),
|
||||||
|
jsonString,
|
||||||
|
); err != nil {
|
||||||
|
c.checkError("custom", err)
|
||||||
|
return fmt.Errorf("can't append to custom batch: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Mobile events
|
// Mobile events
|
||||||
|
|
||||||
func (c *connectorImpl) InsertMobileSession(session *sessions.Session) error {
|
func (c *connectorImpl) InsertMobileSession(session *sessions.Session) error {
|
||||||
|
|
|
||||||
|
|
@ -270,3 +270,15 @@ func (conn *Conn) InsertWebStatsPerformance(p *messages.PerformanceTrackAggr) er
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) InsertIncident(sess *sessions.Session, e *messages.Incident) error {
|
||||||
|
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
|
||||||
|
issueID := hashid.MobileIncidentID(sess.ProjectID, sess.SessionID, e.Timestamp)
|
||||||
|
if err := conn.bulks.Get("webIssues").Append(sess.ProjectID, issueID, "incident", e.Url); err != nil {
|
||||||
|
conn.log.Error(sessCtx, "insert incident issue err: %s", err)
|
||||||
|
}
|
||||||
|
if err := conn.bulks.Get("webIssueEvents").Append(sess.SessionID, issueID, e.Timestamp, truncSqIdx(e.MsgID()), nil); err != nil {
|
||||||
|
conn.log.Error(sessCtx, "insert incident issue event err: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,11 @@ func MouseThrashingID(projectID uint32, sessID, ts uint64) string {
|
||||||
hash.Write([]byte(strconv.FormatUint(ts, 10)))
|
hash.Write([]byte(strconv.FormatUint(ts, 10)))
|
||||||
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MobileIncidentID(projectID uint32, sessID, ts uint64) string {
|
||||||
|
hash := fnv.New128a()
|
||||||
|
hash.Write([]byte("mobile_incident"))
|
||||||
|
hash.Write([]byte(strconv.FormatUint(sessID, 10)))
|
||||||
|
hash.Write([]byte(strconv.FormatUint(ts, 10)))
|
||||||
|
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,4 @@ func IsMobileType(id int) bool {
|
||||||
|
|
||||||
func IsDOMType(id int) bool {
|
func IsDOMType(id int) bool {
|
||||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
5
ee/api/.gitignore
vendored
5
ee/api/.gitignore
vendored
|
|
@ -201,16 +201,17 @@ Pipfile.lock
|
||||||
/chalicelib/core/metrics/heatmaps
|
/chalicelib/core/metrics/heatmaps
|
||||||
/chalicelib/core/metrics/product_analytics
|
/chalicelib/core/metrics/product_analytics
|
||||||
/chalicelib/core/metrics/product_anaytics2.py
|
/chalicelib/core/metrics/product_anaytics2.py
|
||||||
/chalicelib/core/events
|
/chalicelib/core/events*
|
||||||
/chalicelib/core/feature_flags.py
|
/chalicelib/core/feature_flags.py
|
||||||
/chalicelib/core/issue_tracking/*
|
/chalicelib/core/issue_tracking/*
|
||||||
/chalicelib/core/issues.py
|
/chalicelib/core/issues.py
|
||||||
|
/chalicelib/core/issues/
|
||||||
/chalicelib/core/jobs.py
|
/chalicelib/core/jobs.py
|
||||||
/chalicelib/core/log_tools/*
|
/chalicelib/core/log_tools/*
|
||||||
/chalicelib/core/metadata.py
|
/chalicelib/core/metadata.py
|
||||||
/chalicelib/core/mobile.py
|
/chalicelib/core/mobile.py
|
||||||
/chalicelib/core/saved_search.py
|
/chalicelib/core/saved_search.py
|
||||||
/chalicelib/core/sessions/*.py
|
/chalicelib/core/sessions/**/*.py
|
||||||
/chalicelib/core/sessions/sessions_viewed
|
/chalicelib/core/sessions/sessions_viewed
|
||||||
/chalicelib/core/metrics/modules
|
/chalicelib/core/metrics/modules
|
||||||
/chalicelib/core/socket_ios.py
|
/chalicelib/core/socket_ios.py
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ python-decouple = "==3.8"
|
||||||
pydantic = {extras = ["email"], version = "==2.11.4"}
|
pydantic = {extras = ["email"], version = "==2.11.4"}
|
||||||
apscheduler = "==3.11.0"
|
apscheduler = "==3.11.0"
|
||||||
python3-saml = "==1.16.0"
|
python3-saml = "==1.16.0"
|
||||||
|
lxml = "==5.3.0"
|
||||||
|
xmlsec = "==1.3.14"
|
||||||
python-multipart = "==0.0.20"
|
python-multipart = "==0.0.20"
|
||||||
redis = "==6.1.0"
|
redis = "==6.1.0"
|
||||||
azure-storage-blob = "==12.25.1"
|
azure-storage-blob = "==12.25.1"
|
||||||
|
|
|
||||||
|
|
@ -101,4 +101,4 @@ rm -rf ./chalicelib/core/errors/errors_details.py
|
||||||
rm -rf ./chalicelib/core/notes.py
|
rm -rf ./chalicelib/core/notes.py
|
||||||
rm -rf ./chalicelib/utils/contextual_validators.py
|
rm -rf ./chalicelib/utils/contextual_validators.py
|
||||||
rm -rf ./routers/subs/product_analytics.py
|
rm -rf ./routers/subs/product_analytics.py
|
||||||
rm -rf ./schemas/product_analytics.py
|
rm -rf ./schemas/product_analytics.py
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ apscheduler==3.11.0
|
||||||
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
|
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
|
||||||
#--no-binary is used to avoid libxml2 library version incompatibilities between xmlsec and lxml
|
#--no-binary is used to avoid libxml2 library version incompatibilities between xmlsec and lxml
|
||||||
python3-saml==1.16.0
|
python3-saml==1.16.0
|
||||||
--no-binary=lxml
|
lxml==5.3.0 --no-binary=lxml
|
||||||
|
xmlsec==1.3.14 --no-binary=xmlsec
|
||||||
|
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -275,8 +275,7 @@ def get_projects(context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
|
def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
|
||||||
Depends(contextual_validators.validate_contextual_payload),
|
Depends(contextual_validators.validate_contextual_payload),
|
||||||
context: schemas.CurrentContext = Depends(OR_context)):
|
context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id,
|
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id)
|
||||||
platform=context.project.platform)
|
|
||||||
return {'data': data}
|
return {'data': data}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -285,8 +284,7 @@ def search_sessions(projectId: int, data: schemas.SessionsSearchPayloadSchema =
|
||||||
def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
|
def session_ids_search(projectId: int, data: schemas.SessionsSearchPayloadSchema = \
|
||||||
Depends(contextual_validators.validate_contextual_payload),
|
Depends(contextual_validators.validate_contextual_payload),
|
||||||
context: schemas.CurrentContext = Depends(OR_context)):
|
context: schemas.CurrentContext = Depends(OR_context)):
|
||||||
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id, ids_only=True,
|
data = sessions_search.search_sessions(data=data, project=context.project, user_id=context.user_id, ids_only=True)
|
||||||
platform=context.project.platform)
|
|
||||||
return {'data': data}
|
return {'data': data}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ from .schemas_ee import *
|
||||||
from .assist_stats_schema import *
|
from .assist_stats_schema import *
|
||||||
from .product_analytics import *
|
from .product_analytics import *
|
||||||
from . import overrides as _overrides
|
from . import overrides as _overrides
|
||||||
from .schemas import _PaginatedSchema as PaginatedSchema
|
from .schemas import _PaginatedSchema as PaginatedSchema
|
||||||
|
|
|
||||||
|
|
@ -829,6 +829,15 @@ class ResourceTiming(Message):
|
||||||
self.stalled = stalled
|
self.stalled = stalled
|
||||||
|
|
||||||
|
|
||||||
|
class Incident(Message):
|
||||||
|
__id__ = 87
|
||||||
|
|
||||||
|
def __init__(self, label, start_time, end_time):
|
||||||
|
self.label = label
|
||||||
|
self.start_time = start_time
|
||||||
|
self.end_time = end_time
|
||||||
|
|
||||||
|
|
||||||
class LongAnimationTask(Message):
|
class LongAnimationTask(Message):
|
||||||
__id__ = 89
|
__id__ = 89
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1241,6 +1241,19 @@ cdef class ResourceTiming(PyMessage):
|
||||||
self.stalled = stalled
|
self.stalled = stalled
|
||||||
|
|
||||||
|
|
||||||
|
cdef class Incident(PyMessage):
|
||||||
|
cdef public int __id__
|
||||||
|
cdef public str label
|
||||||
|
cdef public long start_time
|
||||||
|
cdef public long end_time
|
||||||
|
|
||||||
|
def __init__(self, str label, long start_time, long end_time):
|
||||||
|
self.__id__ = 87
|
||||||
|
self.label = label
|
||||||
|
self.start_time = start_time
|
||||||
|
self.end_time = end_time
|
||||||
|
|
||||||
|
|
||||||
cdef class LongAnimationTask(PyMessage):
|
cdef class LongAnimationTask(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public str name
|
cdef public str name
|
||||||
|
|
|
||||||
|
|
@ -750,6 +750,13 @@ class MessageCodec(Codec):
|
||||||
stalled=self.read_uint(reader)
|
stalled=self.read_uint(reader)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message_id == 87:
|
||||||
|
return Incident(
|
||||||
|
label=self.read_string(reader),
|
||||||
|
start_time=self.read_int(reader),
|
||||||
|
end_time=self.read_int(reader)
|
||||||
|
)
|
||||||
|
|
||||||
if message_id == 89:
|
if message_id == 89:
|
||||||
return LongAnimationTask(
|
return LongAnimationTask(
|
||||||
name=self.read_string(reader),
|
name=self.read_string(reader),
|
||||||
|
|
|
||||||
|
|
@ -848,6 +848,13 @@ cdef class MessageCodec:
|
||||||
stalled=self.read_uint(reader)
|
stalled=self.read_uint(reader)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if message_id == 87:
|
||||||
|
return Incident(
|
||||||
|
label=self.read_string(reader),
|
||||||
|
start_time=self.read_int(reader),
|
||||||
|
end_time=self.read_int(reader)
|
||||||
|
)
|
||||||
|
|
||||||
if message_id == 89:
|
if message_id == 89:
|
||||||
return LongAnimationTask(
|
return LongAnimationTask(
|
||||||
name=self.read_string(reader),
|
name=self.read_string(reader),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
SELECT 1
|
||||||
|
FROM (SELECT throwIf(platform = 'ios', 'IOS sessions found')
|
||||||
|
FROM experimental.sessions) AS raw
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
SELECT 1
|
||||||
|
FROM (SELECT throwIf(platform = 'android', 'Android sessions found')
|
||||||
|
FROM experimental.sessions) AS raw
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
ALTER TABLE experimental.sessions
|
||||||
|
MODIFY COLUMN platform Enum8('web'=1,'mobile'=2) DEFAULT 'web';
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.22.0-ee';
|
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.22.0-ee';
|
||||||
|
|
||||||
SET allow_experimental_json_type = 1;
|
SET allow_experimental_json_type = 1;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
SELECT 1
|
||||||
|
FROM (SELECT throwIf(platform = 'ios', 'IOS sessions found')
|
||||||
|
FROM experimental.sessions) AS raw
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
SELECT 1
|
||||||
|
FROM (SELECT throwIf(platform = 'android', 'Android sessions found')
|
||||||
|
FROM experimental.sessions) AS raw
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
ALTER TABLE experimental.sessions
|
||||||
|
MODIFY COLUMN platform Enum8('web'=1,'mobile'=2) DEFAULT 'web';
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.23.0-ee';
|
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.23.0-ee';
|
||||||
|
|
||||||
DROP TABLE IF EXISTS product_analytics.all_events;
|
DROP TABLE IF EXISTS product_analytics.all_events;
|
||||||
|
|
@ -41,26 +54,27 @@ CREATE TABLE IF NOT EXISTS product_analytics.event_properties
|
||||||
event_name String,
|
event_name String,
|
||||||
property_name String,
|
property_name String,
|
||||||
value_type String,
|
value_type String,
|
||||||
|
auto_captured BOOL,
|
||||||
|
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
ORDER BY (project_id, event_name, property_name, value_type);
|
ORDER BY (project_id, event_name, property_name, value_type, auto_captured);
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.event_properties_extractor_mv
|
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.event_properties_extractor_mv
|
||||||
TO product_analytics.event_properties AS
|
TO product_analytics.event_properties AS
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
`$event_name` AS event_name,
|
`$event_name` AS event_name,
|
||||||
property_name,
|
property_name,
|
||||||
JSONType(JSONExtractRaw(toString(`$properties`), property_name)) AS value_type
|
toString(JSONType(JSONExtractRaw(toString(`$properties`), property_name))) AS value_type,
|
||||||
|
`$auto_captured` AS auto_captured
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name;
|
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||||
|
UNION DISTINCT
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.event_cproperties_extractor
|
|
||||||
TO product_analytics.event_properties AS
|
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
`$event_name` AS event_name,
|
`$event_name` AS event_name,
|
||||||
property_name,
|
property_name,
|
||||||
JSONType(JSONExtractRaw(toString(`properties`), property_name)) AS value_type
|
toString(JSONType(JSONExtractRaw(toString(`properties`), property_name))) AS value_type,
|
||||||
|
`$auto_captured` AS auto_captured
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
ARRAY JOIN JSONExtractKeys(toString(`properties`)) as property_name;
|
ARRAY JOIN JSONExtractKeys(toString(`properties`)) as property_name;
|
||||||
|
|
||||||
|
|
@ -105,10 +119,8 @@ FROM product_analytics.events
|
||||||
WHERE (all_properties.display_name != ''
|
WHERE (all_properties.display_name != ''
|
||||||
OR all_properties.description != '')
|
OR all_properties.description != '')
|
||||||
AND is_event_property) AS old_data
|
AND is_event_property) AS old_data
|
||||||
ON (events.project_id = old_data.project_id AND property_name = old_data.property_name);
|
ON (events.project_id = old_data.project_id AND property_name = old_data.property_name)
|
||||||
|
UNION DISTINCT
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.all_cproperties_extractor_mv
|
|
||||||
TO product_analytics.all_properties AS
|
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
property_name,
|
property_name,
|
||||||
TRUE AS is_event_property,
|
TRUE AS is_event_property,
|
||||||
|
|
@ -155,7 +167,7 @@ FROM product_analytics.events
|
||||||
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
||||||
AND value != ''
|
AND value != ''
|
||||||
LIMIT 2 BY project_id,property_name
|
LIMIT 2 BY project_id,property_name
|
||||||
UNION ALL
|
UNION DISTINCT
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
property_name,
|
property_name,
|
||||||
TRUE AS is_event_property,
|
TRUE AS is_event_property,
|
||||||
|
|
@ -225,6 +237,16 @@ SELECT project_id,
|
||||||
_timestamp
|
_timestamp
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||||
|
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||||
|
AND _timestamp > now() - INTERVAL 1 MONTH
|
||||||
|
UNION DISTINCT
|
||||||
|
SELECT project_id,
|
||||||
|
`$event_name` AS event_name,
|
||||||
|
property_name,
|
||||||
|
JSONExtractString(toString(`properties`), property_name) AS value,
|
||||||
|
_timestamp
|
||||||
|
FROM product_analytics.events
|
||||||
|
ARRAY JOIN JSONExtractKeys(toString(`properties`)) as property_name
|
||||||
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||||
AND _timestamp > now() - INTERVAL 1 MONTH;
|
AND _timestamp > now() - INTERVAL 1 MONTH;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
|
||||||
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
|
user_country Enum8('UN'=-128, 'RW'=-127, 'SO'=-126, 'YE'=-125, 'IQ'=-124, 'SA'=-123, 'IR'=-122, 'CY'=-121, 'TZ'=-120, 'SY'=-119, 'AM'=-118, 'KE'=-117, 'CD'=-116, 'DJ'=-115, 'UG'=-114, 'CF'=-113, 'SC'=-112, 'JO'=-111, 'LB'=-110, 'KW'=-109, 'OM'=-108, 'QA'=-107, 'BH'=-106, 'AE'=-105, 'IL'=-104, 'TR'=-103, 'ET'=-102, 'ER'=-101, 'EG'=-100, 'SD'=-99, 'GR'=-98, 'BI'=-97, 'EE'=-96, 'LV'=-95, 'AZ'=-94, 'LT'=-93, 'SJ'=-92, 'GE'=-91, 'MD'=-90, 'BY'=-89, 'FI'=-88, 'AX'=-87, 'UA'=-86, 'MK'=-85, 'HU'=-84, 'BG'=-83, 'AL'=-82, 'PL'=-81, 'RO'=-80, 'XK'=-79, 'ZW'=-78, 'ZM'=-77, 'KM'=-76, 'MW'=-75, 'LS'=-74, 'BW'=-73, 'MU'=-72, 'SZ'=-71, 'RE'=-70, 'ZA'=-69, 'YT'=-68, 'MZ'=-67, 'MG'=-66, 'AF'=-65, 'PK'=-64, 'BD'=-63, 'TM'=-62, 'TJ'=-61, 'LK'=-60, 'BT'=-59, 'IN'=-58, 'MV'=-57, 'IO'=-56, 'NP'=-55, 'MM'=-54, 'UZ'=-53, 'KZ'=-52, 'KG'=-51, 'TF'=-50, 'HM'=-49, 'CC'=-48, 'PW'=-47, 'VN'=-46, 'TH'=-45, 'ID'=-44, 'LA'=-43, 'TW'=-42, 'PH'=-41, 'MY'=-40, 'CN'=-39, 'HK'=-38, 'BN'=-37, 'MO'=-36, 'KH'=-35, 'KR'=-34, 'JP'=-33, 'KP'=-32, 'SG'=-31, 'CK'=-30, 'TL'=-29, 'RU'=-28, 'MN'=-27, 'AU'=-26, 'CX'=-25, 'MH'=-24, 'FM'=-23, 'PG'=-22, 'SB'=-21, 'TV'=-20, 'NR'=-19, 'VU'=-18, 'NC'=-17, 'NF'=-16, 'NZ'=-15, 'FJ'=-14, 'LY'=-13, 'CM'=-12, 'SN'=-11, 'CG'=-10, 'PT'=-9, 'LR'=-8, 'CI'=-7, 'GH'=-6, 'GQ'=-5, 'NG'=-4, 'BF'=-3, 'TG'=-2, 'GW'=-1, 'MR'=0, 'BJ'=1, 'GA'=2, 'SL'=3, 'ST'=4, 'GI'=5, 'GM'=6, 'GN'=7, 'TD'=8, 'NE'=9, 'ML'=10, 'EH'=11, 'TN'=12, 'ES'=13, 'MA'=14, 'MT'=15, 'DZ'=16, 'FO'=17, 'DK'=18, 'IS'=19, 'GB'=20, 'CH'=21, 'SE'=22, 'NL'=23, 'AT'=24, 'BE'=25, 'DE'=26, 'LU'=27, 'IE'=28, 'MC'=29, 'FR'=30, 'AD'=31, 'LI'=32, 'JE'=33, 'IM'=34, 'GG'=35, 'SK'=36, 'CZ'=37, 'NO'=38, 'VA'=39, 'SM'=40, 'IT'=41, 'SI'=42, 'ME'=43, 'HR'=44, 'BA'=45, 'AO'=46, 'NA'=47, 'SH'=48, 'BV'=49, 'BB'=50, 'CV'=51, 'GY'=52, 'GF'=53, 'SR'=54, 'PM'=55, 'GL'=56, 'PY'=57, 'UY'=58, 'BR'=59, 'FK'=60, 'GS'=61, 'JM'=62, 'DO'=63, 'CU'=64, 'MQ'=65, 'BS'=66, 'BM'=67, 'AI'=68, 'TT'=69, 'KN'=70, 'DM'=71, 'AG'=72, 'LC'=73, 'TC'=74, 'AW'=75, 'VG'=76, 'VC'=77, 'MS'=78, 'MF'=79, 'BL'=80, 'GP'=81, 'GD'=82, 'KY'=83, 'BZ'=84, 'SV'=85, 'GT'=86, 'HN'=87, 'NI'=88, 'CR'=89, 'VE'=90, 'EC'=91, 'CO'=92, 'PA'=93, 'HT'=94, 'AR'=95, 'CL'=96, 'BO'=97, 'PE'=98, 'MX'=99, 'PF'=100, 'PN'=101, 'KI'=102, 'TK'=103, 'TO'=104, 'WF'=105, 'WS'=106, 'NU'=107, 'MP'=108, 'GU'=109, 'PR'=110, 'VI'=111, 'UM'=112, 'AS'=113, 'CA'=114, 'US'=115, 'PS'=116, 'RS'=117, 'AQ'=118, 'SX'=119, 'CW'=120, 'BQ'=121, 'SS'=122,'BU'=123, 'VD'=124, 'YD'=125, 'DD'=126),
|
||||||
user_city LowCardinality(String),
|
user_city LowCardinality(String),
|
||||||
user_state LowCardinality(String),
|
user_state LowCardinality(String),
|
||||||
platform Enum8('web'=1,'ios'=2,'android'=3) DEFAULT 'web',
|
platform Enum8('web'=1,'mobile'=2) DEFAULT 'web',
|
||||||
datetime DateTime,
|
datetime DateTime,
|
||||||
timezone LowCardinality(Nullable(String)),
|
timezone LowCardinality(Nullable(String)),
|
||||||
duration UInt32,
|
duration UInt32,
|
||||||
|
|
@ -134,7 +134,7 @@ CREATE TABLE IF NOT EXISTS experimental.sessions
|
||||||
metadata_8 Nullable(String),
|
metadata_8 Nullable(String),
|
||||||
metadata_9 Nullable(String),
|
metadata_9 Nullable(String),
|
||||||
metadata_10 Nullable(String),
|
metadata_10 Nullable(String),
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
PARTITION BY toYYYYMMDD(datetime)
|
PARTITION BY toYYYYMMDD(datetime)
|
||||||
ORDER BY (project_id, datetime, session_id)
|
ORDER BY (project_id, datetime, session_id)
|
||||||
|
|
@ -676,29 +676,29 @@ CREATE TABLE IF NOT EXISTS product_analytics.event_properties
|
||||||
event_name String,
|
event_name String,
|
||||||
property_name String,
|
property_name String,
|
||||||
value_type String,
|
value_type String,
|
||||||
|
auto_captured BOOL,
|
||||||
|
|
||||||
_timestamp DateTime DEFAULT now()
|
_timestamp DateTime DEFAULT now()
|
||||||
) ENGINE = ReplacingMergeTree(_timestamp)
|
) ENGINE = ReplacingMergeTree(_timestamp)
|
||||||
ORDER BY (project_id, event_name, property_name, value_type);
|
ORDER BY (project_id, event_name, property_name, value_type, auto_captured);
|
||||||
|
|
||||||
-- ----------------- This is experimental, if it doesn't work, we need to do it in db worker -------------
|
-- ----------------- This is experimental, if it doesn't work, we need to do it in db worker -------------
|
||||||
-- Incremental materialized view to fill event_properties using $properties
|
-- Incremental materialized view to fill event_properties using $properties & properties
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.event_properties_extractor_mv
|
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.event_properties_extractor_mv
|
||||||
TO product_analytics.event_properties AS
|
TO product_analytics.event_properties AS
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
`$event_name` AS event_name,
|
`$event_name` AS event_name,
|
||||||
property_name,
|
property_name,
|
||||||
JSONType(JSONExtractRaw(toString(`$properties`), property_name)) AS value_type
|
toString(JSONType(JSONExtractRaw(toString(`$properties`), property_name))) AS value_type,
|
||||||
|
`$auto_captured` AS auto_captured
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name;
|
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||||
|
UNION DISTINCT
|
||||||
-- Incremental materialized view to fill event_properties using properties
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.event_cproperties_extractor
|
|
||||||
TO product_analytics.event_properties AS
|
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
`$event_name` AS event_name,
|
`$event_name` AS event_name,
|
||||||
property_name,
|
property_name,
|
||||||
JSONType(JSONExtractRaw(toString(`properties`), property_name)) AS value_type
|
toString(JSONType(JSONExtractRaw(toString(`properties`), property_name))) AS value_type,
|
||||||
|
`$auto_captured` AS auto_captured
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
ARRAY JOIN JSONExtractKeys(toString(`properties`)) as property_name;
|
ARRAY JOIN JSONExtractKeys(toString(`properties`)) as property_name;
|
||||||
-- -------- END ---------
|
-- -------- END ---------
|
||||||
|
|
@ -724,7 +724,7 @@ CREATE TABLE IF NOT EXISTS product_analytics.all_properties
|
||||||
|
|
||||||
|
|
||||||
-- ----------------- This is experimental, if it doesn't work, we need to do it in db worker -------------
|
-- ----------------- This is experimental, if it doesn't work, we need to do it in db worker -------------
|
||||||
-- Incremental materialized view to fill all_properties using $properties
|
-- Incremental materialized view to fill all_properties using $properties and properties
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.all_properties_extractor_mv
|
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.all_properties_extractor_mv
|
||||||
TO product_analytics.all_properties AS
|
TO product_analytics.all_properties AS
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
|
|
@ -748,11 +748,8 @@ FROM product_analytics.events
|
||||||
WHERE (all_properties.display_name != ''
|
WHERE (all_properties.display_name != ''
|
||||||
OR all_properties.description != '')
|
OR all_properties.description != '')
|
||||||
AND is_event_property) AS old_data
|
AND is_event_property) AS old_data
|
||||||
ON (events.project_id = old_data.project_id AND property_name = old_data.property_name);
|
ON (events.project_id = old_data.project_id AND property_name = old_data.property_name)
|
||||||
|
UNION DISTINCT
|
||||||
-- Incremental materialized view to fill all_properties using properties
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS product_analytics.all_cproperties_extractor_mv
|
|
||||||
TO product_analytics.all_properties AS
|
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
property_name,
|
property_name,
|
||||||
TRUE AS is_event_property,
|
TRUE AS is_event_property,
|
||||||
|
|
@ -802,7 +799,7 @@ FROM product_analytics.events
|
||||||
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
WHERE randCanonical() < 0.5 -- This randomly skips inserts
|
||||||
AND value != ''
|
AND value != ''
|
||||||
LIMIT 2 BY project_id,property_name
|
LIMIT 2 BY project_id,property_name
|
||||||
UNION ALL
|
UNION DISTINCT
|
||||||
-- using union because each table should be the target of 1 single refreshable MV
|
-- using union because each table should be the target of 1 single refreshable MV
|
||||||
SELECT project_id,
|
SELECT project_id,
|
||||||
property_name,
|
property_name,
|
||||||
|
|
@ -873,6 +870,16 @@ SELECT project_id,
|
||||||
_timestamp
|
_timestamp
|
||||||
FROM product_analytics.events
|
FROM product_analytics.events
|
||||||
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
ARRAY JOIN JSONExtractKeys(toString(`$properties`)) as property_name
|
||||||
|
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||||
|
AND _timestamp > now() - INTERVAL 1 MONTH
|
||||||
|
UNION DISTINCT
|
||||||
|
SELECT project_id,
|
||||||
|
`$event_name` AS event_name,
|
||||||
|
property_name,
|
||||||
|
JSONExtractString(toString(`properties`), property_name) AS value,
|
||||||
|
_timestamp
|
||||||
|
FROM product_analytics.events
|
||||||
|
ARRAY JOIN JSONExtractKeys(toString(`properties`)) as property_name
|
||||||
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
WHERE length(value) > 0 AND isNull(toFloat64OrNull(value))
|
||||||
AND _timestamp > now() - INTERVAL 1 MONTH;
|
AND _timestamp > now() - INTERVAL 1 MONTH;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ DROP SCHEMA IF EXISTS or_cache CASCADE;
|
||||||
ALTER TABLE public.tenants
|
ALTER TABLE public.tenants
|
||||||
ALTER COLUMN scope_state SET DEFAULT 2;
|
ALTER COLUMN scope_state SET DEFAULT 2;
|
||||||
|
|
||||||
|
ALTER TYPE issue_type ADD VALUE IF NOT EXISTS 'incident';
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
||||||
\elif :is_next
|
\elif :is_next
|
||||||
|
|
|
||||||
|
|
@ -352,7 +352,8 @@ CREATE TYPE issue_type AS ENUM (
|
||||||
'custom',
|
'custom',
|
||||||
'js_exception',
|
'js_exception',
|
||||||
'mouse_thrashing',
|
'mouse_thrashing',
|
||||||
'app_crash'
|
'app_crash',
|
||||||
|
'incident'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE public.issues
|
CREATE TABLE public.issues
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
header: {
|
header: {
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#333',
|
color: 'var(--color-gray-darkest)',
|
||||||
overflow: 'truncate',
|
overflow: 'truncate',
|
||||||
paddingBottom: '.5rem',
|
paddingBottom: '.5rem',
|
||||||
paddingLeft: '14px',
|
paddingLeft: '14px',
|
||||||
|
|
@ -203,16 +203,16 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#000',
|
color: 'var(--color-black)',
|
||||||
},
|
},
|
||||||
percentage: {
|
percentage: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#454545',
|
color: 'var(--color-gray-dark)',
|
||||||
},
|
},
|
||||||
sessions: {
|
sessions: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: "mono, 'monospace', sans-serif",
|
fontFamily: "mono, 'monospace', sans-serif",
|
||||||
color: '#999999',
|
color: 'var(--color-gray-dark)',
|
||||||
},
|
},
|
||||||
clickIcon: {
|
clickIcon: {
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
|
|
@ -266,6 +266,7 @@ const EChartsSankey: React.FC<Props> = (props) => {
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
formatter: sankeyTooltip(echartNodes, nodeValues),
|
formatter: sankeyTooltip(echartNodes, nodeValues),
|
||||||
|
backgroundColor: 'var(--color-white)',
|
||||||
},
|
},
|
||||||
nodeAlign: 'left',
|
nodeAlign: 'left',
|
||||||
nodeWidth: 40,
|
nodeWidth: 40,
|
||||||
|
|
|
||||||
|
|
@ -102,18 +102,18 @@ export function customTooltipFormatter(uuid: string) {
|
||||||
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div style="
|
<div style="
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
background: ${params.color};
|
background: ${params.color};
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;">
|
height: 1rem;">
|
||||||
</div>
|
</div>
|
||||||
<div class="font-medium text-black">${fullname}</div>
|
<div class="font-medium text-black">${fullname}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="border-left: 2px solid ${
|
<div style="border-left: 2px solid ${
|
||||||
params.color
|
params.color
|
||||||
};" class="flex flex-col px-2 ml-2">
|
};" class="flex flex-col px-2 ml-2">
|
||||||
<div class="text-neutral-600 text-sm">
|
<div class="text-neutral-600 text-sm">
|
||||||
Total:
|
Total:
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|
@ -127,7 +127,7 @@ export function customTooltipFormatter(uuid: string) {
|
||||||
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||||||
str += `
|
str += `
|
||||||
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
||||||
<div class="text-neutral-600 text-sm">
|
<div class="text-neutral-600 text-sm">
|
||||||
${isPrevious ? 'Current' : 'Previous'} Total:
|
${isPrevious ? 'Current' : 'Previous'} Total:
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|
@ -168,9 +168,9 @@ export function customTooltipFormatter(uuid: string) {
|
||||||
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
<div class="flex flex-col gap-1 bg-white shadow border rounded p-2 z-50">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div style="
|
<div style="
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
background: ${params.color};
|
background: ${params.color};
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;">
|
height: 1rem;">
|
||||||
</div>
|
</div>
|
||||||
<div class="font-medium text-black">${seriesName}</div>
|
<div class="font-medium text-black">${seriesName}</div>
|
||||||
|
|
@ -179,7 +179,7 @@ export function customTooltipFormatter(uuid: string) {
|
||||||
<div style="border-left: 2px solid ${
|
<div style="border-left: 2px solid ${
|
||||||
params.color
|
params.color
|
||||||
};" class="flex flex-col px-2 ml-2">
|
};" class="flex flex-col px-2 ml-2">
|
||||||
<div class="text-neutral-600 text-sm">
|
<div class="text-neutral-600 text-sm">
|
||||||
${firstTs ? formatTimeOrDate(firstTs) : categoryLabel}
|
${firstTs ? formatTimeOrDate(firstTs) : categoryLabel}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|
@ -194,7 +194,7 @@ export function customTooltipFormatter(uuid: string) {
|
||||||
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
(window as any).__seriesColorMap?.[uuid]?.[partnerName] || '#999';
|
||||||
tooltipContent += `
|
tooltipContent += `
|
||||||
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
<div style="border-left: 2px dashed ${partnerColor};" class="flex flex-col px-2 ml-2">
|
||||||
<div class="text-neutral-600 text-sm">
|
<div class="text-neutral-600 text-sm">
|
||||||
${secondTs ? formatTimeOrDate(secondTs) : categoryLabel}
|
${secondTs ? formatTimeOrDate(secondTs) : categoryLabel}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
|
@ -229,13 +229,13 @@ function buildCompareTag(val: number, prevVal: number): string {
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div style="
|
<div style="
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
background: ${tagColor};
|
background: ${tagColor};
|
||||||
color: ${arrowColor};
|
color: ${arrowColor};
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.75rem;">
|
font-size: 0.75rem;">
|
||||||
<span>${arrow}</span>
|
<span>${arrow}</span>
|
||||||
<span>${absDelta}</span>
|
<span>${absDelta}</span>
|
||||||
|
|
@ -290,7 +290,7 @@ export function createSeries(
|
||||||
datasetId,
|
datasetId,
|
||||||
encode: { x: 'idx', y: fullName },
|
encode: { x: 'idx', y: fullName },
|
||||||
lineStyle: dashed ? { type: 'dashed' } : undefined,
|
lineStyle: dashed ? { type: 'dashed' } : undefined,
|
||||||
showSymbol: false,
|
showSymbol: data.chart.length === 1,
|
||||||
// custom flag to hide prev data from legend
|
// custom flag to hide prev data from legend
|
||||||
_hideInLegend: hideFromLegend,
|
_hideInLegend: hideFromLegend,
|
||||||
itemStyle: { opacity: 1 },
|
itemStyle: { opacity: 1 },
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Form, Input, Space, Modal } from 'antd';
|
import { Button, Form, Input, Space } from 'antd';
|
||||||
import { Trash } from 'UI/Icons';
|
import { Trash } from 'UI/Icons';
|
||||||
import { useStore } from '@/mstore';
|
import { useStore } from '@/mstore';
|
||||||
import { useModal } from 'Components/ModalContext';
|
import { useModal } from 'Components/ModalContext';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { confirm } from 'UI';
|
||||||
interface Props {
|
interface Props {
|
||||||
tag: any;
|
tag: any;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
|
|
@ -23,14 +23,16 @@ function TagForm(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
Modal.confirm({
|
if (
|
||||||
title: t('Tag'),
|
await confirm({
|
||||||
content: t('Are you sure you want to remove?'),
|
header: t('Remove Tag'),
|
||||||
onOk: async () => {
|
confirmButton: t('Remove'),
|
||||||
await tagWatchStore.deleteTag(tag.tagId, projectId);
|
confirmation: t('Are you sure you want to remove this tag?'),
|
||||||
closeModal();
|
})
|
||||||
},
|
) {
|
||||||
});
|
await tagWatchStore.deleteTag(tag.tagId, projectId);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ function BottomButtons({
|
||||||
<Button
|
<Button
|
||||||
loading={loading}
|
loading={loading}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
disabled={loading || !instance.validate()}
|
disabled={loading || !instance.validate()}
|
||||||
id="submit-button"
|
id="submit-button"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ function Condition({
|
||||||
<label className="w-1/6 flex-shrink-0 font-normal">{t('is')}</label>
|
<label className="w-1/6 flex-shrink-0 font-normal">{t('is')}</label>
|
||||||
<div className="w-2/6 flex items-center">
|
<div className="w-2/6 flex items-center">
|
||||||
<Select
|
<Select
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
placeholder={t('Select Condition')}
|
placeholder={t('Select Condition')}
|
||||||
options={localizedConditions}
|
options={localizedConditions}
|
||||||
name="operator"
|
name="operator"
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ function DashboardView(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
dashboardStore.resetPeriod();
|
||||||
if (queryParams.has('modal')) {
|
if (queryParams.has('modal')) {
|
||||||
onAddWidgets();
|
onAddWidgets();
|
||||||
trimQuery();
|
trimQuery();
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,18 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
Modal as AntdModal,
|
Modal as AntdModal,
|
||||||
Avatar, TableColumnType, Spin
|
Avatar,
|
||||||
|
TableColumnType,
|
||||||
|
Spin,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
EditOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import { EllipsisVertical } from 'lucide-react';
|
import { EllipsisVertical } from 'lucide-react';
|
||||||
import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface';
|
import { TablePaginationConfig, SorterResult } from 'antd/lib/table/interface';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { withSiteId } from 'App/routes';
|
import { withSiteId } from 'App/routes';
|
||||||
import { Icon } from 'UI';
|
import { Icon, confirm } from 'UI';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { TYPE_ICONS, TYPE_NAMES } from 'App/constants/card';
|
import { TYPE_ICONS, TYPE_NAMES } from 'App/constants/card';
|
||||||
import Widget from 'App/mstore/types/widget';
|
import Widget from 'App/mstore/types/widget';
|
||||||
|
|
@ -45,7 +44,7 @@ const ListView: React.FC<Props> = ({
|
||||||
toggleSelection,
|
toggleSelection,
|
||||||
disableSelection = false,
|
disableSelection = false,
|
||||||
inLibrary = false,
|
inLibrary = false,
|
||||||
loading = false
|
loading = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [editingMetricId, setEditingMetricId] = useState<number | null>(null);
|
const [editingMetricId, setEditingMetricId] = useState<number | null>(null);
|
||||||
|
|
@ -63,7 +62,7 @@ const ListView: React.FC<Props> = ({
|
||||||
<Text strong>
|
<Text strong>
|
||||||
{Math.min(
|
{Math.min(
|
||||||
(metricStore.pageSize || 10) * (metricStore.page || 1),
|
(metricStore.pageSize || 10) * (metricStore.page || 1),
|
||||||
list.length
|
list.length,
|
||||||
)}
|
)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
{t('of')} <Text strong>{list.length}</Text> {t('cards')}
|
{t('of')} <Text strong>{list.length}</Text> {t('cards')}
|
||||||
|
|
@ -124,15 +123,17 @@ const ListView: React.FC<Props> = ({
|
||||||
|
|
||||||
const onMenuClick = async (metric: Widget, { key }: { key: string }) => {
|
const onMenuClick = async (metric: Widget, { key }: { key: string }) => {
|
||||||
if (key === 'delete') {
|
if (key === 'delete') {
|
||||||
AntdModal.confirm({
|
if (
|
||||||
title: t('Confirm'),
|
await confirm({
|
||||||
content: t('Are you sure you want to permanently delete this card?'),
|
header: t('Delete Card'),
|
||||||
okText: t('Yes, delete'),
|
confirmButton: t('Delete'),
|
||||||
cancelText: t('No'),
|
confirmation: t(
|
||||||
onOk: async () => {
|
'Are you sure you want to permanently delete this card? This action cannot be undone.',
|
||||||
await metricStore.delete(metric);
|
),
|
||||||
}
|
})
|
||||||
});
|
) {
|
||||||
|
await metricStore.delete(metric);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (key === 'rename') {
|
if (key === 'rename') {
|
||||||
setEditingMetricId(metric.metricId);
|
setEditingMetricId(metric.metricId);
|
||||||
|
|
@ -155,7 +156,7 @@ const ListView: React.FC<Props> = ({
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ key: 'rename', icon: <EditOutlined />, label: t('Rename') },
|
{ key: 'rename', icon: <EditOutlined />, label: t('Rename') },
|
||||||
{ key: 'delete', icon: <DeleteOutlined />, label: t('Delete') }
|
{ key: 'delete', icon: <DeleteOutlined />, label: t('Delete') },
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderTitle = (_text: string, metric: Widget) => (
|
const renderTitle = (_text: string, metric: Widget) => (
|
||||||
|
|
@ -201,9 +202,10 @@ const ListView: React.FC<Props> = ({
|
||||||
key: 'title',
|
key: 'title',
|
||||||
className: 'cap-first pl-4',
|
className: 'cap-first pl-4',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
sortOrder: metricStore.sort.field === 'name' ? metricStore.sort.order : undefined,
|
sortOrder:
|
||||||
|
metricStore.sort.field === 'name' ? metricStore.sort.order : undefined,
|
||||||
width: inLibrary ? '31%' : '25%',
|
width: inLibrary ? '31%' : '25%',
|
||||||
render: renderTitle
|
render: renderTitle,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Owner'),
|
title: t('Owner'),
|
||||||
|
|
@ -211,19 +213,25 @@ const ListView: React.FC<Props> = ({
|
||||||
key: 'owner',
|
key: 'owner',
|
||||||
className: 'capitalize',
|
className: 'capitalize',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
sortOrder: metricStore.sort.field === 'owner_email' ? metricStore.sort.order : undefined,
|
sortOrder:
|
||||||
|
metricStore.sort.field === 'owner_email'
|
||||||
|
? metricStore.sort.order
|
||||||
|
: undefined,
|
||||||
width: inLibrary ? '31%' : '25%',
|
width: inLibrary ? '31%' : '25%',
|
||||||
render: renderOwner
|
render: renderOwner,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Last Modified'),
|
title: t('Last Modified'),
|
||||||
dataIndex: 'edited_at',
|
dataIndex: 'edited_at',
|
||||||
key: 'lastModified',
|
key: 'lastModified',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
sortOrder: metricStore.sort.field === 'edited_at' ? metricStore.sort.order : undefined,
|
sortOrder:
|
||||||
|
metricStore.sort.field === 'edited_at'
|
||||||
|
? metricStore.sort.order
|
||||||
|
: undefined,
|
||||||
width: inLibrary ? '31%' : '25%',
|
width: inLibrary ? '31%' : '25%',
|
||||||
render: renderLastModified
|
render: renderLastModified,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!inLibrary) {
|
if (!inLibrary) {
|
||||||
|
|
@ -232,14 +240,14 @@ const ListView: React.FC<Props> = ({
|
||||||
key: 'options',
|
key: 'options',
|
||||||
className: 'text-right',
|
className: 'text-right',
|
||||||
width: '5%',
|
width: '5%',
|
||||||
render: renderOptions
|
render: renderOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pag: TablePaginationConfig,
|
pag: TablePaginationConfig,
|
||||||
_filters: Record<string, (string | number | boolean)[] | null>,
|
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||||
sorterParam: SorterResult<Widget> | SorterResult<Widget>[]
|
sorterParam: SorterResult<Widget> | SorterResult<Widget>[],
|
||||||
) => {
|
) => {
|
||||||
const sorter = Array.isArray(sorterParam) ? sorterParam[0] : sorterParam;
|
const sorter = Array.isArray(sorterParam) ? sorterParam[0] : sorterParam;
|
||||||
let order = sorter.order;
|
let order = sorter.order;
|
||||||
|
|
@ -268,19 +276,19 @@ const ListView: React.FC<Props> = ({
|
||||||
onRow={
|
onRow={
|
||||||
inLibrary
|
inLibrary
|
||||||
? (record) => ({
|
? (record) => ({
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (!disableSelection) toggleSelection?.(record?.metricId);
|
if (!disableSelection) toggleSelection?.(record?.metricId);
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
rowSelection={
|
rowSelection={
|
||||||
!disableSelection
|
!disableSelection
|
||||||
? {
|
? {
|
||||||
selectedRowKeys: selectedList,
|
selectedRowKeys: selectedList,
|
||||||
onChange: (keys) => toggleSelection && toggleSelection(keys),
|
onChange: (keys) => toggleSelection && toggleSelection(keys),
|
||||||
columnWidth: 16
|
columnWidth: 16,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
|
@ -292,7 +300,7 @@ const ListView: React.FC<Props> = ({
|
||||||
showLessItems: true,
|
showLessItems: true,
|
||||||
showTotal: () => totalMessage,
|
showTotal: () => totalMessage,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
simple: true
|
simple: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AntdModal
|
<AntdModal
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import AddCardSection from '../AddCardSection/AddCardSection';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function MetricsList({
|
function MetricsList({
|
||||||
siteId,
|
siteId,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
inLibrary
|
inLibrary,
|
||||||
}: {
|
}: {
|
||||||
siteId: string;
|
siteId: string;
|
||||||
onSelectionChange?: (selected: any[]) => void;
|
onSelectionChange?: (selected: any[]) => void;
|
||||||
inLibrary?: boolean;
|
inLibrary?: boolean;
|
||||||
|
|
@ -26,16 +26,16 @@ function MetricsList({
|
||||||
const dashboard = dashboardStore.selectedDashboard;
|
const dashboard = dashboardStore.selectedDashboard;
|
||||||
const existingCardIds = useMemo(
|
const existingCardIds = useMemo(
|
||||||
() => dashboard?.widgets?.map((i) => parseInt(i.metricId)),
|
() => dashboard?.widgets?.map((i) => parseInt(i.metricId)),
|
||||||
[dashboard]
|
[dashboard],
|
||||||
);
|
);
|
||||||
const cards = useMemo(
|
const cards = useMemo(
|
||||||
() =>
|
() =>
|
||||||
onSelectionChange
|
onSelectionChange
|
||||||
? metricStore.filteredCards.filter(
|
? metricStore.filteredCards.filter(
|
||||||
(i) => !existingCardIds?.includes(parseInt(i.metricId))
|
(i) => !existingCardIds?.includes(parseInt(i.metricId)),
|
||||||
)
|
)
|
||||||
: metricStore.filteredCards,
|
: metricStore.filteredCards,
|
||||||
[metricStore.filteredCards, existingCardIds, onSelectionChange]
|
[metricStore.filteredCards, existingCardIds, onSelectionChange],
|
||||||
);
|
);
|
||||||
const loading = metricStore.isLoading;
|
const loading = metricStore.isLoading;
|
||||||
|
|
||||||
|
|
@ -66,7 +66,8 @@ function MetricsList({
|
||||||
metricStore.updateKey('sessionsPage', 1);
|
metricStore.updateKey('sessionsPage', 1);
|
||||||
}, [metricStore]);
|
}, [metricStore]);
|
||||||
|
|
||||||
const isFiltered = metricStore.filter.query !== '' || metricStore.filter.type !== '';
|
const isFiltered =
|
||||||
|
metricStore.filter.query !== '' || metricStore.filter.type !== '';
|
||||||
|
|
||||||
const searchImageDimensions = { width: 60, height: 'auto' };
|
const searchImageDimensions = { width: 60, height: 'auto' };
|
||||||
const defaultImageDimensions = { width: 600, height: 'auto' };
|
const defaultImageDimensions = { width: 600, height: 'auto' };
|
||||||
|
|
@ -93,7 +94,9 @@ function MetricsList({
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div>
|
<div>
|
||||||
{t('Create and customize cards to analyze trends and user behavior effectively.')}
|
{t(
|
||||||
|
'Create and customize cards to analyze trends and user behavior effectively.',
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
<Popover
|
||||||
arrow={false}
|
arrow={false}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ interface Props {
|
||||||
isSaved?: boolean;
|
isSaved?: boolean;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
isPreview?: boolean;
|
isPreview?: boolean;
|
||||||
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WidgetChart(props: Props) {
|
function WidgetChart(props: Props) {
|
||||||
|
|
@ -52,7 +53,7 @@ function WidgetChart(props: Props) {
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
rootMargin: '200px 0px',
|
rootMargin: '200px 0px',
|
||||||
});
|
});
|
||||||
const { isSaved = false, metric, isTemplate } = props;
|
const { isSaved = false, metric, isTemplate, height } = props;
|
||||||
const { dashboardStore, metricStore } = useStore();
|
const { dashboardStore, metricStore } = useStore();
|
||||||
const _metric: any = props.metric;
|
const _metric: any = props.metric;
|
||||||
const data = _metric.data;
|
const data = _metric.data;
|
||||||
|
|
@ -283,6 +284,7 @@ function WidgetChart(props: Props) {
|
||||||
hideLegend
|
hideLegend
|
||||||
onClick={onChartClick}
|
onClick={onChartClick}
|
||||||
label={t('Conversion')}
|
label={t('Conversion')}
|
||||||
|
height={height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +295,7 @@ function WidgetChart(props: Props) {
|
||||||
data={data}
|
data={data}
|
||||||
compData={compData}
|
compData={compData}
|
||||||
isWidget={isSaved || isTemplate}
|
isWidget={isSaved || isTemplate}
|
||||||
|
height={height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -308,6 +311,7 @@ function WidgetChart(props: Props) {
|
||||||
metric={defaultMetric}
|
metric={defaultMetric}
|
||||||
data={data}
|
data={data}
|
||||||
predefinedKey={_metric.metricOf}
|
predefinedKey={_metric.metricOf}
|
||||||
|
height={height}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +335,7 @@ function WidgetChart(props: Props) {
|
||||||
compData={compDataCopy}
|
compData={compDataCopy}
|
||||||
onSeriesFocus={onFocus}
|
onSeriesFocus={onFocus}
|
||||||
onClick={onChartClick}
|
onClick={onChartClick}
|
||||||
|
height={height}
|
||||||
label={
|
label={
|
||||||
_metric.metricOf === 'sessionCount'
|
_metric.metricOf === 'sessionCount'
|
||||||
? t('Number of Sessions')
|
? t('Number of Sessions')
|
||||||
|
|
@ -360,6 +365,7 @@ function WidgetChart(props: Props) {
|
||||||
return (
|
return (
|
||||||
<BarChart
|
<BarChart
|
||||||
inGrid={!props.isPreview}
|
inGrid={!props.isPreview}
|
||||||
|
height={height}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
compData={compDataCopy}
|
compData={compDataCopy}
|
||||||
params={params}
|
params={params}
|
||||||
|
|
@ -378,6 +384,7 @@ function WidgetChart(props: Props) {
|
||||||
if (viewType === 'progressChart') {
|
if (viewType === 'progressChart') {
|
||||||
return (
|
return (
|
||||||
<ColumnChart
|
<ColumnChart
|
||||||
|
height={height}
|
||||||
inGrid={!props.isPreview}
|
inGrid={!props.isPreview}
|
||||||
horizontal
|
horizontal
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
|
@ -396,6 +403,7 @@ function WidgetChart(props: Props) {
|
||||||
if (viewType === 'pieChart') {
|
if (viewType === 'pieChart') {
|
||||||
return (
|
return (
|
||||||
<PieChart
|
<PieChart
|
||||||
|
height={height}
|
||||||
inGrid={!props.isPreview}
|
inGrid={!props.isPreview}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
onSeriesFocus={onFocus}
|
onSeriesFocus={onFocus}
|
||||||
|
|
@ -412,6 +420,7 @@ function WidgetChart(props: Props) {
|
||||||
<CustomMetricPercentage
|
<CustomMetricPercentage
|
||||||
inGrid={!props.isPreview}
|
inGrid={!props.isPreview}
|
||||||
data={data[0]}
|
data={data[0]}
|
||||||
|
height={height}
|
||||||
colors={colors}
|
colors={colors}
|
||||||
params={params}
|
params={params}
|
||||||
label={
|
label={
|
||||||
|
|
@ -451,6 +460,7 @@ function WidgetChart(props: Props) {
|
||||||
return (
|
return (
|
||||||
<BugNumChart
|
<BugNumChart
|
||||||
values={values}
|
values={values}
|
||||||
|
height={height}
|
||||||
inGrid={!props.isPreview}
|
inGrid={!props.isPreview}
|
||||||
colors={colors}
|
colors={colors}
|
||||||
onSeriesFocus={onFocus}
|
onSeriesFocus={onFocus}
|
||||||
|
|
@ -470,6 +480,7 @@ function WidgetChart(props: Props) {
|
||||||
<CustomMetricTableSessions
|
<CustomMetricTableSessions
|
||||||
metric={_metric}
|
metric={_metric}
|
||||||
data={data}
|
data={data}
|
||||||
|
height={height}
|
||||||
isTemplate={isTemplate}
|
isTemplate={isTemplate}
|
||||||
isEdit={!isSaved && !isTemplate}
|
isEdit={!isSaved && !isTemplate}
|
||||||
/>
|
/>
|
||||||
|
|
@ -480,6 +491,7 @@ function WidgetChart(props: Props) {
|
||||||
<CustomMetricTableErrors
|
<CustomMetricTableErrors
|
||||||
metric={_metric}
|
metric={_metric}
|
||||||
data={data}
|
data={data}
|
||||||
|
height={height}
|
||||||
// isTemplate={isTemplate}
|
// isTemplate={isTemplate}
|
||||||
isEdit={!isSaved && !isTemplate}
|
isEdit={!isSaved && !isTemplate}
|
||||||
/>
|
/>
|
||||||
|
|
@ -490,6 +502,7 @@ function WidgetChart(props: Props) {
|
||||||
<SessionsBy
|
<SessionsBy
|
||||||
metric={_metric}
|
metric={_metric}
|
||||||
data={data}
|
data={data}
|
||||||
|
height={height}
|
||||||
onClick={onChartClick}
|
onClick={onChartClick}
|
||||||
isTemplate={isTemplate}
|
isTemplate={isTemplate}
|
||||||
/>
|
/>
|
||||||
|
|
@ -518,18 +531,18 @@ function WidgetChart(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <ClickMapCard />;
|
return <ClickMapCard height={height} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metricType === INSIGHTS) {
|
if (metricType === INSIGHTS) {
|
||||||
return <InsightsCard data={data} />;
|
return <InsightsCard height={height} data={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metricType === USER_PATH && data && data.links) {
|
if (metricType === USER_PATH && data && data.links) {
|
||||||
const isUngrouped = props.isPreview
|
const isUngrouped = props.isPreview
|
||||||
? !(_metric.hideExcess ?? true)
|
? !(_metric.hideExcess ?? true)
|
||||||
: false;
|
: false;
|
||||||
const height = props.isPreview ? 550 : 240;
|
const height = props.height ? props.height : props.isPreview ? 550 : 240;
|
||||||
return (
|
return (
|
||||||
<SankeyChart
|
<SankeyChart
|
||||||
height={height}
|
height={height}
|
||||||
|
|
@ -548,6 +561,7 @@ function WidgetChart(props: Props) {
|
||||||
if (viewType === 'trend') {
|
if (viewType === 'trend') {
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<LineChart
|
||||||
|
height={height}
|
||||||
data={data}
|
data={data}
|
||||||
colors={colors}
|
colors={colors}
|
||||||
params={params}
|
params={params}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ function WidgetPreview(props: Props) {
|
||||||
metric.viewType,
|
metric.viewType,
|
||||||
);
|
);
|
||||||
// [rangeStart, rangeEnd] or [period_name] -- have to check options
|
// [rangeStart, rangeEnd] or [period_name] -- have to check options
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// otherwise data obj change won't be registered if you get data -> change page -> go back
|
||||||
|
return () => metricStore.init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const presetComparison = metric.compareTo;
|
const presetComparison = metric.compareTo;
|
||||||
return (
|
return (
|
||||||
<div className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}>
|
<div className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Button, Dropdown, MenuProps, Modal } from 'antd';
|
import { Button, Dropdown, MenuProps } from 'antd';
|
||||||
import {
|
import {
|
||||||
BellIcon,
|
BellIcon,
|
||||||
EllipsisVertical,
|
EllipsisVertical,
|
||||||
|
|
@ -14,6 +14,7 @@ import { useModal } from 'Components/ModalContext';
|
||||||
import AlertFormModal from 'Components/Alerts/AlertFormModal/AlertFormModal';
|
import AlertFormModal from 'Components/Alerts/AlertFormModal/AlertFormModal';
|
||||||
import { showAddToDashboardModal } from 'Components/Dashboard/components/AddToDashboardButton';
|
import { showAddToDashboardModal } from 'Components/Dashboard/components/AddToDashboardButton';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { confirm } from 'UI';
|
||||||
|
|
||||||
function CardViewMenu() {
|
function CardViewMenu() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -51,29 +52,25 @@ function CardViewMenu() {
|
||||||
label: t('Delete'),
|
label: t('Delete'),
|
||||||
icon: <TrashIcon size={15} />,
|
icon: <TrashIcon size={15} />,
|
||||||
disabled: !widget.exists(),
|
disabled: !widget.exists(),
|
||||||
onClick: () => {
|
onClick: async () => {
|
||||||
Modal.confirm({
|
if (
|
||||||
title: t('Confirm Card Deletion'),
|
await confirm({
|
||||||
icon: null,
|
header: t('Remove Card'),
|
||||||
content:
|
confirmButton: t('Remove'),
|
||||||
t('Are you sure you want to remove this card? This action is permanent and cannot be undone.'),
|
confirmation: t(
|
||||||
footer: (_, { OkBtn, CancelBtn }) => (
|
'Are you sure you want to remove this card? This action is permanent and cannot be undone.',
|
||||||
<>
|
),
|
||||||
<CancelBtn />
|
})
|
||||||
<OkBtn />
|
) {
|
||||||
</>
|
metricStore
|
||||||
),
|
.delete(widget)
|
||||||
onOk: () => {
|
.then(() => {
|
||||||
metricStore
|
history.goBack();
|
||||||
.delete(widget)
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
history.goBack();
|
toast.error(t('Failed to remove card'));
|
||||||
})
|
});
|
||||||
.catch(() => {
|
}
|
||||||
toast.error(t('Failed to remove card'));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,6 @@ function WidgetView({
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const mk = params.get('mk');
|
const mk = params.get('mk');
|
||||||
if (mk) {
|
if (mk) {
|
||||||
metricStore.init();
|
|
||||||
const selectedCard = CARD_LIST(t).find((c) => c.key === mk) as CardType;
|
const selectedCard = CARD_LIST(t).find((c) => c.key === mk) as CardType;
|
||||||
if (selectedCard) {
|
if (selectedCard) {
|
||||||
const cardData: any = {
|
const cardData: any = {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Table, Tooltip } from 'antd';
|
import { Table, Dropdown } from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import Widget from 'App/mstore/types/widget';
|
import Widget from 'App/mstore/types/widget';
|
||||||
import Funnel from 'App/mstore/types/funnel';
|
import Funnel from 'App/mstore/types/funnel';
|
||||||
import { ItemMenu } from 'UI';
|
|
||||||
import { EllipsisVertical } from 'lucide-react';
|
import { EllipsisVertical } from 'lucide-react';
|
||||||
import { exportAntCsv } from '../../../utils';
|
import { exportAntCsv } from 'App/utils';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -111,19 +110,20 @@ export function TableExporter({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const onClick = () => exportAntCsv(tableColumns, tableData, filename);
|
const onClick = () => exportAntCsv(tableColumns, tableData, filename);
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('Export Data to CSV')}>
|
<div
|
||||||
<div className={`absolute ${top || 'top-0'} ${right || '-right-1'}`}>
|
className={`absolute ${top || 'top-0'} ${right || '-right-1'}`}
|
||||||
<ItemMenu
|
style={{ zIndex: 10 }}
|
||||||
items={[{ icon: 'download', text: 'Export to CSV', onClick }]}
|
>
|
||||||
bold
|
<Dropdown
|
||||||
customTrigger={
|
menu={{
|
||||||
<div className="flex items-center justify-center bg-gradient-to-r from-[#fafafa] to-neutral-200 cursor-pointer rounded-lg h-[38px] w-[38px] btn-export-table-data">
|
items: [{ key: 'download', label: 'Export to CSV', onClick }],
|
||||||
<EllipsisVertical size={16} />
|
}}
|
||||||
</div>
|
>
|
||||||
}
|
<div className="flex items-center justify-center bg-gray-lighter cursor-pointer rounded-lg h-[38px] w-[38px] btn-export-table-data">
|
||||||
/>
|
<EllipsisVertical size={16} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Dropdown>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,16 @@ import { useStore } from 'App/mstore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import ChatsModal from './components/ChatsModal';
|
import ChatsModal from './components/ChatsModal';
|
||||||
|
import { kaiStore } from './KaiStore';
|
||||||
|
|
||||||
function KaiChat() {
|
function KaiChat() {
|
||||||
const { userStore, projectsStore } = useStore();
|
const { userStore, projectsStore } = useStore();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [chatTitle, setTitle] = React.useState<string | null>(null);
|
const chatTitle = kaiStore.chatTitle;
|
||||||
|
const setTitle = kaiStore.setTitle;
|
||||||
const userId = userStore.account.id;
|
const userId = userStore.account.id;
|
||||||
const userLetter = userStore.account.name[0].toUpperCase();
|
const userName = userStore.account.name;
|
||||||
|
const limited = kaiStore.usage.percent >= 100;
|
||||||
const { activeSiteId } = projectsStore;
|
const { activeSiteId } = projectsStore;
|
||||||
const [section, setSection] = React.useState<'intro' | 'chat'>('intro');
|
const [section, setSection] = React.useState<'intro' | 'chat'>('intro');
|
||||||
const [threadId, setThreadId] = React.useState<string | null>(null);
|
const [threadId, setThreadId] = React.useState<string | null>(null);
|
||||||
|
|
@ -36,13 +39,18 @@ function KaiChat() {
|
||||||
showModal(
|
showModal(
|
||||||
<ChatsModal
|
<ChatsModal
|
||||||
projectId={activeSiteId!}
|
projectId={activeSiteId!}
|
||||||
|
onHide={hideModal}
|
||||||
onSelect={(threadId: string, title: string) => {
|
onSelect={(threadId: string, title: string) => {
|
||||||
setTitle(title);
|
setTitle(title);
|
||||||
setThreadId(threadId);
|
setThreadId(threadId);
|
||||||
hideModal();
|
hideModal();
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
{ right: true, width: 300 },
|
{
|
||||||
|
right: true,
|
||||||
|
width: 320,
|
||||||
|
className: 'bg-none flex items-center h-screen',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -91,44 +99,77 @@ function KaiChat() {
|
||||||
const newThread = await kaiService.createKaiChat(activeSiteId);
|
const newThread = await kaiService.createKaiChat(activeSiteId);
|
||||||
if (newThread) {
|
if (newThread) {
|
||||||
setThreadId(newThread.toString());
|
setThreadId(newThread.toString());
|
||||||
|
kaiStore.setTitle(null);
|
||||||
setSection('chat');
|
setSection('chat');
|
||||||
} else {
|
} else {
|
||||||
toast.error("Something wen't wrong. Please try again later.");
|
toast.error("Something wen't wrong. Please try again later.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
if (!threadId) return;
|
||||||
|
void kaiStore.cancelGeneration({
|
||||||
|
projectId: activeSiteId,
|
||||||
|
threadId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
<div
|
||||||
|
className="w-full mx-auto h-full"
|
||||||
|
style={{ maxWidth: PANEL_SIZES.maxWidth }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={'w-full rounded-lg overflow-hidden border shadow relative'}
|
className={
|
||||||
|
'w-full rounded-lg overflow-hidden bg-white relative h-full reset'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
chatTitle={chatTitle}
|
chatTitle={chatTitle}
|
||||||
openChats={openChats}
|
openChats={openChats}
|
||||||
goBack={goBack}
|
goBack={goBack}
|
||||||
|
onCreate={onCreate}
|
||||||
/>
|
/>
|
||||||
<div
|
{section === 'intro' ? (
|
||||||
className={
|
<>
|
||||||
'w-full bg-active-blue flex flex-col items-center justify-center py-4 relative'
|
<div
|
||||||
}
|
className={
|
||||||
style={{
|
'flex flex-col items-center justify-center py-4 relative'
|
||||||
height: '70svh',
|
}
|
||||||
background:
|
style={{
|
||||||
'radial-gradient(50% 50% at 50% 50%, var(--color-glassWhite) 0%, var(--color-glassMint) 46%, var(--color-glassLavander) 100%)',
|
position: 'absolute',
|
||||||
}}
|
top: '50%',
|
||||||
>
|
left: 0,
|
||||||
{section === 'intro' ? (
|
width: '100%',
|
||||||
<IntroSection onAsk={onCreate} />
|
transform: 'translateY(-50%)',
|
||||||
) : (
|
}}
|
||||||
<ChatLog
|
>
|
||||||
threadId={threadId}
|
<IntroSection
|
||||||
projectId={activeSiteId}
|
onCancel={onCancel}
|
||||||
userLetter={userLetter}
|
onAsk={onCreate}
|
||||||
onTitleChange={setTitle}
|
projectId={activeSiteId}
|
||||||
initialMsg={initialMsg}
|
userName={userName}
|
||||||
setInitialMsg={setInitialMsg}
|
limited={limited}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div
|
||||||
|
className={
|
||||||
|
'text-disabled-text absolute bottom-4 left-0 right-0 text-center text-sm'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
OpenReplay AI can make mistakes. Verify its outputs.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ChatLog
|
||||||
|
threadId={threadId}
|
||||||
|
projectId={activeSiteId}
|
||||||
|
chatTitle={chatTitle}
|
||||||
|
initialMsg={initialMsg}
|
||||||
|
setInitialMsg={setInitialMsg}
|
||||||
|
onCancel={onCancel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ export default class KaiService extends AiService {
|
||||||
getKaiChat = async (
|
getKaiChat = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
threadId: string,
|
threadId: string,
|
||||||
): Promise<
|
): Promise<{
|
||||||
{
|
messages: {
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
message_id: any;
|
message_id: any;
|
||||||
|
|
@ -36,8 +36,10 @@ export default class KaiService extends AiService {
|
||||||
supports_visualization: boolean;
|
supports_visualization: boolean;
|
||||||
chart: string;
|
chart: string;
|
||||||
chart_data: string;
|
chart_data: string;
|
||||||
}[]
|
sessions?: Record<string, any>[];
|
||||||
> => {
|
}[];
|
||||||
|
title: string;
|
||||||
|
}> => {
|
||||||
const r = await this.client.get(`/kai/${projectId}/chats/${threadId}`);
|
const r = await this.client.get(`/kai/${projectId}/chats/${threadId}`);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
throw new Error('Failed to fetch chat');
|
throw new Error('Failed to fetch chat');
|
||||||
|
|
@ -84,7 +86,7 @@ export default class KaiService extends AiService {
|
||||||
getMsgChart = async (
|
getMsgChart = async (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<{ filters: any[]; chart: string; eventsOrder: string }> => {
|
): Promise<string> => {
|
||||||
const r = await this.client.get(
|
const r = await this.client.get(
|
||||||
`/kai/${projectId}/chats/data/${messageId}`,
|
`/kai/${projectId}/chats/data/${messageId}`,
|
||||||
);
|
);
|
||||||
|
|
@ -122,4 +124,19 @@ export default class KaiService extends AiService {
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getPromptSuggestions = async (
|
||||||
|
projectId: string,
|
||||||
|
threadId?: string | null,
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const endpoint = threadId
|
||||||
|
? `/kai/${projectId}/chats/${threadId}/prompt-suggestions`
|
||||||
|
: `/kai/${projectId}/prompt-suggestions`;
|
||||||
|
const r = await this.client.get(endpoint);
|
||||||
|
if (!r.ok) {
|
||||||
|
throw new Error('Failed to fetch prompt suggestions');
|
||||||
|
}
|
||||||
|
const data = await r.json();
|
||||||
|
return data;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { BotChunk, ChatManager } from './SocketManager';
|
||||||
import { kaiService as aiService, kaiService } from 'App/services';
|
import { kaiService as aiService, kaiService } from 'App/services';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import Widget from 'App/mstore/types/widget';
|
import Widget from 'App/mstore/types/widget';
|
||||||
|
import Session, { ISession } from '@/types/session/session';
|
||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
text: string;
|
text: string;
|
||||||
|
|
@ -15,6 +16,7 @@ export interface Message {
|
||||||
supports_visualization: boolean;
|
supports_visualization: boolean;
|
||||||
feedback: boolean | null;
|
feedback: boolean | null;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
sessions?: Session[];
|
||||||
}
|
}
|
||||||
export interface SentMessage
|
export interface SentMessage
|
||||||
extends Omit<
|
extends Omit<
|
||||||
|
|
@ -29,6 +31,7 @@ class KaiStore {
|
||||||
processingStage: BotChunk | null = null;
|
processingStage: BotChunk | null = null;
|
||||||
messages: Array<Message> = [];
|
messages: Array<Message> = [];
|
||||||
queryText = '';
|
queryText = '';
|
||||||
|
chatTitle: string | null = null;
|
||||||
loadingChat = false;
|
loadingChat = false;
|
||||||
replacing: string | null = null;
|
replacing: string | null = null;
|
||||||
usage = {
|
usage = {
|
||||||
|
|
@ -56,6 +59,20 @@ class KaiStore {
|
||||||
return { msg, index };
|
return { msg, index };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get firstHumanMessage() {
|
||||||
|
let msg = null;
|
||||||
|
let index = null;
|
||||||
|
for (let i = 0; i < this.messages.length; i++) {
|
||||||
|
const message = this.messages[i];
|
||||||
|
if (message.isUser) {
|
||||||
|
msg = message;
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { msg, index };
|
||||||
|
}
|
||||||
|
|
||||||
get lastKaiMessage() {
|
get lastKaiMessage() {
|
||||||
let msg = null;
|
let msg = null;
|
||||||
let index = null;
|
let index = null;
|
||||||
|
|
@ -70,6 +87,14 @@ class KaiStore {
|
||||||
return { msg, index };
|
return { msg, index };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreviousMessage = (messageId: string) => {
|
||||||
|
const index = this.messages.findIndex((msg) => msg.messageId === messageId);
|
||||||
|
if (index > 0) {
|
||||||
|
return this.messages[index - 1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
setQueryText = (text: string) => {
|
setQueryText = (text: string) => {
|
||||||
this.queryText = text;
|
this.queryText = text;
|
||||||
};
|
};
|
||||||
|
|
@ -113,13 +138,21 @@ class KaiStore {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setTitle = (title: string | null) => {
|
||||||
|
this.chatTitle = title;
|
||||||
|
};
|
||||||
|
|
||||||
getChat = async (projectId: string, threadId: string) => {
|
getChat = async (projectId: string, threadId: string) => {
|
||||||
this.setLoadingChat(true);
|
this.setLoadingChat(true);
|
||||||
try {
|
try {
|
||||||
const res = await aiService.getKaiChat(projectId, threadId);
|
const { messages, title } = await aiService.getKaiChat(
|
||||||
if (res && res.length) {
|
projectId,
|
||||||
|
threadId,
|
||||||
|
);
|
||||||
|
if (messages && messages.length) {
|
||||||
|
this.setTitle(title);
|
||||||
this.setMessages(
|
this.setMessages(
|
||||||
res.map((m) => {
|
messages.map((m) => {
|
||||||
const isUser = m.role === 'human';
|
const isUser = m.role === 'human';
|
||||||
return {
|
return {
|
||||||
text: m.content,
|
text: m.content,
|
||||||
|
|
@ -130,6 +163,9 @@ class KaiStore {
|
||||||
chart: m.chart,
|
chart: m.chart,
|
||||||
supports_visualization: m.supports_visualization,
|
supports_visualization: m.supports_visualization,
|
||||||
chart_data: m.chart_data,
|
chart_data: m.chart_data,
|
||||||
|
sessions: m.sessions
|
||||||
|
? m.sessions.map((s) => new Session(s))
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -144,7 +180,6 @@ class KaiStore {
|
||||||
|
|
||||||
createChatManager = (
|
createChatManager = (
|
||||||
settings: { projectId: string; threadId: string },
|
settings: { projectId: string; threadId: string },
|
||||||
setTitle: (title: string) => void,
|
|
||||||
initialMsg: string | null,
|
initialMsg: string | null,
|
||||||
) => {
|
) => {
|
||||||
const token = kaiService.client.getJwt();
|
const token = kaiService.client.getJwt();
|
||||||
|
|
@ -190,6 +225,9 @@ class KaiStore {
|
||||||
chart: '',
|
chart: '',
|
||||||
supports_visualization: msg.supports_visualization,
|
supports_visualization: msg.supports_visualization,
|
||||||
chart_data: '',
|
chart_data: '',
|
||||||
|
sessions: msg.sessions
|
||||||
|
? msg.sessions.map((s) => new Session(s))
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
this.bumpUsage();
|
this.bumpUsage();
|
||||||
this.addMessage(msgObj);
|
this.addMessage(msgObj);
|
||||||
|
|
@ -197,7 +235,7 @@ class KaiStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
titleCallback: setTitle,
|
titleCallback: this.setTitle,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (initialMsg) {
|
if (initialMsg) {
|
||||||
|
|
@ -211,7 +249,13 @@ class KaiStore {
|
||||||
|
|
||||||
bumpUsage = () => {
|
bumpUsage = () => {
|
||||||
this.usage.used += 1;
|
this.usage.used += 1;
|
||||||
this.usage.percent = (this.usage.used / this.usage.total) * 100;
|
this.usage.percent = Math.min(
|
||||||
|
(this.usage.used / this.usage.total) * 100,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
if (this.usage.used >= this.usage.total) {
|
||||||
|
toast.error('You have reached the daily limit for queries.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessage = (message: string) => {
|
sendMessage = (message: string) => {
|
||||||
|
|
@ -232,7 +276,7 @@ class KaiStore {
|
||||||
deleting.push(this.lastKaiMessage.index);
|
deleting.push(this.lastKaiMessage.index);
|
||||||
}
|
}
|
||||||
this.deleteAtIndex(deleting);
|
this.deleteAtIndex(deleting);
|
||||||
this.setReplacing(false);
|
this.setReplacing(null);
|
||||||
}
|
}
|
||||||
this.addMessage({
|
this.addMessage({
|
||||||
text: message,
|
text: message,
|
||||||
|
|
@ -273,7 +317,6 @@ class KaiStore {
|
||||||
|
|
||||||
cancelGeneration = async (settings: {
|
cancelGeneration = async (settings: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
userId: string;
|
|
||||||
threadId: string;
|
threadId: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -298,6 +341,7 @@ class KaiStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
charts = new Map<string, Record<string, any>>();
|
||||||
getMessageChart = async (msgId: string, projectId: string) => {
|
getMessageChart = async (msgId: string, projectId: string) => {
|
||||||
this.setProcessingStage({
|
this.setProcessingStage({
|
||||||
content: 'Generating visualization...',
|
content: 'Generating visualization...',
|
||||||
|
|
@ -308,27 +352,16 @@ class KaiStore {
|
||||||
supports_visualization: false,
|
supports_visualization: false,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const filters = await kaiService.getMsgChart(msgId, projectId);
|
const filtersStr = await kaiService.getMsgChart(msgId, projectId);
|
||||||
|
if (!filtersStr.length) {
|
||||||
|
throw new Error('No filters found for the message');
|
||||||
|
}
|
||||||
|
const filters = JSON.parse(filtersStr);
|
||||||
const data = {
|
const data = {
|
||||||
metricId: undefined,
|
...filters,
|
||||||
dashboardId: undefined,
|
|
||||||
widgetId: undefined,
|
|
||||||
metricOf: undefined,
|
|
||||||
metricType: undefined,
|
|
||||||
metricFormat: undefined,
|
|
||||||
viewType: undefined,
|
|
||||||
name: 'Kai Visualization',
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'Kai Visualization',
|
|
||||||
filter: {
|
|
||||||
eventsOrder: filters.eventsOrder,
|
|
||||||
filters: filters.filters,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
const metric = new Widget().fromJson(data);
|
const metric = new Widget().fromJson(data);
|
||||||
|
this.charts.set(msgId, data);
|
||||||
return metric;
|
return metric;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -338,19 +371,31 @@ class KaiStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
saveLatestChart = async (msgId: string, projectId: string) => {
|
||||||
|
const data = this.charts.get(msgId);
|
||||||
|
if (data) {
|
||||||
|
try {
|
||||||
|
await kaiService.saveChartData(msgId, projectId, data);
|
||||||
|
this.charts.delete(msgId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getParsedChart = (data: string) => {
|
getParsedChart = (data: string) => {
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData = JSON.parse(data);
|
||||||
return new Widget().fromJson(parsedData);
|
return new Widget().fromJson(parsedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setUsage = (usage: { total: number; used: number; percent: number }) => {
|
||||||
|
this.usage = usage;
|
||||||
|
};
|
||||||
|
|
||||||
checkUsage = async () => {
|
checkUsage = async () => {
|
||||||
try {
|
try {
|
||||||
const { total, used } = await kaiService.checkUsage();
|
const { total, used } = await kaiService.checkUsage();
|
||||||
this.usage = {
|
this.setUsage({ total, used, percent: Math.round((used / total) * 100) });
|
||||||
total,
|
|
||||||
used,
|
|
||||||
percent: (used / total) * 100,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import io from 'socket.io-client';
|
import io from 'socket.io-client';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { ISession } from '@/types/session/session';
|
||||||
|
|
||||||
export class ChatManager {
|
export class ChatManager {
|
||||||
socket: ReturnType<typeof io>;
|
socket: ReturnType<typeof io>;
|
||||||
|
|
@ -77,9 +78,7 @@ export class ChatManager {
|
||||||
msgCallback,
|
msgCallback,
|
||||||
titleCallback,
|
titleCallback,
|
||||||
}: {
|
}: {
|
||||||
msgCallback: (
|
msgCallback: (msg: StateEvent | BotChunk) => void;
|
||||||
msg: StateEvent | BotChunk,
|
|
||||||
) => void;
|
|
||||||
titleCallback: (title: string) => void;
|
titleCallback: (title: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
this.socket.on('chunk', (msg: BotChunk) => {
|
this.socket.on('chunk', (msg: BotChunk) => {
|
||||||
|
|
@ -111,7 +110,8 @@ export interface BotChunk {
|
||||||
messageId: string;
|
messageId: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
supports_visualization: boolean;
|
supports_visualization: boolean;
|
||||||
type: 'chunk'
|
sessions?: ISession[];
|
||||||
|
type: 'chunk';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateEvent {
|
interface StateEvent {
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,58 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import { MessagesSquare, ArrowLeft } from 'lucide-react';
|
import { MessagesSquare, ArrowLeft, SquarePen } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function ChatHeader({
|
function ChatHeader({
|
||||||
openChats = () => {},
|
openChats = () => {},
|
||||||
goBack,
|
goBack,
|
||||||
chatTitle,
|
chatTitle,
|
||||||
|
onCreate,
|
||||||
}: {
|
}: {
|
||||||
goBack?: () => void;
|
goBack?: () => void;
|
||||||
openChats?: () => void;
|
openChats?: () => void;
|
||||||
chatTitle: string | null;
|
chatTitle: string | null;
|
||||||
|
onCreate: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="p-4 pb-0 w-full">
|
||||||
className={
|
<div
|
||||||
'px-4 py-2 flex items-center bg-white border-b border-b-gray-lighter'
|
className={'px-4 py-2 flex items-center bg-gray-lightest rounded-lg'}
|
||||||
}
|
>
|
||||||
>
|
<div className={'flex-1'}>
|
||||||
<div className={'flex-1'}>
|
{goBack ? (
|
||||||
{goBack ? (
|
<div
|
||||||
|
className={
|
||||||
|
'w-fit flex items-center gap-2 font-semibold cursor-pointer hover:text-main'
|
||||||
|
}
|
||||||
|
onClick={goBack}
|
||||||
|
>
|
||||||
|
<SquarePen size={14} />
|
||||||
|
<div>{t('New Chat')}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon name={'kai-mono'} size={18} />
|
||||||
|
<div className={'font-semibold text-xl'}>Kai</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={'flex items-center gap-2 mx-auto max-w-[80%]'}>
|
||||||
|
{chatTitle ? (
|
||||||
|
<div className="font-semibold text-xl whitespace-nowrap truncate">
|
||||||
|
{chatTitle}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className={'flex-1 justify-end flex items-center'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className="font-semibold w-fit cursor-pointer hover:text-main flex items-center gap-2"
|
||||||
'w-fit flex items-center gap-2 font-semibold cursor-pointer'
|
onClick={openChats}
|
||||||
}
|
|
||||||
onClick={goBack}
|
|
||||||
>
|
>
|
||||||
<ArrowLeft size={14} />
|
<MessagesSquare size={14} />
|
||||||
<div>{t('Back')}</div>
|
<div>{t('Chats')}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className={'flex items-center gap-2 mx-auto max-w-[80%]'}>
|
|
||||||
{chatTitle ? (
|
|
||||||
<div className="font-semibold text-xl whitespace-nowrap truncate">
|
|
||||||
{chatTitle}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name={'kai_colored'} size={18} />
|
|
||||||
<div className={'font-semibold text-xl'}>Kai</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={'flex-1 justify-end flex items-center gap-2'}>
|
|
||||||
<div
|
|
||||||
className="font-semibold w-fit cursor-pointer flex items-center gap-2"
|
|
||||||
onClick={openChats}
|
|
||||||
>
|
|
||||||
<MessagesSquare size={14} />
|
|
||||||
<div>{t('Chats')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Input, Tooltip } from 'antd';
|
import { Button, Input, Tooltip } from 'antd';
|
||||||
import { SendHorizonal, OctagonX } from 'lucide-react';
|
import { X, ArrowUp } from 'lucide-react';
|
||||||
import { kaiStore } from '../KaiStore';
|
import { kaiStore } from '../KaiStore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import Usage from './Usage';
|
import Usage from './Usage';
|
||||||
|
|
@ -8,11 +8,13 @@ import Usage from './Usage';
|
||||||
function ChatInput({
|
function ChatInput({
|
||||||
isLoading,
|
isLoading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
threadId,
|
isArea,
|
||||||
|
onCancel,
|
||||||
}: {
|
}: {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onSubmit: (str: string) => void;
|
onSubmit: (str: string) => void;
|
||||||
threadId: string;
|
onCancel: () => void;
|
||||||
|
isArea?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const inputRef = React.useRef<typeof Input>(null);
|
const inputRef = React.useRef<typeof Input>(null);
|
||||||
const usage = kaiStore.usage;
|
const usage = kaiStore.usage;
|
||||||
|
|
@ -23,13 +25,14 @@ function ChatInput({
|
||||||
kaiStore.setQueryText(text);
|
kaiStore.setQueryText(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = () => {
|
const submit = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
if (limited) {
|
if (limited) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
const settings = { projectId: '2325', userId: '0', threadId };
|
onCancel();
|
||||||
void kaiStore.cancelGeneration(settings);
|
|
||||||
} else {
|
} else {
|
||||||
if (inputValue.length > 0) {
|
if (inputValue.length > 0) {
|
||||||
onSubmit(inputValue);
|
onSubmit(inputValue);
|
||||||
|
|
@ -50,9 +53,36 @@ function ChatInput({
|
||||||
}, [inputValue]);
|
}, [inputValue]);
|
||||||
|
|
||||||
const isReplacing = kaiStore.replacing !== null;
|
const isReplacing = kaiStore.replacing !== null;
|
||||||
|
const placeholder = limited
|
||||||
|
? `You've reached the daily limit for queries, come again tomorrow!`
|
||||||
|
: 'Ask anything about your product and users...';
|
||||||
|
if (isArea) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
className="!resize-none rounded-lg shadow"
|
||||||
|
onPressEnter={submit}
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder={placeholder}
|
||||||
|
size={'large'}
|
||||||
|
disabled={limited}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-2 right-2">
|
||||||
|
<SendButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
submit={submit}
|
||||||
|
limited={limited}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative bg-white">
|
||||||
<Input
|
<Input
|
||||||
onPressEnter={submit}
|
onPressEnter={submit}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
|
@ -61,12 +91,9 @@ function ChatInput({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={
|
placeholder={placeholder}
|
||||||
limited
|
|
||||||
? `You've reached the daily limit for queries, come again tomorrow!`
|
|
||||||
: 'Ask anything about your product and users...'
|
|
||||||
}
|
|
||||||
size={'large'}
|
size={'large'}
|
||||||
|
className="rounded-lg shadow"
|
||||||
disabled={limited}
|
disabled={limited}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
|
@ -76,31 +103,19 @@ function ChatInput({
|
||||||
<Tooltip title={'Cancel Editing'}>
|
<Tooltip title={'Cancel Editing'}>
|
||||||
<Button
|
<Button
|
||||||
onClick={cancelReplace}
|
onClick={cancelReplace}
|
||||||
icon={<OctagonX size={16} />}
|
icon={<X className="reset" size={16} />}
|
||||||
type={'text'}
|
|
||||||
size={'small'}
|
size={'small'}
|
||||||
shape={'circle'}
|
shape={'circle'}
|
||||||
disabled={limited}
|
disabled={limited}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
<Tooltip title={'Send message'}>
|
<SendButton
|
||||||
<Button
|
isLoading={isLoading}
|
||||||
loading={isLoading}
|
submit={submit}
|
||||||
onClick={submit}
|
limited={limited}
|
||||||
disabled={limited}
|
isProcessing={isProcessing}
|
||||||
icon={
|
/>
|
||||||
isProcessing ? (
|
|
||||||
<OctagonX size={16} />
|
|
||||||
) : (
|
|
||||||
<SendHorizonal size={16} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
type={'text'}
|
|
||||||
size={'small'}
|
|
||||||
shape={'circle'}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -111,4 +126,42 @@ function ChatInput({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SendButton({
|
||||||
|
isLoading,
|
||||||
|
submit,
|
||||||
|
limited,
|
||||||
|
isProcessing,
|
||||||
|
}: {
|
||||||
|
isLoading?: boolean;
|
||||||
|
submit: () => void;
|
||||||
|
limited: boolean;
|
||||||
|
isProcessing?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tooltip title={isProcessing ? 'Cancel processing' : 'Send message'}>
|
||||||
|
<Button
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={submit}
|
||||||
|
disabled={limited}
|
||||||
|
icon={
|
||||||
|
isProcessing ? (
|
||||||
|
<X size={16} strokeWidth={2} className="reset" />
|
||||||
|
) : (
|
||||||
|
<div className="bg-[#fff] text-main rounded-full">
|
||||||
|
<ArrowUp size={14} strokeWidth={2} className="reset" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type={'primary'}
|
||||||
|
size={'small'}
|
||||||
|
shape={isProcessing ? 'circle' : 'round'}
|
||||||
|
iconPosition={'end'}
|
||||||
|
className="font-semibold text-[#fff]"
|
||||||
|
>
|
||||||
|
{isProcessing ? null : 'Ask'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default observer(ChatInput);
|
export default observer(ChatInput);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ChatInput from './ChatInput';
|
import ChatInput from './ChatInput';
|
||||||
import ChatMsg, { ChatNotice } from './ChatMsg';
|
import ChatMsg, { ChatNotice } from './ChatMsg';
|
||||||
|
import Ideas from './Ideas';
|
||||||
import { Loader } from 'UI';
|
import { Loader } from 'UI';
|
||||||
import { kaiStore } from '../KaiStore';
|
import { kaiStore } from '../KaiStore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import EmbedPlayer from './EmbedPlayer';
|
||||||
|
|
||||||
function ChatLog({
|
function ChatLog({
|
||||||
projectId,
|
projectId,
|
||||||
threadId,
|
threadId,
|
||||||
userLetter,
|
|
||||||
onTitleChange,
|
|
||||||
initialMsg,
|
initialMsg,
|
||||||
|
chatTitle,
|
||||||
setInitialMsg,
|
setInitialMsg,
|
||||||
|
onCancel,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
threadId: any;
|
threadId: any;
|
||||||
userLetter: string;
|
|
||||||
onTitleChange: (title: string | null) => void;
|
|
||||||
initialMsg: string | null;
|
initialMsg: string | null;
|
||||||
setInitialMsg: (msg: string | null) => void;
|
setInitialMsg: (msg: string | null) => void;
|
||||||
|
chatTitle: string | null;
|
||||||
|
onCancel: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [embedSession, setEmbedSession] = React.useState<any>(null);
|
||||||
const messages = kaiStore.messages;
|
const messages = kaiStore.messages;
|
||||||
const loading = kaiStore.loadingChat;
|
const loading = kaiStore.loadingChat;
|
||||||
const chatRef = React.useRef<HTMLDivElement>(null);
|
const chatRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -31,7 +34,7 @@ function ChatLog({
|
||||||
void kaiStore.getChat(settings.projectId, threadId);
|
void kaiStore.getChat(settings.projectId, threadId);
|
||||||
}
|
}
|
||||||
if (threadId) {
|
if (threadId) {
|
||||||
kaiStore.createChatManager(settings, onTitleChange, initialMsg);
|
kaiStore.createChatManager(settings, initialMsg);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
kaiStore.clearChat();
|
kaiStore.clearChat();
|
||||||
|
|
@ -50,23 +53,38 @@ function ChatLog({
|
||||||
});
|
});
|
||||||
}, [messages.length, processingStage]);
|
}, [messages.length, processingStage]);
|
||||||
|
|
||||||
const lastHumanMsgInd: null | number = kaiStore.lastHumanMessage.index;
|
const lastKaiMessageInd: null | number = kaiStore.lastKaiMessage.index;
|
||||||
|
const lastHumanMsgInd: number | null = kaiStore.lastHumanMessage.index;
|
||||||
|
const showIdeas =
|
||||||
|
!processingStage && lastKaiMessageInd === messages.length - 1;
|
||||||
return (
|
return (
|
||||||
<Loader loading={loading} className={'w-full h-full'}>
|
<Loader loading={loading} className={'w-full h-full'}>
|
||||||
<div
|
<div
|
||||||
ref={chatRef}
|
ref={chatRef}
|
||||||
|
style={{ maxHeight: 'calc(100svh - 165px)' }}
|
||||||
className={
|
className={
|
||||||
'overflow-y-auto relative flex flex-col items-center justify-between w-full h-full'
|
'overflow-y-auto relative flex flex-col items-center justify-between w-full h-full pt-4'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col gap-4 w-2/3 min-h-max'}>
|
{embedSession ? (
|
||||||
|
<EmbedPlayer
|
||||||
|
session={embedSession}
|
||||||
|
onClose={() => setEmbedSession(null)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className={'flex flex-col gap-2 w-2/3 min-h-max'}>
|
||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<React.Fragment key={msg.messageId ?? index}>
|
<React.Fragment key={msg.messageId ?? index}>
|
||||||
<ChatMsg
|
<ChatMsg
|
||||||
userName={userLetter}
|
|
||||||
siteId={projectId}
|
siteId={projectId}
|
||||||
message={msg}
|
message={msg}
|
||||||
canEdit={processingStage === null && msg.isUser && index === lastHumanMsgInd}
|
chatTitle={chatTitle}
|
||||||
|
onReplay={(session) => setEmbedSession(session)}
|
||||||
|
canEdit={
|
||||||
|
processingStage === null &&
|
||||||
|
msg.isUser &&
|
||||||
|
index === lastHumanMsgInd
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
@ -76,9 +94,17 @@ function ChatLog({
|
||||||
duration={processingStage.duration}
|
duration={processingStage.duration}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showIdeas ? (
|
||||||
|
<Ideas
|
||||||
|
onClick={(query) => onSubmit(query)}
|
||||||
|
projectId={projectId}
|
||||||
|
threadId={threadId}
|
||||||
|
inChat
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={'sticky bottom-0 pt-6 w-2/3'}>
|
<div className={'sticky bottom-0 pt-6 w-2/3 z-50'}>
|
||||||
<ChatInput onSubmit={onSubmit} threadId={threadId} />
|
<ChatInput onCancel={onCancel} onSubmit={onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon, CopyButton } from 'UI';
|
import { CopyButton, Icon } from 'UI';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import Markdown from 'react-markdown';
|
import Markdown from 'react-markdown';
|
||||||
|
|
@ -8,8 +8,7 @@ import {
|
||||||
Loader,
|
Loader,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
ThumbsDown,
|
ThumbsDown,
|
||||||
ListRestart,
|
SquarePen,
|
||||||
FileDown,
|
|
||||||
Clock,
|
Clock,
|
||||||
ChartLine,
|
ChartLine,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
@ -20,17 +19,22 @@ import { durationFormatted } from 'App/date';
|
||||||
import WidgetChart from '@/components/Dashboard/components/WidgetChart';
|
import WidgetChart from '@/components/Dashboard/components/WidgetChart';
|
||||||
import Widget from 'App/mstore/types/widget';
|
import Widget from 'App/mstore/types/widget';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import SessionItem from 'Shared/SessionItem';
|
||||||
|
|
||||||
function ChatMsg({
|
function ChatMsg({
|
||||||
userName,
|
userName,
|
||||||
siteId,
|
siteId,
|
||||||
canEdit,
|
canEdit,
|
||||||
message,
|
message,
|
||||||
|
chatTitle,
|
||||||
|
onReplay,
|
||||||
}: {
|
}: {
|
||||||
message: Message;
|
message: Message;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
|
chatTitle: string | null;
|
||||||
|
onReplay: (session: any) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [metric, setMetric] = React.useState<Widget | null>(null);
|
const [metric, setMetric] = React.useState<Widget | null>(null);
|
||||||
|
|
@ -47,13 +51,14 @@ function ChatMsg({
|
||||||
const isEditing = kaiStore.replacing && messageId === kaiStore.replacing;
|
const isEditing = kaiStore.replacing && messageId === kaiStore.replacing;
|
||||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||||
const bodyRef = React.useRef<HTMLDivElement>(null);
|
const bodyRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = React.useRef<HTMLDivElement>(null);
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
kaiStore.editMessage(text, messageId);
|
kaiStore.editMessage(text, messageId);
|
||||||
};
|
};
|
||||||
const onCancelEdit = () => {
|
const onCancelEdit = () => {
|
||||||
kaiStore.setQueryText('');
|
kaiStore.setQueryText('');
|
||||||
kaiStore.setReplacing(null);
|
kaiStore.setReplacing(null);
|
||||||
}
|
};
|
||||||
const onFeedback = (feedback: 'like' | 'dislike', messageId: string) => {
|
const onFeedback = (feedback: 'like' | 'dislike', messageId: string) => {
|
||||||
kaiStore.sendMsgFeedback(feedback, messageId, siteId);
|
kaiStore.sendMsgFeedback(feedback, messageId, siteId);
|
||||||
};
|
};
|
||||||
|
|
@ -65,19 +70,79 @@ function ChatMsg({
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const userPrompt = kaiStore.getPreviousMessage(message.messageId);
|
||||||
import('jspdf')
|
import('jspdf')
|
||||||
.then(({ jsPDF }) => {
|
.then(async ({ jsPDF }) => {
|
||||||
const doc = new jsPDF();
|
const doc = new jsPDF();
|
||||||
doc.addImage('/assets/img/logo-img.png', 80, 3, 30, 5);
|
const blockWidth = 170; // mm
|
||||||
doc.html(bodyRef.current!, {
|
const content = bodyRef.current!.cloneNode(true) as HTMLElement;
|
||||||
|
if (userPrompt) {
|
||||||
|
const titleHeader = document.createElement('h2');
|
||||||
|
titleHeader.textContent = userPrompt.text;
|
||||||
|
titleHeader.style.marginBottom = '10px';
|
||||||
|
content.prepend(titleHeader);
|
||||||
|
}
|
||||||
|
// insert logo /assets/img/logo-img.png
|
||||||
|
const logo = new Image();
|
||||||
|
logo.src = '/assets/img/logo-img.png';
|
||||||
|
logo.style.width = '130px';
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.display = 'flex';
|
||||||
|
container.style.alignItems = 'center';
|
||||||
|
container.style.justifyContent = 'center';
|
||||||
|
container.style.marginBottom = '10mm';
|
||||||
|
container.style.width = `${blockWidth}mm`;
|
||||||
|
container.appendChild(logo);
|
||||||
|
content.prepend(container);
|
||||||
|
content.querySelectorAll('ul').forEach((ul) => {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
ul.querySelectorAll('li').forEach((li) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = '• ' + li.textContent;
|
||||||
|
frag.appendChild(div);
|
||||||
|
});
|
||||||
|
ul.replaceWith(frag);
|
||||||
|
});
|
||||||
|
content.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach((el) => {
|
||||||
|
(el as HTMLElement).style.letterSpacing = '0.5px';
|
||||||
|
});
|
||||||
|
content.querySelectorAll('*').forEach((node) => {
|
||||||
|
node.childNodes.forEach((child) => {
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
const txt = child.textContent || '';
|
||||||
|
const replaced = txt.replace(/-/g, '–');
|
||||||
|
if (replaced !== txt) child.textContent = replaced;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (metric && chartRef.current) {
|
||||||
|
const { default: html2canvas } = await import('html2canvas');
|
||||||
|
const metricContainer = chartRef.current;
|
||||||
|
const image = await html2canvas(metricContainer, {
|
||||||
|
backgroundColor: null,
|
||||||
|
scale: 2,
|
||||||
|
});
|
||||||
|
const imgData = image.toDataURL('image/png');
|
||||||
|
const imgHeight = (image.height * blockWidth) / image.width;
|
||||||
|
content.appendChild(
|
||||||
|
Object.assign(document.createElement('img'), {
|
||||||
|
src: imgData,
|
||||||
|
style: `width: ${blockWidth}mm; height: ${imgHeight}mm; margin-top: 10px;`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
doc.html(content, {
|
||||||
callback: function (doc) {
|
callback: function (doc) {
|
||||||
doc.save('document.pdf');
|
doc.save((chatTitle ?? 'document') + '.pdf');
|
||||||
},
|
},
|
||||||
margin: [10, 10, 10, 10],
|
// top, bottom, ?, left
|
||||||
|
margin: [10, 10, 20, 20],
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 190, // Target width
|
// Target width
|
||||||
windowWidth: 675, // Window width for rendering
|
width: blockWidth,
|
||||||
|
// Window width for rendering
|
||||||
|
windowWidth: 675,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|
@ -107,35 +172,25 @@ function ChatMsg({
|
||||||
setLoadingChart(false);
|
setLoadingChart(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metricData = metric?.data;
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!chart_data && metricData) {
|
||||||
|
const hasValues =
|
||||||
|
metricData.values?.length > 0 || metricData.chart?.length > 0;
|
||||||
|
if (hasValues) {
|
||||||
|
kaiStore.saveLatestChart(messageId, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [metricData, chart_data]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn('flex gap-2', isUser ? 'flex-row-reverse' : 'flex-row')}>
|
||||||
className={cn(
|
<div className={'mt-1 flex flex-col group/actions max-w-[60svw]'}>
|
||||||
'flex items-start gap-2',
|
|
||||||
isUser ? 'flex-row-reverse' : 'flex-row',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isUser ? (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'rounded-full bg-main text-white min-w-8 min-h-8 flex items-center justify-center sticky top-0 shadow'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className={'font-semibold'}>{userName}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'rounded-full bg-gray-lightest shadow min-w-8 min-h-8 flex items-center justify-center sticky top-0'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon name={'kai_colored'} size={18} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={'mt-1 flex flex-col'}>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'markdown-body',
|
'markdown-body',
|
||||||
isEditing ? 'border-l border-l-main pl-2' : '',
|
isUser ? 'bg-gray-lighter px-4 rounded-full' : '',
|
||||||
|
isEditing ? '!bg-active-blue' : '',
|
||||||
)}
|
)}
|
||||||
data-openreplay-obscured
|
data-openreplay-obscured
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
|
|
@ -143,36 +198,56 @@ function ChatMsg({
|
||||||
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
|
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
{metric ? (
|
{metric ? (
|
||||||
<div className="p-2 border-gray-light rounded-lg shadow">
|
<div
|
||||||
<WidgetChart metric={metric} isPreview />
|
ref={chartRef}
|
||||||
|
className="p-2 border-gray-light rounded-lg shadow bg-glassWhite mb-2"
|
||||||
|
>
|
||||||
|
<WidgetChart metric={metric} isPreview height={360} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{message.sessions ? (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{message.sessions.map((session) => (
|
||||||
|
<div className="shadow border rounded-2xl overflow-hidden mb-2">
|
||||||
|
<SessionItem
|
||||||
|
disableUser
|
||||||
|
key={session.sessionId}
|
||||||
|
session={session}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onReplay(session);
|
||||||
|
}}
|
||||||
|
slim
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<>
|
<div className="invisible group-hover/actions:visible mt-1 ml-auto flex gap-2 items-center">
|
||||||
<div
|
{canEdit && !isEditing ? (
|
||||||
onClick={onEdit}
|
<IconButton onClick={onEdit} tooltip={t('Edit')}>
|
||||||
className={cn(
|
<SquarePen size={16} />
|
||||||
'ml-auto flex items-center gap-2 px-2',
|
</IconButton>
|
||||||
'rounded-lg border border-gray-medium text-sm cursor-pointer',
|
) : null}
|
||||||
'hover:border-main hover:text-main w-fit',
|
{isEditing ? (
|
||||||
canEdit && !isEditing ? '' : 'hidden',
|
<Button
|
||||||
)}
|
onClick={onCancelEdit}
|
||||||
>
|
type="text"
|
||||||
<ListRestart size={16} />
|
size="small"
|
||||||
<div>{t('Edit')}</div>
|
className={'text-xs'}
|
||||||
</div>
|
>
|
||||||
<div
|
{t('Cancel')}
|
||||||
onClick={onCancelEdit}
|
</Button>
|
||||||
className={cn(
|
) : null}
|
||||||
'ml-auto flex items-center gap-2 px-2',
|
<CopyButton
|
||||||
'rounded-lg border border-gray-medium text-sm cursor-pointer',
|
getHtml={() => bodyRef.current?.innerHTML}
|
||||||
'hover:border-main hover:text-main w-fit',
|
content={text}
|
||||||
isEditing ? '' : 'hidden',
|
isIcon
|
||||||
)}
|
format={'text/html'}
|
||||||
>
|
/>
|
||||||
<div>{t('Cancel')}</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={'flex items-center gap-2'}>
|
<div className={'flex items-center gap-2'}>
|
||||||
{duration ? <MsgDuration duration={duration} /> : null}
|
{duration ? <MsgDuration duration={duration} /> : null}
|
||||||
|
|
@ -182,14 +257,14 @@ function ChatMsg({
|
||||||
tooltip="Like this answer"
|
tooltip="Like this answer"
|
||||||
onClick={() => onFeedback('like', messageId)}
|
onClick={() => onFeedback('like', messageId)}
|
||||||
>
|
>
|
||||||
<ThumbsUp size={16} />
|
<ThumbsUp strokeWidth={2} size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
active={feedback === false}
|
active={feedback === false}
|
||||||
tooltip="Dislike this answer"
|
tooltip="Dislike this answer"
|
||||||
onClick={() => onFeedback('dislike', messageId)}
|
onClick={() => onFeedback('dislike', messageId)}
|
||||||
>
|
>
|
||||||
<ThumbsDown size={16} />
|
<ThumbsDown strokeWidth={2} size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{supports_visualization ? (
|
{supports_visualization ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
@ -197,7 +272,7 @@ function ChatMsg({
|
||||||
onClick={getChart}
|
onClick={getChart}
|
||||||
processing={loadingChart}
|
processing={loadingChart}
|
||||||
>
|
>
|
||||||
<ChartLine size={16} />
|
<ChartLine strokeWidth={2} size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) : null}
|
) : null}
|
||||||
<CopyButton
|
<CopyButton
|
||||||
|
|
@ -211,7 +286,7 @@ function ChatMsg({
|
||||||
tooltip="Export as PDF"
|
tooltip="Export as PDF"
|
||||||
onClick={onExport}
|
onClick={onExport}
|
||||||
>
|
>
|
||||||
<FileDown size={16} />
|
<Icon name="export-pdf" size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { splitByDate } from '../utils';
|
import { splitByDate } from '../utils';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { MessagesSquare, Trash } from 'lucide-react';
|
import { MessagesSquare, Trash, X } from 'lucide-react';
|
||||||
import { kaiService } from 'App/services';
|
import { kaiService } from 'App/services';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -11,9 +11,11 @@ import { observer } from 'mobx-react-lite';
|
||||||
function ChatsModal({
|
function ChatsModal({
|
||||||
onSelect,
|
onSelect,
|
||||||
projectId,
|
projectId,
|
||||||
|
onHide,
|
||||||
}: {
|
}: {
|
||||||
onSelect: (threadId: string, title: string) => void;
|
onSelect: (threadId: string, title: string) => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
onHide: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { usage } = kaiStore;
|
const { usage } = kaiStore;
|
||||||
|
|
@ -44,10 +46,22 @@ function ChatsModal({
|
||||||
refetch();
|
refetch();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={'h-screen w-full flex flex-col gap-2 p-4'}>
|
<div
|
||||||
|
className={'flex flex-col gap-2 p-4 mr-1 rounded-lg bg-white'}
|
||||||
|
style={{ height: 'calc(-100px + 100svh)', marginTop: 60, width: 310 }}
|
||||||
|
>
|
||||||
<div className={'flex items-center font-semibold text-lg gap-2'}>
|
<div className={'flex items-center font-semibold text-lg gap-2'}>
|
||||||
<MessagesSquare size={16} />
|
<MessagesSquare size={16} />
|
||||||
<span>{t('Chats')}</span>
|
<span>{t('Previous Chats')}</span>
|
||||||
|
<div className="ml-auto" />
|
||||||
|
<div>
|
||||||
|
<X
|
||||||
|
size={16}
|
||||||
|
strokeWidth={2}
|
||||||
|
className="cursor-pointer hover:text-main"
|
||||||
|
onClick={onHide}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{usage.percent > 80 ? (
|
{usage.percent > 80 ? (
|
||||||
<div className="text-red text-sm">
|
<div className="text-red text-sm">
|
||||||
|
|
@ -58,16 +72,20 @@ function ChatsModal({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className="animate-pulse text-disabled-text">{t('Loading chats')}...</div>
|
<div className="animate-pulse text-disabled-text">
|
||||||
|
{t('Loading chats')}...
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-y-auto flex flex-col gap-2">
|
<div className="overflow-y-auto flex flex-col gap-2">
|
||||||
{datedCollections.map((col) => (
|
{datedCollections.map((col, i) => (
|
||||||
<ChatCollection
|
<React.Fragment key={`${i}_${col.date}`}>
|
||||||
data={col.entries}
|
<ChatCollection
|
||||||
date={col.date}
|
data={col.entries}
|
||||||
onSelect={onSelect}
|
date={col.date}
|
||||||
onDelete={onDelete}
|
onSelect={onSelect}
|
||||||
/>
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -87,8 +105,8 @@ function ChatCollection({
|
||||||
date: string;
|
date: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="border-b border-b-gray-lighter py-2">
|
||||||
<div className="text-disabled-text">{date}</div>
|
<div className="font-semibold">{date}</div>
|
||||||
<ChatsList data={data} onSelect={onSelect} onDelete={onDelete} />
|
<ChatsList data={data} onSelect={onSelect} onDelete={onDelete} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
75
frontend/app/components/Kai/components/EmbedPlayer.tsx
Normal file
75
frontend/app/components/Kai/components/EmbedPlayer.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import { useStore } from 'App/mstore';
|
||||||
|
import { Loader } from 'UI';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import MobileClipsPlayer from 'App/components/Session/MobileClipsPlayer';
|
||||||
|
import ClipsPlayer from 'App/components/Session/ClipsPlayer';
|
||||||
|
import Session from '@/types/session/session';
|
||||||
|
|
||||||
|
interface Clip {
|
||||||
|
sessionId: string | undefined;
|
||||||
|
range: [number, number];
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbedPlayer({
|
||||||
|
session,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
session: Session;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const clip = {
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
range: [0, session.durationMs],
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
const { isMobile } = projectsStore;
|
||||||
|
|
||||||
|
const onBgClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-screen h-screen fixed top-0 left-0 flex items-center justify-center"
|
||||||
|
style={{ zIndex: 100, background: 'rgba(0,0,0, 0.15)' }}
|
||||||
|
onClick={onBgClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-lg overflow-hidden',
|
||||||
|
'rounded shadow boarder bg-white',
|
||||||
|
)}
|
||||||
|
style={{ width: 960 }}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<MobileClipsPlayer
|
||||||
|
isHighlight
|
||||||
|
onClose={onClose}
|
||||||
|
clip={clip}
|
||||||
|
currentIndex={0}
|
||||||
|
isCurrent
|
||||||
|
autoplay={false}
|
||||||
|
isFull
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ClipsPlayer
|
||||||
|
isHighlight
|
||||||
|
onClose={onClose}
|
||||||
|
clip={clip}
|
||||||
|
currentIndex={0}
|
||||||
|
isCurrent
|
||||||
|
autoplay={false}
|
||||||
|
isFull
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(EmbedPlayer);
|
||||||
|
|
@ -1,30 +1,84 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Lightbulb, MoveRight } from 'lucide-react';
|
import cn from 'classnames';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { kaiService } from 'App/services';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function Ideas({ onClick }: { onClick: (query: string) => void }) {
|
function Ideas({
|
||||||
|
onClick,
|
||||||
|
projectId,
|
||||||
|
threadId = null,
|
||||||
|
inChat,
|
||||||
|
limited,
|
||||||
|
}: {
|
||||||
|
onClick: (query: string) => void;
|
||||||
|
projectId: string;
|
||||||
|
threadId?: string | null;
|
||||||
|
inChat?: boolean;
|
||||||
|
limited?: boolean;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data: suggestedPromptIdeas = [], isPending } = useQuery({
|
||||||
|
queryKey: ['kai', projectId, 'chats', threadId, 'prompt-suggestions'],
|
||||||
|
queryFn: () => kaiService.getPromptSuggestions(projectId, threadId),
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
});
|
||||||
|
const ideas = React.useMemo(() => {
|
||||||
|
const defaultPromptIdeas = [
|
||||||
|
'Top user journeys',
|
||||||
|
'Where do users drop off',
|
||||||
|
'Failed network requests today',
|
||||||
|
];
|
||||||
|
const result = suggestedPromptIdeas;
|
||||||
|
const targetSize = 3;
|
||||||
|
while (result.length < targetSize && defaultPromptIdeas.length) {
|
||||||
|
result.push(defaultPromptIdeas.pop());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [suggestedPromptIdeas.length]);
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<div className={'flex items-center gap-2 mb-1 text-gray-dark'}>
|
<div className={'flex items-center gap-2 mb-1 text-gray-dark'}>
|
||||||
<Lightbulb size={16} />
|
<b>{inChat ? 'Suggested Follow-up Questions' : 'Suggested Ideas:'}</b>
|
||||||
<b>Ideas:</b>
|
|
||||||
</div>
|
</div>
|
||||||
<IdeaItem onClick={onClick} title={'Top user journeys'} />
|
{isPending ? (
|
||||||
<IdeaItem onClick={onClick} title={'Where do users drop off'} />
|
<div className="animate-pulse text-disabled-text">
|
||||||
<IdeaItem onClick={onClick} title={'Failed network requests today'} />
|
{t('Generating ideas')}...
|
||||||
</>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{ideas.map((title) => (
|
||||||
|
<IdeaItem
|
||||||
|
limited={limited}
|
||||||
|
key={title}
|
||||||
|
onClick={onClick}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function IdeaItem({ title, onClick }: { title: string, onClick: (query: string) => void }) {
|
function IdeaItem({
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
limited,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
onClick: (query: string) => void;
|
||||||
|
limited?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => onClick(title)}
|
onClick={() => (limited ? null : onClick(title))}
|
||||||
className={
|
className={cn(
|
||||||
'flex items-center gap-2 cursor-pointer text-gray-dark hover:text-black'
|
'cursor-pointer text-gray-dark hover:text-black rounded-full px-4 py-2 shadow border',
|
||||||
}
|
limited ? 'bg-gray-lightest cursor-not-allowed' : 'bg-white',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<MoveRight size={16} />
|
{title}
|
||||||
<span>{title}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,35 @@ import React from 'react';
|
||||||
import ChatInput from './ChatInput';
|
import ChatInput from './ChatInput';
|
||||||
import Ideas from './Ideas';
|
import Ideas from './Ideas';
|
||||||
|
|
||||||
function IntroSection({ onAsk }: { onAsk: (query: string) => void }) {
|
function IntroSection({
|
||||||
|
onAsk,
|
||||||
|
onCancel,
|
||||||
|
userName,
|
||||||
|
projectId,
|
||||||
|
limited,
|
||||||
|
}: {
|
||||||
|
onAsk: (query: string) => void;
|
||||||
|
projectId: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
userName: string;
|
||||||
|
limited?: boolean;
|
||||||
|
}) {
|
||||||
const isLoading = false;
|
const isLoading = false;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'text-disabled-text text-xl absolute top-4'}>
|
<div className={'relative w-2/3 flex flex-col gap-4'}>
|
||||||
Kai is your AI assistant, delivering smart insights in response to your
|
<div className="font-semibold text-lg">
|
||||||
queries.
|
Hey {userName}, how can I help you?
|
||||||
</div>
|
</div>
|
||||||
<div className={'relative w-2/3'} style={{ height: 44 }}>
|
<ChatInput
|
||||||
{/*<GradientBorderInput placeholder={'Ask anything about your product and users...'} onButtonClick={() => null} />*/}
|
onCancel={onCancel}
|
||||||
<ChatInput isLoading={isLoading} onSubmit={onAsk} />
|
isLoading={isLoading}
|
||||||
<div className={'absolute top-full flex flex-col gap-2 mt-4'}>
|
onSubmit={onAsk}
|
||||||
<Ideas onClick={(query) => onAsk(query)} />
|
isArea
|
||||||
|
/>
|
||||||
|
<div className={'absolute top-full flex flex-col gap-2 mt-4'}>
|
||||||
|
<Ideas limited={limited} onClick={(query) => onAsk(query)} projectId={projectId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={'text-disabled-text absolute bottom-4'}>
|
|
||||||
OpenReplay AI can make mistakes. Verify its outputs.
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,23 @@ import React from 'react';
|
||||||
import { kaiStore } from '../KaiStore';
|
import { kaiStore } from '../KaiStore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Progress, Tooltip } from 'antd';
|
import { Progress, Tooltip } from 'antd';
|
||||||
const getUsageColor = (percent: number) => {
|
|
||||||
return 'disabled-text';
|
|
||||||
};
|
|
||||||
|
|
||||||
function Usage() {
|
function Usage() {
|
||||||
const { usage } = kaiStore;
|
const usage = kaiStore.usage;
|
||||||
const color = getUsageColor(usage.percent);
|
|
||||||
|
|
||||||
if (usage.total === 0) {
|
if (usage.total === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const roundPercent = Math.round((usage.used / usage.total) * 100);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip title={`Daily response limit (${usage.used}/${usage.total})`}>
|
<Tooltip title={`Daily response limit (${usage.used}/${usage.total})`}>
|
||||||
<Progress
|
<Progress
|
||||||
percent={usage.percent}
|
percent={roundPercent}
|
||||||
strokeColor={usage.percent < 99 ? 'var(--color-main)' : 'var(--color-red)'}
|
strokeColor={
|
||||||
|
roundPercent < 99 ? 'var(--color-main)' : 'var(--color-red)'
|
||||||
|
}
|
||||||
showInfo={false}
|
showInfo={false}
|
||||||
type="circle"
|
type="circle"
|
||||||
size={24}
|
size={24}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { Component, createContext } from 'react';
|
import React, { Component, createContext } from 'react';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
import { className } from '@medv/finder';
|
||||||
|
|
||||||
const ModalContext = createContext({
|
const ModalContext = createContext({
|
||||||
component: null,
|
component: null,
|
||||||
|
|
@ -29,6 +30,7 @@ export class ModalProvider extends Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
component,
|
component,
|
||||||
props,
|
props,
|
||||||
|
className: props.className || undefined,
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', this.handleKeyDown);
|
document.addEventListener('keydown', this.handleKeyDown);
|
||||||
document.querySelector('body').style.overflow = 'hidden';
|
document.querySelector('body').style.overflow = 'hidden';
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ interface Props {
|
||||||
autoplay: boolean;
|
autoplay: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
isHighlight?: boolean;
|
isHighlight?: boolean;
|
||||||
|
isFull?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClipsPlayer(props: Props) {
|
function ClipsPlayer(props: Props) {
|
||||||
const { clip, currentIndex, isCurrent, onClose, isHighlight } = props;
|
const { clip, currentIndex, isCurrent, onClose, isHighlight, isFull } = props;
|
||||||
const { sessionStore } = useStore();
|
const { sessionStore } = useStore();
|
||||||
const { prefetched } = sessionStore;
|
const { prefetched } = sessionStore;
|
||||||
const [windowActive, setWindowActive] = useState(!document.hidden);
|
const [windowActive, setWindowActive] = useState(!document.hidden);
|
||||||
|
|
@ -146,6 +147,7 @@ function ClipsPlayer(props: Props) {
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
range={clip.range}
|
range={clip.range}
|
||||||
session={session!}
|
session={session!}
|
||||||
|
isFull={isFull}
|
||||||
/>
|
/>
|
||||||
<ClipPlayerContent
|
<ClipPlayerContent
|
||||||
message={clip.message}
|
message={clip.message}
|
||||||
|
|
@ -153,6 +155,7 @@ function ClipsPlayer(props: Props) {
|
||||||
autoplay={props.autoplay}
|
autoplay={props.autoplay}
|
||||||
range={clip.range}
|
range={clip.range}
|
||||||
session={session!}
|
session={session!}
|
||||||
|
isFull={isFull}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,14 @@ interface Props {
|
||||||
isHighlight?: boolean;
|
isHighlight?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
isFull?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClipPlayerContent(props: Props) {
|
function ClipPlayerContent(props: Props) {
|
||||||
const playerContext = React.useContext<IPlayerContext>(PlayerContext);
|
const playerContext = React.useContext<IPlayerContext>(PlayerContext);
|
||||||
const screenWrapper = React.useRef<HTMLDivElement>(null);
|
const screenWrapper = React.useRef<HTMLDivElement>(null);
|
||||||
const { time } = playerContext.store.get();
|
const { time } = playerContext.store.get();
|
||||||
const { range } = props;
|
const { range, isFull } = props;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!playerContext.player) return;
|
if (!playerContext.player) return;
|
||||||
|
|
@ -90,7 +91,7 @@ function ClipPlayerContent(props: Props) {
|
||||||
<div className="leading-none font-medium">{props.message}</div>
|
<div className="leading-none font-medium">{props.message}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ClipPlayerControls session={props.session} range={props.range} />
|
<ClipPlayerControls isFull={isFull} session={props.session} range={props.range} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@ import { useTranslation } from 'react-i18next';
|
||||||
function ClipPlayerControls({
|
function ClipPlayerControls({
|
||||||
session,
|
session,
|
||||||
range,
|
range,
|
||||||
|
isFull,
|
||||||
}: {
|
}: {
|
||||||
session: Session;
|
session: Session;
|
||||||
range: [number, number];
|
range: [number, number];
|
||||||
|
isFull?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { projectsStore } = useStore();
|
const { projectsStore } = useStore();
|
||||||
|
|
@ -47,7 +49,7 @@ function ClipPlayerControls({
|
||||||
<PlayButton state={state} togglePlay={togglePlay} iconSize={30} />
|
<PlayButton state={state} togglePlay={togglePlay} iconSize={30} />
|
||||||
<Timeline range={range} />
|
<Timeline range={range} />
|
||||||
<Button size="small" type="primary" onClick={showFullSession}>
|
<Button size="small" type="primary" onClick={showFullSession}>
|
||||||
{t('Play Full Session')}
|
{isFull ? t('Open Session') : t('Play Full Session')}
|
||||||
<CirclePlay size={16} style={{ marginLeft: '0px'}} />
|
<CirclePlay size={16} style={{ marginLeft: '0px'}} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@ interface Props {
|
||||||
range: [number, number];
|
range: [number, number];
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
isHighlight?: boolean;
|
isHighlight?: boolean;
|
||||||
|
isFull?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClipPlayerHeader(props: Props) {
|
function ClipPlayerHeader(props: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { projectsStore } = useStore();
|
const { projectsStore } = useStore();
|
||||||
const { session, range, onClose, isHighlight } = props;
|
const { session, range, onClose, isHighlight, isFull } = props;
|
||||||
const { siteId } = projectsStore;
|
const { siteId } = projectsStore;
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
|
@ -33,7 +34,7 @@ function ClipPlayerHeader(props: Props) {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-3 flex justify-between items-center border-b relative">
|
<div className="bg-white p-3 flex justify-between items-center border-b relative">
|
||||||
{isHighlight ? <PartialSessionBadge /> : null}
|
{isHighlight && !isFull ? <PartialSessionBadge /> : null}
|
||||||
<UserCard session={props.session} />
|
<UserCard session={props.session} />
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { TYPES } from 'Types/session/event';
|
import { TYPES } from 'Types/session/event';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import UxtEvent from 'Components/Session_/EventsBlock/UxtEvent';
|
import UxtEvent from 'Components/Session_/EventsBlock/UxtEvent';
|
||||||
|
|
@ -32,6 +32,7 @@ function EventGroupWrapper(props) {
|
||||||
presentInSearch,
|
presentInSearch,
|
||||||
isNote,
|
isNote,
|
||||||
isTabChange,
|
isTabChange,
|
||||||
|
isIncident,
|
||||||
filterOutNote,
|
filterOutNote,
|
||||||
} = props;
|
} = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -57,6 +58,15 @@ function EventGroupWrapper(props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isIncident) {
|
||||||
|
return (
|
||||||
|
<Incident
|
||||||
|
isCurrent={isCurrent}
|
||||||
|
label={event.label}
|
||||||
|
onClick={onEventClick}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
if (isLocation) {
|
if (isLocation) {
|
||||||
return (
|
return (
|
||||||
<Event
|
<Event
|
||||||
|
|
@ -100,11 +110,19 @@ function EventGroupWrapper(props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const shadowColor = isSearched ? '#F0A930' : props.isPrev
|
const shadowColor = useMemo(() => {
|
||||||
? '#A7BFFF'
|
if (isSearched) {
|
||||||
: props.isCurrent
|
return '#F0A930';
|
||||||
? '#394EFF'
|
}
|
||||||
: 'transparent';
|
if (props.isPrev) {
|
||||||
|
return '#A7BFFF';
|
||||||
|
}
|
||||||
|
if (props.isCurrent) {
|
||||||
|
return '#394EFF';
|
||||||
|
}
|
||||||
|
return 'transparent';
|
||||||
|
}, [isSearched, props.isPrev, props.isCurrent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -172,4 +190,22 @@ function TabChange({ from, to, activeUrl, onClick }) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function Incident({ label, onClick }: { label: string; onClick: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className="pr-6 pl-4 py-2 relative user-select-none transition-all duration-200 cursor-pointer rounded-[3px] hover:bg-[var(--color-active-blue)] bg-[var(--color-white)]"
|
||||||
|
>
|
||||||
|
<div className='flex items-center py-2 gap-[10.5px]'>
|
||||||
|
<Icon name="console/warning" size={18} color="gray-dark" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span style={{ fontWeight: 500 }}>{t('Incident')}</span>
|
||||||
|
<span className="text-ellipsis overflow-hidden whitespace-nowrap max-w-full text-sm text-[var(--color-gray-medium)]">{label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default observer(EventGroupWrapper);
|
export default observer(EventGroupWrapper);
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ function EventsBlock(props: IProps) {
|
||||||
const { notesStore, uxtestingStore, uiPlayerStore, sessionStore } =
|
const { notesStore, uxtestingStore, uiPlayerStore, sessionStore } =
|
||||||
useStore();
|
useStore();
|
||||||
const session = sessionStore.current;
|
const session = sessionStore.current;
|
||||||
const { notesWithEvents } = session;
|
const notesWithEvents = session.notesWithEvents;
|
||||||
const { uxtVideo } = session;
|
const incidents = session.incidents;
|
||||||
|
const uxtVideo = session.uxtVideo;
|
||||||
const { filteredEvents } = sessionStore;
|
const { filteredEvents } = sessionStore;
|
||||||
const query = sessionStore.eventsQuery;
|
const query = sessionStore.eventsQuery;
|
||||||
const { eventsIndex } = sessionStore;
|
const { eventsIndex } = sessionStore;
|
||||||
|
|
@ -86,26 +87,28 @@ function EventsBlock(props: IProps) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const eventsWithMobxNotes = [...notesWithEvents, ...notes]
|
const eventsWithMobxNotes = [...incidents, ...notesWithEvents, ...notes, ].sort(sortEvents);
|
||||||
.sort(sortEvents);
|
|
||||||
const filteredTabEvents = query.length
|
const filteredTabEvents = query.length
|
||||||
? tabChangeEvents
|
? tabChangeEvents.filter((e) => (e.activeUrl as string).includes(query))
|
||||||
.filter((e => (e.activeUrl as string).includes(query)))
|
: tabChangeEvents;
|
||||||
: tabChangeEvents;
|
return mergeEventLists(
|
||||||
const list = mergeEventLists(
|
filteredLength > 0 ? filteredEvents : eventsWithMobxNotes,
|
||||||
query.length > 0 ? filteredEvents : eventsWithMobxNotes,
|
tabChangeEvents,
|
||||||
filteredTabEvents
|
|
||||||
)
|
)
|
||||||
if (zoomEnabled) {
|
.filter((e) =>
|
||||||
return list.filter((e) =>
|
|
||||||
zoomEnabled
|
zoomEnabled
|
||||||
? '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);
|
)
|
||||||
}
|
.filter((e: any) =>
|
||||||
return list;
|
!e.noteId &&
|
||||||
|
e.type !== 'TABCHANGE' &&
|
||||||
|
uiPlayerStore.showOnlySearchEvents
|
||||||
|
? e.isHighlighted
|
||||||
|
: true,
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
filteredLength,
|
filteredLength,
|
||||||
query,
|
query,
|
||||||
|
|
@ -114,15 +117,17 @@ function EventsBlock(props: IProps) {
|
||||||
zoomEnabled,
|
zoomEnabled,
|
||||||
zoomStartTs,
|
zoomStartTs,
|
||||||
zoomEndTs,
|
zoomEndTs,
|
||||||
uiPlayerStore.showOnlySearchEvents
|
uiPlayerStore.showOnlySearchEvents,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const findLastFitting = React.useCallback(
|
const findLastFitting = React.useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
if (!usedEvents.length) return 0;
|
const allEvents = usedEvents.concat(incidents);
|
||||||
let i = usedEvents.length - 1;
|
if (!allEvents.length) return 0;
|
||||||
|
let i = allEvents.length - 1;
|
||||||
if (time > endTime / 2) {
|
if (time > endTime / 2) {
|
||||||
while (i >= 0) {
|
while (i > 0) {
|
||||||
const event = usedEvents[i];
|
const event = allEvents[i];
|
||||||
if ('time' in event && event.time <= time) break;
|
if ('time' in event && event.time <= time) break;
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
|
|
@ -130,18 +135,18 @@ function EventsBlock(props: IProps) {
|
||||||
}
|
}
|
||||||
let l = 0;
|
let l = 0;
|
||||||
while (l < i) {
|
while (l < i) {
|
||||||
const event = usedEvents[l];
|
const event = allEvents[l];
|
||||||
if ('time' in event && event.time >= time) break;
|
if ('time' in event && event.time >= time) break;
|
||||||
l++;
|
l++;
|
||||||
}
|
}
|
||||||
return l;
|
return l;
|
||||||
},
|
},
|
||||||
[usedEvents, time, endTime],
|
[usedEvents, incidents, time, endTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentTimeEventIndex(findLastFitting(time));
|
setCurrentTimeEventIndex(findLastFitting(time));
|
||||||
}, [])
|
}, [time]);
|
||||||
|
|
||||||
const write = ({
|
const write = ({
|
||||||
target: { value },
|
target: { value },
|
||||||
|
|
@ -195,9 +200,10 @@ function EventsBlock(props: IProps) {
|
||||||
const event = usedEvents[index];
|
const event = usedEvents[index];
|
||||||
const isNote = 'noteId' in event;
|
const isNote = 'noteId' in event;
|
||||||
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
|
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
|
||||||
|
const isIncident = 'type' in event && event.type === 'INCIDENT';
|
||||||
const isCurrent = index === currentTimeEventIndex;
|
const isCurrent = index === currentTimeEventIndex;
|
||||||
const isPrev = index < currentTimeEventIndex;
|
const isPrev = index < currentTimeEventIndex;
|
||||||
const isSearched = event.isHighlighted
|
const isSearched = event.isHighlighted;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventGroupWrapper
|
<EventGroupWrapper
|
||||||
|
|
@ -213,6 +219,7 @@ function EventsBlock(props: IProps) {
|
||||||
showSelection={!playing}
|
showSelection={!playing}
|
||||||
isNote={isNote}
|
isNote={isNote}
|
||||||
isTabChange={isTabChange}
|
isTabChange={isTabChange}
|
||||||
|
isIncident={isIncident}
|
||||||
isPrev={isPrev}
|
isPrev={isPrev}
|
||||||
filterOutNote={filterOutNote}
|
filterOutNote={filterOutNote}
|
||||||
setActiveTab={setActiveTab}
|
setActiveTab={setActiveTab}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,15 @@ import {
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { getTimelinePosition } from './getTimelinePosition';
|
import { getTimelinePosition } from './getTimelinePosition';
|
||||||
import { useStore } from '@/mstore';
|
import { useStore } from '@/mstore';
|
||||||
|
import { getTimelineEventWidth } from './getTimelineEventWidth';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
|
||||||
function EventsList() {
|
function EventsList() {
|
||||||
const { store } = useContext(PlayerContext);
|
const { store } = useContext(PlayerContext);
|
||||||
const { uiPlayerStore } = useStore();
|
const { uiPlayerStore, sessionStore } = useStore();
|
||||||
|
const { eventCount, endTime, tabStates, sessionStart } = store.get();
|
||||||
|
const { incidents } = sessionStore.current;
|
||||||
|
|
||||||
const { eventCount, endTime } = 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) => {
|
() => Object.values(tabStates)[0]?.eventList.filter((e) => {
|
||||||
|
|
@ -39,10 +41,25 @@ function EventsList() {
|
||||||
<div
|
<div
|
||||||
/* @ts-ignore TODO */
|
/* @ts-ignore TODO */
|
||||||
key={`${e.key}_${e.time}`}
|
key={`${e.key}_${e.time}`}
|
||||||
className={`absolute w-[2px] h-[10px] z-[3] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
|
className={`absolute w-[2px] h-[10px] z-[4] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
|
||||||
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
|
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{incidents?.map((i) => {
|
||||||
|
const width = getTimelineEventWidth(endTime, (i as any).time, (i as any).endTime - sessionStart);
|
||||||
|
return (
|
||||||
|
<Tooltip title={i.label} key={(i as any).startTime}>
|
||||||
|
<div
|
||||||
|
/* @ts-ignore TODO */
|
||||||
|
className={`absolute h-[10px] z-[3] bg-[#ff5454]`}
|
||||||
|
style={{
|
||||||
|
left: `${getTimelinePosition((i as any).time, scale)}%`,
|
||||||
|
width: typeof width === 'string' ? width : `${width}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { getTimelinePosition } from '@/utils';
|
||||||
|
|
||||||
|
export function getTimelineEventWidth(
|
||||||
|
sessionDuration: number,
|
||||||
|
eventStart: number,
|
||||||
|
eventEnd: number,
|
||||||
|
): number | string {
|
||||||
|
if (eventStart < 0) {
|
||||||
|
eventStart = 0;
|
||||||
|
}
|
||||||
|
if (eventEnd > sessionDuration) {
|
||||||
|
eventEnd = sessionDuration;
|
||||||
|
}
|
||||||
|
if (eventStart === eventEnd) {
|
||||||
|
return '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = ((eventEnd - eventStart) / sessionDuration) * 100;
|
||||||
|
|
||||||
|
return width < 1 ? '4px' : width;
|
||||||
|
}
|
||||||
|
|
@ -35,14 +35,11 @@ function SessionFilters() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Add default location/screen filter if no filters are present
|
// Add default location/screen filter if no filters are present
|
||||||
if (searchStore.instance.filters.length === 0) {
|
if (
|
||||||
searchStore.addFilterByKeyAndValue(
|
searchStore.instance.filters.length === 0 &&
|
||||||
activeProject?.platform === 'web'
|
activeProject?.platform === 'web'
|
||||||
? FilterKey.LOCATION
|
) {
|
||||||
: FilterKey.VIEW_MOBILE,
|
searchStore.addFilterByKeyAndValue(FilterKey.LOCATION, '', 'isAny');
|
||||||
'',
|
|
||||||
'isAny',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
void reloadTags();
|
void reloadTags();
|
||||||
}, [projectsStore.activeSiteId, activeProject]);
|
}, [projectsStore.activeSiteId, activeProject]);
|
||||||
|
|
@ -65,7 +62,7 @@ function SessionFilters() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFilterMove = (newFilters: any) => {
|
const onFilterMove = (newFilters: any) => {
|
||||||
searchStore.updateSearch({ ...appliedFilter, filters: newFilters});
|
searchStore.updateSearch({ ...appliedFilter, filters: newFilters });
|
||||||
// debounceFetch();
|
// debounceFetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Icon, Link } from 'UI';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
|
|
||||||
const PLAY_ICON_NAMES = {
|
const PLAY_ICON_NAMES = {
|
||||||
notPlayed: 'play-fill',
|
notPlayed: 'play-v2',
|
||||||
played: 'play-circle-light',
|
played: 'play-circle-light',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -76,10 +76,14 @@ function PlayLink(props: Props) {
|
||||||
rel={props.newTab ? 'noopener noreferrer' : undefined}
|
rel={props.newTab ? 'noopener noreferrer' : undefined}
|
||||||
>
|
>
|
||||||
<div className="group-hover:block hidden">
|
<div className="group-hover:block hidden">
|
||||||
<Icon name="play-hover" size={38} color={isAssist ? 'tealx' : 'teal'} />
|
<Icon name={`play-fill-v2${isAssist ? '-assist' : ''}`} size={38} />
|
||||||
</div>
|
</div>
|
||||||
<div className="group-hover:hidden block">
|
<div className="group-hover:hidden block">
|
||||||
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
|
<Icon
|
||||||
|
name={`${iconName}${isAssist ? '-assist' : ''}`}
|
||||||
|
size={38}
|
||||||
|
color="teal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ interface Props {
|
||||||
bookmarked?: boolean;
|
bookmarked?: boolean;
|
||||||
toggleFavorite?: (sessionId: string) => void;
|
toggleFavorite?: (sessionId: string) => void;
|
||||||
query?: string;
|
query?: string;
|
||||||
|
slim?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PREFETCH_STATE = {
|
const PREFETCH_STATE = {
|
||||||
|
|
@ -81,7 +82,8 @@ const PREFETCH_STATE = {
|
||||||
|
|
||||||
function SessionItem(props: RouteComponentProps & Props) {
|
function SessionItem(props: RouteComponentProps & Props) {
|
||||||
const { location } = useHistory();
|
const { location } = useHistory();
|
||||||
const { settingsStore, sessionStore, searchStore, searchStoreLive } = useStore();
|
const { settingsStore, sessionStore, searchStore, searchStoreLive } =
|
||||||
|
useStore();
|
||||||
const { timezone, shownTimezone } = settingsStore.sessionSettings;
|
const { timezone, shownTimezone } = settingsStore.sessionSettings;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [prefetchState, setPrefetched] = useState(PREFETCH_STATE.none);
|
const [prefetchState, setPrefetched] = useState(PREFETCH_STATE.none);
|
||||||
|
|
@ -99,6 +101,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
isDisabled,
|
isDisabled,
|
||||||
live: propsLive,
|
live: propsLive,
|
||||||
isAdd,
|
isAdd,
|
||||||
|
slim,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -176,7 +179,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
await sessionStore.getFirstMob(sessionId);
|
await sessionStore.getFirstMob(sessionId);
|
||||||
setPrefetched(PREFETCH_STATE.fetched);
|
setPrefetched(PREFETCH_STATE.fetched);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setPrefetched(PREFETCH_STATE.none)
|
setPrefetched(PREFETCH_STATE.none);
|
||||||
console.error('Error while prefetching first mob', e);
|
console.error('Error while prefetching first mob', e);
|
||||||
}
|
}
|
||||||
}, [prefetchState, live, isAssist, isMobile, sessionStore, sessionId]);
|
}, [prefetchState, live, isAssist, isMobile, sessionStore, sessionId]);
|
||||||
|
|
@ -245,13 +248,13 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
);
|
);
|
||||||
}, [startedAt, timezone, userTimezone]);
|
}, [startedAt, timezone, userTimezone]);
|
||||||
|
|
||||||
const onMetaClick = (meta: { name: string, value: string }) => {
|
const onMetaClick = (meta: { name: string; value: string }) => {
|
||||||
if (isAssist) {
|
if (isAssist) {
|
||||||
searchStoreLive.addFilterByKeyAndValue(meta.name, meta.value)
|
searchStoreLive.addFilterByKeyAndValue(meta.name, meta.value);
|
||||||
} else {
|
} else {
|
||||||
searchStore.addFilterByKeyAndValue(meta.name, meta.value);
|
searchStore.addFilterByKeyAndValue(meta.name, meta.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
|
|
@ -261,7 +264,11 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(stl.sessionItem, 'flex flex-col p-4')}
|
className={cn(
|
||||||
|
stl.sessionItem,
|
||||||
|
'flex flex-col',
|
||||||
|
slim ? 'px-4 py-2 text-sm' : 'p-4',
|
||||||
|
)}
|
||||||
id="session-item"
|
id="session-item"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onMouseEnter={handleHover}
|
onMouseEnter={handleHover}
|
||||||
|
|
@ -291,13 +298,16 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
|
<div className="overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
|
||||||
<div
|
<div
|
||||||
className={cn('text-lg', {
|
className={cn(
|
||||||
'color-teal cursor-pointer':
|
{
|
||||||
!disableUser && hasUserId && !isDisabled,
|
'color-teal cursor-pointer':
|
||||||
[stl.userName]:
|
!disableUser && hasUserId && !isDisabled,
|
||||||
!disableUser && hasUserId && !isDisabled,
|
[stl.userName]:
|
||||||
'color-gray-medium': disableUser || !hasUserId,
|
!disableUser && hasUserId && !isDisabled,
|
||||||
})}
|
'color-gray-medium': disableUser || !hasUserId,
|
||||||
|
},
|
||||||
|
slim ? 'text-base' : 'text-lg',
|
||||||
|
)}
|
||||||
onClick={handleUserClick}
|
onClick={handleUserClick}
|
||||||
>
|
>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
|
|
@ -308,15 +318,20 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{_metaList.length > 0 && (
|
{!slim && _metaList.length > 0 && (
|
||||||
<SessionMetaList onMetaClick={onMetaClick} maxLength={3} metaList={_metaList} />
|
<SessionMetaList
|
||||||
|
onMetaClick={onMetaClick}
|
||||||
|
maxLength={3}
|
||||||
|
metaList={_metaList}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 flex flex-col justify-between gap-2 mt-3 lg:mt-0',
|
'px-2 flex flex-col justify-between lg:mt-0',
|
||||||
compact ? 'w-[40%]' : 'lg:w-1/5',
|
compact ? 'w-[40%]' : 'lg:w-1/5',
|
||||||
|
slim ? 'gap-1 mt-1' : 'gap-2 mt-3',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -343,7 +358,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
: 'Event'}
|
: 'Event'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
<Icon name="circle-fill" size={3} className="mx-2" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -353,9 +368,12 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{ width: '30%' }}
|
style={{ width: '30%' }}
|
||||||
className="px-2 flex flex-col justify-between gap-2"
|
className={cn(
|
||||||
|
'px-2 flex flex-col justify-between',
|
||||||
|
slim ? 'gap-1' : 'gap-2',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div style={{ height: '21px' }}>
|
<div style={{ height: slim ? undefined : '21px' }}>
|
||||||
<CountryFlag
|
<CountryFlag
|
||||||
userCity={userCity}
|
userCity={userCity}
|
||||||
userState={userState}
|
userState={userState}
|
||||||
|
|
@ -373,7 +391,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{userOs && userBrowser && (
|
{userOs && userBrowser && (
|
||||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
<Icon name="circle-fill" size={3} className="mx-2" />
|
||||||
)}
|
)}
|
||||||
{userOs && (
|
{userOs && (
|
||||||
<span
|
<span
|
||||||
|
|
@ -387,7 +405,7 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{userOs && (
|
{userOs && (
|
||||||
<Icon name="circle-fill" size={3} className="mx-4" />
|
<Icon name="circle-fill" size={3} className="mx-2" />
|
||||||
)}
|
)}
|
||||||
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
<span className="capitalize" style={{ maxWidth: '70px' }}>
|
||||||
<TextEllipsis
|
<TextEllipsis
|
||||||
|
|
@ -452,7 +470,9 @@ function SessionItem(props: RouteComponentProps & Props) {
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
queryParams={queryParams}
|
queryParams={queryParams}
|
||||||
query={query}
|
query={query}
|
||||||
beforeOpen={live || isAssist ? undefined : populateData}
|
beforeOpen={
|
||||||
|
slim || live || isAssist ? undefined : populateData
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { issues_types, types } from 'Types/session/issue';
|
import { issues_types, types } from 'Types/session/issue';
|
||||||
import { Grid, Segmented } from 'antd';
|
import { Grid, Segmented } from 'antd';
|
||||||
import { Angry, CircleAlert, Skull, WifiOff, ChevronDown } from 'lucide-react';
|
import { Angry, CircleAlert, Skull, WifiOff, ChevronDown, MessageCircleWarning } from 'lucide-react';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
|
|
@ -15,6 +15,7 @@ const tagIcons = {
|
||||||
[types.CLICK_RAGE]: <Angry size={14} />,
|
[types.CLICK_RAGE]: <Angry size={14} />,
|
||||||
[types.CRASH]: <Skull size={14} />,
|
[types.CRASH]: <Skull size={14} />,
|
||||||
[types.TAP_RAGE]: <Angry size={14} />,
|
[types.TAP_RAGE]: <Angry size={14} />,
|
||||||
|
[types.INCIDENT]: <MessageCircleWarning size={14} />,
|
||||||
} as Record<string, any>;
|
} as Record<string, any>;
|
||||||
|
|
||||||
function SessionTags() {
|
function SessionTags() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import { Button, Tooltip } from 'antd';
|
import { Button, Tooltip } from 'antd';
|
||||||
import { ClipboardCopy, ClipboardCheck } from 'lucide-react';
|
import { Copy, Check } from 'lucide-react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -30,10 +30,10 @@ function CopyButton({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
};
|
||||||
const copyHandler = () => {
|
const copyHandler = () => {
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
const contentIsGetter = !!getHtml
|
const contentIsGetter = !!getHtml;
|
||||||
const textContent = contentIsGetter ? getHtml() : content;
|
const textContent = contentIsGetter ? getHtml() : content;
|
||||||
const isHttps = window.location.protocol === 'https:';
|
const isHttps = window.location.protocol === 'https:';
|
||||||
if (!isHttps) {
|
if (!isHttps) {
|
||||||
|
|
@ -43,15 +43,16 @@ function CopyButton({
|
||||||
}
|
}
|
||||||
const blob = new Blob([textContent], { type: format });
|
const blob = new Blob([textContent], { type: format });
|
||||||
const cbItem = new ClipboardItem({
|
const cbItem = new ClipboardItem({
|
||||||
[format]: blob
|
[format]: blob,
|
||||||
})
|
});
|
||||||
navigator.clipboard.write([cbItem])
|
navigator.clipboard
|
||||||
.catch(e => {
|
.write([cbItem])
|
||||||
|
.catch((e) => {
|
||||||
copy(textContent);
|
copy(textContent);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
reset()
|
reset();
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isIcon) {
|
if (isIcon) {
|
||||||
|
|
@ -62,7 +63,11 @@ function CopyButton({
|
||||||
onClick={copyHandler}
|
onClick={copyHandler}
|
||||||
size={size}
|
size={size}
|
||||||
icon={
|
icon={
|
||||||
copied ? <ClipboardCheck size={16} /> : <ClipboardCopy size={16} />
|
copied ? (
|
||||||
|
<Check strokeWidth={2} size={16} />
|
||||||
|
) : (
|
||||||
|
<Copy strokeWidth={2} size={16} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
18
frontend/app/components/ui/Icons/export_pdf.tsx
Normal file
18
frontend/app/components/ui/Icons/export_pdf.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/* Auto-generated, do not edit */
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
fill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Export_pdf(props: Props) {
|
||||||
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 21 21" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><path d="M11.496 3H5.454c-.805 0-1.458.653-1.458 1.458V10.5h13.333V8.833h-4.375a1.458 1.458 0 0 1-1.458-1.458V3Z" fill="#1C1B1F"/><path d="M17.33 7.583v-.071c0-.387-.154-.758-.428-1.031L13.85 3.427A1.458 1.458 0 0 0 12.817 3h-.071v4.375c0 .115.093.208.208.208h4.375Z" fill="#1C1B1F"/><path clipRule="evenodd" d="M3.163 12.792c0-.345.28-.625.625-.625h1.666a1.875 1.875 0 1 1 0 3.75H4.413v1.458a.625.625 0 1 1-1.25 0v-4.583Zm1.25 1.875h1.041a.625.625 0 0 0 0-1.25H4.413v1.25ZM8.163 12.792c0-.345.28-.625.625-.625h1.25c.54 0 1.258.134 1.858.582.63.471 1.058 1.237 1.058 2.334 0 1.098-.427 1.863-1.058 2.334-.6.449-1.319.583-1.858.583h-1.25a.625.625 0 0 1-.625-.625v-4.583Zm1.25.625v3.333h.625c.363 0 .79-.095 1.11-.334.29-.216.556-.597.556-1.333s-.267-1.116-.556-1.332c-.32-.24-.747-.334-1.11-.334h-.625ZM13.996 12.792c0-.345.28-.625.625-.625h2.917a.625.625 0 1 1 0 1.25h-2.292v1.25h1.458a.625.625 0 0 1 0 1.25h-1.458v1.458a.625.625 0 1 1-1.25 0v-4.583Z" fill="#1C1B1F"/></svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Export_pdf;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/* Auto-generated, do not edit */
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: number | string;
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
fill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Funnel_message_circle_warning(props: Props) {
|
||||||
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22ZM12 8v4M12 16h.01"/></svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Funnel_message_circle_warning;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue