Merge branch 'dev' into login-redesign

This commit is contained in:
Shekar Siri 2023-03-27 13:15:41 +02:00 committed by GitHub
commit 2ab2a70045
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
132 changed files with 2921 additions and 1859 deletions

View file

@ -6,10 +6,10 @@ on:
- dev
- api-*
paths:
- "ee/utilities/**"
- "utilities/**"
- "!utilities/.gitignore"
- "!utilities/*-dev.sh"
- "ee/assist/**"
- "assist/**"
- "!assist/.gitignore"
- "!assist/*-dev.sh"
name: Build and Deploy Assist EE
@ -44,7 +44,7 @@ jobs:
ENVIRONMENT: staging
run: |
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
cd utilities
cd assist
PUSH_IMAGE=0 bash -x ./build.sh ee
[[ "x$skip_security_checks" == "xtrue" ]] || {
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
@ -101,9 +101,9 @@ jobs:
cat /tmp/image_override.yaml
# Deploy command
mv openreplay/charts/{ingress-nginx,chalice,quickwit} /tmp
mv openreplay/charts/{ingress-nginx,assist,quickwit} /tmp
rm -rf openreplay/charts/*
mv /tmp/{ingress-nginx,chalice,quickwit} openreplay/charts/
mv /tmp/{ingress-nginx,assist,quickwit} openreplay/charts/
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
env:
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}

View file

@ -6,9 +6,9 @@ on:
- dev
- api-*
paths:
- "utilities/**"
- "!utilities/.gitignore"
- "!utilities/*-dev.sh"
- "assist/**"
- "!assist/.gitignore"
- "!assist/*-dev.sh"
name: Build and Deploy Assist
@ -43,7 +43,7 @@ jobs:
ENVIRONMENT: staging
run: |
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
cd utilities
cd assist
PUSH_IMAGE=0 bash -x ./build.sh
[[ "x$skip_security_checks" == "xtrue" ]] || {
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
@ -100,9 +100,9 @@ jobs:
cat /tmp/image_override.yaml
# Deploy command
mv openreplay/charts/{ingress-nginx,chalice,quickwit} /tmp
mv openreplay/charts/{ingress-nginx,assist,quickwit} /tmp
rm -rf openreplay/charts/*
mv /tmp/{ingress-nginx,chalice,quickwit} openreplay/charts/
mv /tmp/{ingress-nginx,assist,quickwit} openreplay/charts/
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
env:
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}

View file

@ -1,6 +1,11 @@
# This action will push the peers changes to aws
on:
workflow_dispatch:
inputs:
skip_security_checks:
description: 'Skip Security checks if there is a unfixable vuln or error. Value: true/false'
required: false
default: 'false'
push:
branches:
- dev
@ -11,7 +16,7 @@ on:
- "!peers/.gitignore"
- "!peers/*-dev.sh"
name: Build and Deploy Peers
name: Build and Deploy Peers EE
jobs:
deploy:
@ -36,30 +41,98 @@ jobs:
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
id: setcontext
- name: Building and Pushing api image
# Caching docker images
- uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Building and Pushing peers image
id: build-image
env:
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
ENVIRONMENT: staging
run: |
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
cd peers
PUSH_IMAGE=1 bash build.sh ee
PUSH_IMAGE=0 bash -x ./build.sh ee
[[ "x$skip_security_checks" == "xtrue" ]] || {
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
images=("peers")
for image in ${images[*]};do
./trivy image --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
done
err_code=$?
[[ $err_code -ne 0 ]] && {
exit $err_code
}
} && {
echo "Skipping Security Checks"
}
images=("peers")
for image in ${images[*]};do
docker push $DOCKER_REPO/$image:$IMAGE_TAG
done
- name: Creating old image input
run: |
#
# Create yaml with existing image tags
#
kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\
tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt
echo > /tmp/image_override.yaml
for line in `cat /tmp/image_tag.txt`;
do
image_array=($(echo "$line" | tr ':' '\n'))
cat <<EOF >> /tmp/image_override.yaml
${image_array[0]}:
image:
# We've to strip off the -ee, as helm will append it.
tag: `echo ${image_array[1]} | cut -d '-' -f 1`
EOF
done
- name: Deploy to kubernetes
run: |
cd scripts/helmcharts/
sed -i "s#openReplayContainerRegistry.*#openReplayContainerRegistry: \"${{ secrets.EE_REGISTRY_URL }}\"#g" vars.yaml
sed -i "s#minio_access_key.*#minio_access_key: \"${{ secrets.EE_MINIO_ACCESS_KEY }}\" #g" vars.yaml
sed -i "s#minio_secret_key.*#minio_secret_key: \"${{ secrets.EE_MINIO_SECRET_KEY }}\" #g" vars.yaml
sed -i "s#domain_name.*#domain_name: \"ee.openreplay.com\" #g" vars.yaml
sed -i "s#kubeconfig.*#kubeconfig_path: ${KUBECONFIG}#g" vars.yaml
sed -i "s/image_tag:.*/image_tag: \"$IMAGE_TAG\"/g" vars.yaml
bash kube-install.sh --app peers
## Update secerts
sed -i "s#openReplayContainerRegistry.*#openReplayContainerRegistry: \"${{ secrets.OSS_REGISTRY_URL }}\"#g" vars.yaml
sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.EE_PG_PASSWORD }}\"/g" vars.yaml
sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.EE_MINIO_ACCESS_KEY }}\"/g" vars.yaml
sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.EE_MINIO_SECRET_KEY }}\"/g" vars.yaml
sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.EE_JWT_SECRET }}\"/g" vars.yaml
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.EE_DOMAIN_NAME }}\"/g" vars.yaml
sed -i "s/enterpriseEditionLicense: \"\"/enterpriseEditionLicense: \"${{ secrets.EE_LICENSE_KEY }}\"/g" vars.yaml
# Update changed image tag
sed -i "/peers/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
cat /tmp/image_override.yaml
# Deploy command
mv openreplay/charts/{ingress-nginx,peers,quickwit} /tmp
rm -rf openreplay/charts/*
mv /tmp/{ingress-nginx,peers,quickwit} openreplay/charts/
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
env:
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
ENVIRONMENT: staging
- name: Alert slack
if: ${{ failure() }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ee
SLACK_TITLE: "Failed ${{ github.workflow }}"
SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEB_HOOK }}
SLACK_USERNAME: "OR Bot"
SLACK_MESSAGE: 'Build failed :bomb:'
# - name: Debug Job
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3

View file

@ -1,6 +1,11 @@
# This action will push the peers changes to aws
on:
workflow_dispatch:
inputs:
skip_security_checks:
description: 'Skip Security checks if there is a unfixable vuln or error. Value: true/false'
required: false
default: 'false'
push:
branches:
- dev
@ -35,30 +40,96 @@ jobs:
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
id: setcontext
- name: Building and Pushing api image
# Caching docker images
- uses: satackey/action-docker-layer-caching@v0.0.11
# Ignore the failure of a step and avoid terminating the job.
continue-on-error: true
- name: Building and Pushing peers image
id: build-image
env:
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
ENVIRONMENT: staging
run: |
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
cd peers
PUSH_IMAGE=1 bash build.sh
PUSH_IMAGE=0 bash -x ./build.sh
[[ "x$skip_security_checks" == "xtrue" ]] || {
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.34.0/trivy_0.34.0_Linux-64bit.tar.gz | tar -xzf - -C ./
images=("peers")
for image in ${images[*]};do
./trivy image --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
done
err_code=$?
[[ $err_code -ne 0 ]] && {
exit $err_code
}
} && {
echo "Skipping Security Checks"
}
images=("peers")
for image in ${images[*]};do
docker push $DOCKER_REPO/$image:$IMAGE_TAG
done
- name: Creating old image input
run: |
#
# Create yaml with existing image tags
#
kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\
tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt
echo > /tmp/image_override.yaml
for line in `cat /tmp/image_tag.txt`;
do
image_array=($(echo "$line" | tr ':' '\n'))
cat <<EOF >> /tmp/image_override.yaml
${image_array[0]}:
image:
tag: ${image_array[1]}
EOF
done
- name: Deploy to kubernetes
run: |
cd scripts/helmcharts/
## Update secerts
sed -i "s#openReplayContainerRegistry.*#openReplayContainerRegistry: \"${{ secrets.OSS_REGISTRY_URL }}\"#g" vars.yaml
sed -i "s#minio_access_key.*#minio_access_key: \"${{ secrets.OSS_MINIO_ACCESS_KEY }}\" #g" vars.yaml
sed -i "s#minio_secret_key.*#minio_secret_key: \"${{ secrets.OSS_MINIO_SECRET_KEY }}\" #g" vars.yaml
sed -i "s#domain_name.*#domain_name: \"foss.openreplay.com\" #g" vars.yaml
sed -i "s#kubeconfig.*#kubeconfig_path: ${KUBECONFIG}#g" vars.yaml
sed -i "s/image_tag:.*/image_tag: \"$IMAGE_TAG\"/g" vars.yaml
bash kube-install.sh --app peers
sed -i "s/postgresqlPassword: \"changeMePassword\"/postgresqlPassword: \"${{ secrets.OSS_PG_PASSWORD }}\"/g" vars.yaml
sed -i "s/accessKey: \"changeMeMinioAccessKey\"/accessKey: \"${{ secrets.OSS_MINIO_ACCESS_KEY }}\"/g" vars.yaml
sed -i "s/secretKey: \"changeMeMinioPassword\"/secretKey: \"${{ secrets.OSS_MINIO_SECRET_KEY }}\"/g" vars.yaml
sed -i "s/jwt_secret: \"SetARandomStringHere\"/jwt_secret: \"${{ secrets.OSS_JWT_SECRET }}\"/g" vars.yaml
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.OSS_DOMAIN_NAME }}\"/g" vars.yaml
# Update changed image tag
sed -i "/peers/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
cat /tmp/image_override.yaml
# Deploy command
mv openreplay/charts/{ingress-nginx,peers,quickwit} /tmp
rm -rf openreplay/charts/*
mv /tmp/{ingress-nginx,peers,quickwit} 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 -
env:
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
ENVIRONMENT: staging
- name: Alert slack
if: ${{ failure() }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: foss
SLACK_TITLE: "Failed ${{ github.workflow }}"
SLACK_COLOR: ${{ job.status }} # or a specific color like 'good' or '#ff00ff'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEB_HOOK }}
SLACK_USERNAME: "OR Bot"
SLACK_MESSAGE: 'Build failed :bomb:'
# - name: Debug Job
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
@ -66,4 +137,4 @@ jobs:
# DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
# IMAGE_TAG: ${{ github.sha }}
# ENVIRONMENT: staging
#

View file

@ -1,4 +1,4 @@
# This action will push the chalice changes to aws
# This action will push the sourcemapreader changes to aws
on:
workflow_dispatch:
push:
@ -83,13 +83,13 @@ jobs:
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.OSS_DOMAIN_NAME }}\"/g" vars.yaml
# Update changed image tag
sed -i "/chalice/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
sed -i "/sourcemapreader/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
cat /tmp/image_override.yaml
# Deploy command
mv openreplay/charts/{ingress-nginx,chalice,quickwit} /tmp
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit} /tmp
rm -rf openreplay/charts/*
mv /tmp/{ingress-nginx,chalice,quickwit} openreplay/charts/
mv /tmp/{ingress-nginx,sourcemapreader,quickwit} 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 -
env:
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}

View file

@ -1,4 +1,5 @@
import logging
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from decouple import config
@ -12,9 +13,42 @@ from chalicelib.utils import pg_client
from routers import core, core_dynamic
from routers.crons import core_crons
from routers.crons import core_dynamic_crons
from routers.subs import insights, metrics, v1_api
from routers.subs import insights, metrics, v1_api, health
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
loglevel = config("LOGLEVEL", default=logging.INFO)
print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logging.info(">>>>> starting up <<<<<")
ap_logger = logging.getLogger('apscheduler')
ap_logger.setLevel(loglevel)
app.schedule = AsyncIOScheduler()
await pg_client.init()
app.schedule.start()
for job in core_crons.cron_jobs + core_dynamic_crons.cron_jobs:
app.schedule.add_job(id=job["func"].__name__, **job)
ap_logger.info(">Scheduled jobs:")
for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
# App listening
yield
# Shutdown
logging.info(">>>>> shutting down <<<<<")
app.schedule.shutdown(wait=False)
await pg_client.terminate()
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""),
lifespan=lifespan)
app.add_middleware(GZipMiddleware, minimum_size=1000)
@ -51,39 +85,13 @@ app.include_router(core_dynamic.app_apikey)
app.include_router(metrics.app)
app.include_router(insights.app)
app.include_router(v1_api.app_apikey)
app.include_router(health.public_app)
app.include_router(health.app)
app.include_router(health.app_apikey)
loglevel = config("LOGLEVEL", default=logging.INFO)
print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel)
ap_logger = logging.getLogger('apscheduler')
ap_logger.setLevel(loglevel)
app.schedule = AsyncIOScheduler()
@app.on_event("startup")
async def startup():
logging.info(">>>>> starting up <<<<<")
await pg_client.init()
app.schedule.start()
for job in core_crons.cron_jobs + core_dynamic_crons.cron_jobs:
app.schedule.add_job(id=job["func"].__name__, **job)
ap_logger.info(">Scheduled jobs:")
for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
@app.on_event("shutdown")
async def shutdown():
logging.info(">>>>> shutting down <<<<<")
app.schedule.shutdown(wait=False)
await pg_client.terminate()
@app.get('/private/shutdown', tags=["private"])
async def stop_server():
logging.info("Requested shutdown")
await shutdown()
import os, signal
os.kill(1, signal.SIGTERM)
# @app.get('/private/shutdown', tags=["private"])
# async def stop_server():
# logging.info("Requested shutdown")
# await shutdown()
# import os, signal
# os.kill(1, signal.SIGTERM)

View file

@ -1,33 +1,17 @@
import logging
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from decouple import config
from fastapi import FastAPI
from chalicelib.utils import pg_client
from chalicelib.core import alerts_processor
app = FastAPI(root_path="/alerts", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
logging.info("============= ALERTS =============")
from chalicelib.utils import pg_client
@app.get("/")
async def root():
return {"status": "Running"}
app.schedule = AsyncIOScheduler()
loglevel = config("LOGLEVEL", default=logging.INFO)
print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel)
ap_logger = logging.getLogger('apscheduler')
ap_logger.setLevel(loglevel)
app.schedule = AsyncIOScheduler()
@app.on_event("startup")
async def startup():
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logging.info(">>>>> starting up <<<<<")
await pg_client.init()
app.schedule.start()
@ -39,24 +23,44 @@ async def startup():
for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
# App listening
yield
@app.on_event("shutdown")
async def shutdown():
# Shutdown
logging.info(">>>>> shutting down <<<<<")
app.schedule.shutdown(wait=False)
await pg_client.terminate()
@app.get('/private/shutdown', tags=["private"])
async def stop_server():
logging.info("Requested shutdown")
await shutdown()
import os, signal
os.kill(1, signal.SIGTERM)
app = FastAPI(root_path="/alerts", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""),
lifespan=lifespan)
logging.info("============= ALERTS =============")
@app.get("/")
async def root():
return {"status": "Running"}
@app.get("/health")
async def get_health_status():
return {"data": {
"health": True,
"details": {"version": config("version_number", default="unknown")}
}}
app.schedule = AsyncIOScheduler()
loglevel = config("LOGLEVEL", default=logging.INFO)
print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel)
ap_logger = logging.getLogger('apscheduler')
ap_logger.setLevel(loglevel)
app.schedule = AsyncIOScheduler()
if config("LOCAL_DEV", default=False, cast=bool):
@app.get('/private/trigger', tags=["private"])
@app.get('/trigger', tags=["private"])
async def trigger_main_cron():
logging.info("Triggering main cron")
alerts_processor.process()

View file

@ -116,7 +116,7 @@ def process_notifications(data):
BATCH_SIZE = 200
for t in full.keys():
for i in range(0, len(full[t]), BATCH_SIZE):
notifications_list = full[t][i:i + BATCH_SIZE]
notifications_list = full[t][i:min(i + BATCH_SIZE, len(full[t]))]
if notifications_list is None or len(notifications_list) == 0:
break

View file

@ -0,0 +1,172 @@
from urllib.parse import urlparse
import redis
import requests
from decouple import config
from chalicelib.utils import pg_client
if config("LOCAL_DEV", cast=bool, default=False):
HEALTH_ENDPOINTS = {
"alerts": "http://127.0.0.1:8888/metrics",
"assets": "http://127.0.0.1:8888/metrics",
"assist": "http://127.0.0.1:8888/metrics",
"chalice": "http://127.0.0.1:8888/metrics",
"db": "http://127.0.0.1:8888/metrics",
"ender": "http://127.0.0.1:8888/metrics",
"heuristics": "http://127.0.0.1:8888/metrics",
"http": "http://127.0.0.1:8888/metrics",
"ingress-nginx": "http://127.0.0.1:8888/metrics",
"integrations": "http://127.0.0.1:8888/metrics",
"peers": "http://127.0.0.1:8888/metrics",
"quickwit": "http://127.0.0.1:8888/metrics",
"sink": "http://127.0.0.1:8888/metrics",
"sourcemapreader": "http://127.0.0.1:8888/metrics",
"storage": "http://127.0.0.1:8888/metrics",
"utilities": "http://127.0.0.1:8888/metrics"
}
else:
HEALTH_ENDPOINTS = {
"alerts": "http://alerts-openreplay.app.svc.cluster.local:8888/health",
"assets": "http://assets-openreplay.app.svc.cluster.local:8888/metrics",
"assist": "http://assist-openreplay.app.svc.cluster.local:8888/health",
"chalice": "http://chalice-openreplay.app.svc.cluster.local:8888/metrics",
"db": "http://db-openreplay.app.svc.cluster.local:8888/metrics",
"ender": "http://ender-openreplay.app.svc.cluster.local:8888/metrics",
"heuristics": "http://heuristics-openreplay.app.svc.cluster.local:8888/metrics",
"http": "http://http-openreplay.app.svc.cluster.local:8888/metrics",
"ingress-nginx": "http://ingress-nginx-openreplay.app.svc.cluster.local:8888/metrics",
"integrations": "http://integrations-openreplay.app.svc.cluster.local:8888/metrics",
"peers": "http://peers-openreplay.app.svc.cluster.local:8888/health",
"sink": "http://sink-openreplay.app.svc.cluster.local:8888/metrics",
"sourcemapreader": "http://sourcemapreader-openreplay.app.svc.cluster.local:8888/health",
"storage": "http://storage-openreplay.app.svc.cluster.local:8888/metrics",
}
def __check_database_pg():
with pg_client.PostgresClient() as cur:
cur.execute("SHOW server_version;")
server_version = cur.fetchone()
cur.execute("SELECT openreplay_version() AS version;")
schema_version = cur.fetchone()
return {
"health": True,
"details": {
"version": server_version["server_version"],
"schema": schema_version["version"]
}
}
def __not_supported():
return {"errors": ["not supported"]}
def __always_healthy():
return {
"health": True,
"details": {}
}
def __always_healthy_with_version():
return {
"health": True,
"details": {"version": config("version_number", default="unknown")}
}
def __check_be_service(service_name):
def fn():
fail_response = {
"health": False,
"details": {
"errors": ["server health-check failed"]
}
}
try:
results = requests.get(HEALTH_ENDPOINTS.get(service_name), timeout=2)
if results.status_code != 200:
print(f"!! issue with the storage-health code:{results.status_code}")
print(results.text)
fail_response["details"]["errors"].append(results.text)
return fail_response
except requests.exceptions.Timeout:
print(f"!! Timeout getting {service_name}-health")
fail_response["details"]["errors"].append("timeout")
return fail_response
except Exception as e:
print("!! Issue getting storage-health response")
print(str(e))
try:
print(results.text)
fail_response["details"]["errors"].append(results.text)
except:
print("couldn't get response")
fail_response["details"]["errors"].append(str(e))
return fail_response
return {
"health": True,
"details": {}
}
return fn
def __check_redis():
fail_response = {
"health": False,
"details": {"errors": ["server health-check failed"]}
}
if config("REDIS_STRING", default=None) is None:
fail_response["details"]["errors"].append("REDIS_STRING not defined in env-vars")
return fail_response
try:
u = urlparse(config("REDIS_STRING"))
r = redis.Redis(host=u.hostname, port=u.port, socket_timeout=2)
r.ping()
except Exception as e:
print("!! Issue getting redis-health response")
print(str(e))
fail_response["details"]["errors"].append(str(e))
return fail_response
return {
"health": True,
"details": {"version": r.execute_command('INFO')['redis_version']}
}
def get_health():
health_map = {
"databases": {
"postgres": __check_database_pg
},
"ingestionPipeline": {
"redis": __check_redis
},
"backendServices": {
"alerts": __check_be_service("alerts"),
"assets": __check_be_service("assets"),
"assist": __check_be_service("assist"),
"chalice": __always_healthy_with_version,
"db": __check_be_service("db"),
"ender": __check_be_service("ender"),
"frontend": __always_healthy,
"heuristics": __check_be_service("heuristics"),
"http": __check_be_service("http"),
"ingress-nginx": __always_healthy,
"integrations": __check_be_service("integrations"),
"peers": __check_be_service("peers"),
"sink": __check_be_service("sink"),
"sourcemapreader": __check_be_service("sourcemapreader"),
"storage": __check_be_service("storage")
}
}
for parent_key in health_map.keys():
for element_key in health_map[parent_key]:
health_map[parent_key][element_key] = health_map[parent_key][element_key]()
return health_map

View file

@ -1,10 +1,7 @@
from typing import List
import schemas
from chalicelib.core import events, metadata, events_ios, \
sessions_mobs, issues, projects, resources, assist, performance_event, sessions_favorite, \
sessions_devtool, sessions_notes
from chalicelib.utils import errors_helper
from chalicelib.core import events, metadata, projects, performance_event, sessions_favorite
from chalicelib.utils import pg_client, helper, metrics_helper
from chalicelib.utils import sql_helper as sh
@ -33,89 +30,6 @@ COALESCE((SELECT TRUE
AND fs.user_id = %(userId)s LIMIT 1), FALSE) AS viewed """
def __group_metadata(session, project_metadata):
meta = {}
for m in project_metadata.keys():
if project_metadata[m] is not None and session.get(m) is not None:
meta[project_metadata[m]] = session[m]
session.pop(m)
return meta
def get_by_id2_pg(project_id, session_id, context: schemas.CurrentContext, full_data=False, include_fav_viewed=False,
group_metadata=False, live=True):
with pg_client.PostgresClient() as cur:
extra_query = []
if include_fav_viewed:
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_favorite_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS favorite""")
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_viewed_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS viewed""")
query = cur.mogrify(
f"""\
SELECT
s.*,
s.session_id::text AS session_id,
(SELECT project_key FROM public.projects WHERE project_id = %(project_id)s LIMIT 1) AS project_key
{"," if len(extra_query) > 0 else ""}{",".join(extra_query)}
{(",json_build_object(" + ",".join([f"'{m}',p.{m}" for m in metadata.column_names()]) + ") AS project_metadata") if group_metadata else ''}
FROM public.sessions AS s {"INNER JOIN public.projects AS p USING (project_id)" if group_metadata else ""}
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id, "userId": context.user_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
data = cur.fetchone()
if data is not None:
data = helper.dict_to_camel_case(data)
if full_data:
if data["platform"] == 'ios':
data['events'] = events_ios.get_by_sessionId(project_id=project_id, session_id=session_id)
for e in data['events']:
if e["type"].endswith("_IOS"):
e["type"] = e["type"][:-len("_IOS")]
data['crashes'] = events_ios.get_crashes_by_session_id(session_id=session_id)
data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id,
session_id=session_id)
data['mobsUrl'] = sessions_mobs.get_ios(session_id=session_id)
else:
data['events'] = events.get_by_session_id(project_id=project_id, session_id=session_id,
group_clickrage=True)
all_errors = events.get_errors_by_session_id(session_id=session_id, project_id=project_id)
data['stackEvents'] = [e for e in all_errors if e['source'] != "js_exception"]
# to keep only the first stack
# limit the number of errors to reduce the response-body size
data['errors'] = [errors_helper.format_first_stack_frame(e) for e in all_errors
if e['source'] == "js_exception"][:500]
data['userEvents'] = events.get_customs_by_session_id(project_id=project_id,
session_id=session_id)
data['domURL'] = sessions_mobs.get_urls(session_id=session_id, project_id=project_id)
data['mobsUrl'] = sessions_mobs.get_urls_depercated(session_id=session_id)
data['devtoolsURL'] = sessions_devtool.get_urls(session_id=session_id, project_id=project_id)
data['resources'] = resources.get_by_session_id(session_id=session_id, project_id=project_id,
start_ts=data["startTs"], duration=data["duration"])
data['notes'] = sessions_notes.get_session_notes(tenant_id=context.tenant_id, project_id=project_id,
session_id=session_id, user_id=context.user_id)
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id)
data['live'] = live and assist.is_live(project_id=project_id, session_id=session_id,
project_key=data["projectKey"])
data["inDB"] = True
return data
elif live:
return assist.get_live_session_by_id(project_id=project_id, session_id=session_id)
else:
return None
# This function executes the query and return result
def search_sessions(data: schemas.SessionsSearchPayloadSchema, project_id, user_id, errors_only=False,
error_status=schemas.ErrorStatus.all, count_only=False, issue=None, ids_only=False):

View file

@ -1,5 +1,4 @@
import schemas
from chalicelib.core import sessions
from chalicelib.utils import pg_client
@ -8,11 +7,14 @@ def add_favorite_session(context: schemas.CurrentContext, project_id, session_id
cur.execute(
cur.mogrify(f"""\
INSERT INTO public.user_favorite_sessions(user_id, session_id)
VALUES (%(userId)s,%(session_id)s);""",
VALUES (%(userId)s,%(session_id)s)
RETURNING session_id;""",
{"userId": context.user_id, "session_id": session_id})
)
return sessions.get_by_id2_pg(context=context, project_id=project_id, session_id=session_id,
full_data=False, include_fav_viewed=True)
row = cur.fetchone()
if row:
return {"data": {"sessionId": session_id}}
return {"errors": ["something went wrong"]}
def remove_favorite_session(context: schemas.CurrentContext, project_id, session_id):
@ -21,11 +23,14 @@ def remove_favorite_session(context: schemas.CurrentContext, project_id, session
cur.mogrify(f"""\
DELETE FROM public.user_favorite_sessions
WHERE user_id = %(userId)s
AND session_id = %(session_id)s;""",
AND session_id = %(session_id)s
RETURNING session_id;""",
{"userId": context.user_id, "session_id": session_id})
)
return sessions.get_by_id2_pg(context=context, project_id=project_id, session_id=session_id,
full_data=False, include_fav_viewed=True)
row = cur.fetchone()
if row:
return {"data": {"sessionId": session_id}}
return {"errors": ["something went wrong"]}
def favorite_session(context: schemas.CurrentContext, project_id, session_id):

View file

@ -0,0 +1,186 @@
import schemas
from chalicelib.core import events, metadata, events_ios, \
sessions_mobs, issues, resources, assist, sessions_devtool, sessions_notes
from chalicelib.utils import errors_helper
from chalicelib.utils import pg_client, helper
def __group_metadata(session, project_metadata):
meta = {}
for m in project_metadata.keys():
if project_metadata[m] is not None and session.get(m) is not None:
meta[project_metadata[m]] = session[m]
session.pop(m)
return meta
# for backward compatibility
def get_by_id2_pg(project_id, session_id, context: schemas.CurrentContext, full_data=False, include_fav_viewed=False,
group_metadata=False, live=True):
with pg_client.PostgresClient() as cur:
extra_query = []
if include_fav_viewed:
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_favorite_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS favorite""")
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_viewed_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS viewed""")
query = cur.mogrify(
f"""\
SELECT
s.*,
s.session_id::text AS session_id,
(SELECT project_key FROM public.projects WHERE project_id = %(project_id)s LIMIT 1) AS project_key
{"," if len(extra_query) > 0 else ""}{",".join(extra_query)}
{(",json_build_object(" + ",".join([f"'{m}',p.{m}" for m in metadata.column_names()]) + ") AS project_metadata") if group_metadata else ''}
FROM public.sessions AS s {"INNER JOIN public.projects AS p USING (project_id)" if group_metadata else ""}
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id, "userId": context.user_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
data = cur.fetchone()
if data is not None:
data = helper.dict_to_camel_case(data)
if full_data:
if data["platform"] == 'ios':
data['events'] = events_ios.get_by_sessionId(project_id=project_id, session_id=session_id)
for e in data['events']:
if e["type"].endswith("_IOS"):
e["type"] = e["type"][:-len("_IOS")]
data['crashes'] = events_ios.get_crashes_by_session_id(session_id=session_id)
data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id,
session_id=session_id)
data['mobsUrl'] = sessions_mobs.get_ios(session_id=session_id)
else:
data['events'] = events.get_by_session_id(project_id=project_id, session_id=session_id,
group_clickrage=True)
all_errors = events.get_errors_by_session_id(session_id=session_id, project_id=project_id)
data['stackEvents'] = [e for e in all_errors if e['source'] != "js_exception"]
# to keep only the first stack
# limit the number of errors to reduce the response-body size
data['errors'] = [errors_helper.format_first_stack_frame(e) for e in all_errors
if e['source'] == "js_exception"][:500]
data['userEvents'] = events.get_customs_by_session_id(project_id=project_id,
session_id=session_id)
data['domURL'] = sessions_mobs.get_urls(session_id=session_id, project_id=project_id)
data['mobsUrl'] = sessions_mobs.get_urls_depercated(session_id=session_id)
data['devtoolsURL'] = sessions_devtool.get_urls(session_id=session_id, project_id=project_id)
data['resources'] = resources.get_by_session_id(session_id=session_id, project_id=project_id,
start_ts=data["startTs"], duration=data["duration"])
data['notes'] = sessions_notes.get_session_notes(tenant_id=context.tenant_id, project_id=project_id,
session_id=session_id, user_id=context.user_id)
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id)
data['live'] = live and assist.is_live(project_id=project_id, session_id=session_id,
project_key=data["projectKey"])
data["inDB"] = True
return data
elif live:
return assist.get_live_session_by_id(project_id=project_id, session_id=session_id)
else:
return None
def get_replay(project_id, session_id, context: schemas.CurrentContext, full_data=False, include_fav_viewed=False,
group_metadata=False, live=True):
with pg_client.PostgresClient() as cur:
extra_query = []
if include_fav_viewed:
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_favorite_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS favorite""")
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_viewed_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS viewed""")
query = cur.mogrify(
f"""\
SELECT
s.*,
s.session_id::text AS session_id,
(SELECT project_key FROM public.projects WHERE project_id = %(project_id)s LIMIT 1) AS project_key
{"," if len(extra_query) > 0 else ""}{",".join(extra_query)}
{(",json_build_object(" + ",".join([f"'{m}',p.{m}" for m in metadata.column_names()]) + ") AS project_metadata") if group_metadata else ''}
FROM public.sessions AS s {"INNER JOIN public.projects AS p USING (project_id)" if group_metadata else ""}
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id, "userId": context.user_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
data = cur.fetchone()
if data is not None:
data = helper.dict_to_camel_case(data)
if full_data:
if data["platform"] == 'ios':
data['mobsUrl'] = sessions_mobs.get_ios(session_id=session_id)
else:
data['domURL'] = sessions_mobs.get_urls(session_id=session_id, project_id=project_id)
data['mobsUrl'] = sessions_mobs.get_urls_depercated(session_id=session_id)
data['devtoolsURL'] = sessions_devtool.get_urls(session_id=session_id, project_id=project_id)
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
data['live'] = live and assist.is_live(project_id=project_id, session_id=session_id,
project_key=data["projectKey"])
data["inDB"] = True
return data
elif live:
return assist.get_live_session_by_id(project_id=project_id, session_id=session_id)
else:
return None
def get_events(project_id, session_id):
with pg_client.PostgresClient() as cur:
query = cur.mogrify(
f"""SELECT session_id, platform, start_ts, duration
FROM public.sessions AS s
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
s_data = cur.fetchone()
if s_data is not None:
s_data = helper.dict_to_camel_case(s_data)
data = {}
if s_data["platform"] == 'ios':
data['events'] = events_ios.get_by_sessionId(project_id=project_id, session_id=session_id)
for e in data['events']:
if e["type"].endswith("_IOS"):
e["type"] = e["type"][:-len("_IOS")]
data['crashes'] = events_ios.get_crashes_by_session_id(session_id=session_id)
data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id,
session_id=session_id)
else:
data['events'] = events.get_by_session_id(project_id=project_id, session_id=session_id,
group_clickrage=True)
all_errors = events.get_errors_by_session_id(session_id=session_id, project_id=project_id)
data['stackEvents'] = [e for e in all_errors if e['source'] != "js_exception"]
# to keep only the first stack
# limit the number of errors to reduce the response-body size
data['errors'] = [errors_helper.format_first_stack_frame(e) for e in all_errors
if e['source'] == "js_exception"][:500]
data['userEvents'] = events.get_customs_by_session_id(project_id=project_id,
session_id=session_id)
data['resources'] = resources.get_by_session_id(session_id=session_id, project_id=project_id,
start_ts=s_data["startTs"], duration=s_data["duration"])
data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id)
return data
else:
return None

View file

@ -68,7 +68,7 @@ def update(tenant_id, user_id, data: schemas.UpdateTenantSchema):
return edit_client(tenant_id=tenant_id, changes=changes)
def tenants_exists():
with pg_client.PostgresClient() as cur:
def tenants_exists(use_pool=True):
with pg_client.PostgresClient(use_pool=use_pool) as cur:
cur.execute(f"SELECT EXISTS(SELECT 1 FROM public.tenants)")
return cur.fetchone()["exists"]

View file

@ -87,9 +87,10 @@ class PostgresClient:
long_query = False
unlimited_query = False
def __init__(self, long_query=False, unlimited_query=False):
def __init__(self, long_query=False, unlimited_query=False, use_pool=True):
self.long_query = long_query
self.unlimited_query = unlimited_query
self.use_pool = use_pool
if unlimited_query:
long_config = dict(_PG_CONFIG)
long_config["application_name"] += "-UNLIMITED"
@ -100,7 +101,7 @@ class PostgresClient:
long_config["options"] = f"-c statement_timeout=" \
f"{config('pg_long_timeout', cast=int, default=5 * 60) * 1000}"
self.connection = psycopg2.connect(**long_config)
elif not config('PG_POOL', cast=bool, default=True):
elif not use_pool or not config('PG_POOL', cast=bool, default=True):
single_config = dict(_PG_CONFIG)
single_config["application_name"] += "-NOPOOL"
single_config["options"] = f"-c statement_timeout={config('PG_TIMEOUT', cast=int, default=30) * 1000}"
@ -120,11 +121,12 @@ class PostgresClient:
try:
self.connection.commit()
self.cursor.close()
if self.long_query or self.unlimited_query:
if not self.use_pool or self.long_query or self.unlimited_query:
self.connection.close()
except Exception as error:
logging.error("Error while committing/closing PG-connection", error)
if str(error) == "connection already closed" \
and self.use_pool \
and not self.long_query \
and not self.unlimited_query \
and config('PG_POOL', cast=bool, default=True):
@ -134,6 +136,7 @@ class PostgresClient:
raise error
finally:
if config('PG_POOL', cast=bool, default=True) \
and self.use_pool \
and not self.long_query \
and not self.unlimited_query:
postgreSQL_pool.putconn(self.connection)

View file

@ -1,3 +1,3 @@
#!/bin/sh
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --reload --proxy-headers
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers

View file

@ -1,3 +1,3 @@
#!/bin/sh
export ASSIST_KEY=ignore
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --reload
uvicorn app:app --host 0.0.0.0 --port 8888

View file

@ -52,4 +52,4 @@ PRESIGNED_URL_EXPIRATION=3600
ASSIST_JWT_EXPIRATION=144000
ASSIST_JWT_SECRET=
PYTHONUNBUFFERED=1
THUMBNAILS_BUCKET=thumbnails
REDIS_STRING=redis://redis-master.db.svc.cluster.local:6379

View file

@ -8,7 +8,7 @@ jira==3.4.1
fastapi==0.92.0
fastapi==0.94.1
uvicorn[standard]==0.20.0
python-decouple==3.7
pydantic[email]==1.10.4

View file

@ -8,8 +8,10 @@ jira==3.4.1
fastapi==0.92.0
fastapi==0.95.0
uvicorn[standard]==0.20.0
python-decouple==3.7
pydantic[email]==1.10.4
apscheduler==3.10.0
redis==4.5.1

View file

@ -6,7 +6,7 @@ from starlette.responses import RedirectResponse, FileResponse
import schemas
from chalicelib.core import sessions, errors, errors_viewed, errors_favorite, sessions_assignments, heatmaps, \
sessions_favorite, assist, sessions_notes, click_maps
sessions_favorite, assist, sessions_notes, click_maps, sessions_replay
from chalicelib.core import sessions_viewed
from chalicelib.core import tenants, users, projects, license
from chalicelib.core import webhook
@ -145,12 +145,13 @@ async def get_projects(context: schemas.CurrentContext = Depends(OR_context)):
stack_integrations=True)}
@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions"])
# for backward compatibility
@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions", "replay"])
async def get_session(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks,
context: schemas.CurrentContext = Depends(OR_context)):
if isinstance(sessionId, str):
return {"errors": ["session not found"]}
data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True,
data = sessions_replay.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True,
include_fav_viewed=True, group_metadata=True, context=context)
if data is None:
return {"errors": ["session not found"]}
@ -162,6 +163,37 @@ async def get_session(projectId: int, sessionId: Union[int, str], background_tas
}
@app.get('/{projectId}/sessions/{sessionId}/replay', tags=["sessions", "replay"])
async def get_session_events(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks,
context: schemas.CurrentContext = Depends(OR_context)):
if isinstance(sessionId, str):
return {"errors": ["session not found"]}
data = sessions_replay.get_replay(project_id=projectId, session_id=sessionId, full_data=True,
include_fav_viewed=True, group_metadata=True, context=context)
if data is None:
return {"errors": ["session not found"]}
if data.get("inDB"):
background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, user_id=context.user_id,
session_id=sessionId)
return {
'data': data
}
@app.get('/{projectId}/sessions/{sessionId}/events', tags=["sessions", "replay"])
async def get_session_events(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)):
if isinstance(sessionId, str):
return {"errors": ["session not found"]}
data = sessions_replay.get_events(project_id=projectId, session_id=sessionId)
if data is None:
return {"errors": ["session not found"]}
return {
'data': data
}
@app.get('/{projectId}/sessions/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"])
async def get_error_trace(projectId: int, sessionId: int, errorId: str,
context: schemas.CurrentContext = Depends(OR_context)):
@ -239,7 +271,7 @@ async def get_live_session(projectId: int, sessionId: str, background_tasks: Bac
context: schemas.CurrentContext = Depends(OR_context)):
data = assist.get_live_session_by_id(project_id=projectId, session_id=sessionId)
if data is None:
data = sessions.get_by_id2_pg(context=context, project_id=projectId, session_id=sessionId,
data = sessions_replay.get_replay(context=context, project_id=projectId, session_id=sessionId,
full_data=True, include_fav_viewed=True, group_metadata=True, live=False)
if data is None:
return {"errors": ["session not found"]}

View file

@ -0,0 +1,14 @@
from chalicelib.core import health, tenants
from routers.base import get_routers
public_app, app, app_apikey = get_routers()
health_router = public_app
if tenants.tenants_exists(use_pool=False):
health_router = app
@health_router.get('/health', tags=["health-check"])
def get_global_health_status():
return {"data": health.get_health()}

View file

@ -1,3 +1,3 @@
#!/bin/zsh
uvicorn app_alerts:app --reload
uvicorn app_alerts:app --reload --port 8888

View file

@ -35,20 +35,20 @@ update_helm_release() {
}
function build_api(){
destination="_utilities"
destination="_assist"
[[ $1 == "ee" ]] && {
destination="_utilities_ee"
destination="_assist_ee"
}
cp -R ../utilities ../${destination}
cp -R ../assist ../${destination}
cd ../${destination}
# Copy enterprise code
[[ $1 == "ee" ]] && {
cp -rf ../ee/utilities/* ./
cp -rf ../ee/assist/* ./
}
docker build -f ./Dockerfile --build-arg GIT_SHA=$git_sha -t ${DOCKER_REPO:-'local'}/assist:${image_tag} .
cd ../utilities
cd ../assist
rm -rf ../${destination}
[[ $PUSH_IMAGE -eq 1 ]] && {
docker push ${DOCKER_REPO:-'local'}/assist:${image_tag}

View file

@ -45,9 +45,9 @@
}
},
"node_modules/@types/node": {
"version": "18.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
"version": "18.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz",
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA=="
},
"node_modules/accepts": {
"version": "1.3.8",
@ -987,9 +987,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"funding": [
{
"type": "opencollective",

View file

@ -1,6 +1,6 @@
{
"name": "assist-server",
"version": "1.0.0",
"version": "v1.11.0",
"description": "assist server to get live sessions & sourcemaps reader to get stack trace",
"main": "peerjs-server.js",
"scripts": {

View file

@ -2,6 +2,7 @@ const dumps = require('./utils/HeapSnapshot');
const express = require('express');
const socket = require("./servers/websocket");
const {request_logger} = require("./utils/helper");
const health = require("./utils/health");
const assert = require('assert').strict;
const debug = process.env.debug === "1";
@ -10,7 +11,7 @@ const HOST = process.env.LISTEN_HOST || '0.0.0.0';
const PORT = process.env.LISTEN_PORT || 9001;
assert.ok(process.env.ASSIST_KEY, 'The "ASSIST_KEY" environment variable is required');
const P_KEY = process.env.ASSIST_KEY;
const PREFIX = process.env.PREFIX || process.env.prefix || `/assist`
const PREFIX = process.env.PREFIX || process.env.prefix || `/assist`;
const wsapp = express();
wsapp.use(express.json());
@ -27,16 +28,9 @@ heapdump && wsapp.use(`${PREFIX}/${P_KEY}/heapdump`, dumps.router);
const wsserver = wsapp.listen(PORT, HOST, () => {
console.log(`WS App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
health.healthApp.listen(health.PORT, HOST, health.listen_cb);
});
wsapp.enable('trust proxy');
socket.start(wsserver);
module.exports = {wsserver};
wsapp.get('/private/shutdown', (req, res) => {
console.log("Requested shutdown");
res.statusCode = 200;
res.end("ok!");
process.kill(1, "SIGTERM");
}
);

View file

@ -26,7 +26,7 @@ const debug = process.env.debug === "1";
const createSocketIOServer = function (server, prefix) {
io = _io(server, {
maxHttpBufferSize: (parseInt(process.env.maxHttpBufferSize) || 5) * 1e6,
maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
@ -45,7 +45,22 @@ const respond = function (res, data) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": data}));
}
const countSessions = async function () {
let count = 0;
try {
const arr = Array.from(io.sockets.adapter.rooms);
const filtered = arr.filter(room => !room[1].has(room[0]));
for (let i of filtered) {
let {projectKey, sessionId} = extractPeerId(i[0]);
if (projectKey !== null && sessionId !== null) {
count++;
}
}
} catch (e) {
console.error(e);
}
return count;
}
const socketsList = async function (req, res) {
debug && console.log("[WS]looking for all available sessions");
let filters = extractPayloadFromRequest(req);
@ -360,6 +375,7 @@ module.exports = {
socketConnexionTimeout(io);
},
countSessions,
handlers: {
socketsList,
socketsListByProject,

54
assist/utils/health.js Normal file
View file

@ -0,0 +1,54 @@
const express = require('express');
const socket = require("../servers/websocket");
const HOST = process.env.LISTEN_HOST || '0.0.0.0';
const PORT = process.env.HEALTH_PORT || 8888;
const {request_logger} = require("./helper");
const debug = process.env.debug === "1";
const respond = function (res, data) {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": data}));
}
const check_health = async function (req, res) {
debug && console.log("[WS]looking for all available sessions");
respond(res, {
"health": true,
"details": {
"version": process.env.npm_package_version,
"connectedSessions": await socket.countSessions()
}
});
}
const healthApp = express();
healthApp.use(express.json());
healthApp.use(express.urlencoded({extended: true}));
healthApp.use(request_logger("[healthApp]"));
healthApp.get(['/'], (req, res) => {
res.statusCode = 200;
res.end("healthApp ok!");
}
);
healthApp.get('/health', check_health);
healthApp.get('/shutdown', (req, res) => {
console.log("Requested shutdown");
res.statusCode = 200;
res.end("ok!");
process.kill(1, "SIGTERM");
}
);
const listen_cb = async function () {
console.log(`Health App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
}
module.exports = {
healthApp,
PORT,
listen_cb
};

4
ee/api/.gitignore vendored
View file

@ -215,6 +215,7 @@ Pipfile.lock
/chalicelib/core/log_tool_sumologic.py
/chalicelib/core/metadata.py
/chalicelib/core/mobile.py
/chalicelib/core/sessions.py
/chalicelib/core/sessions_assignments.py
#exp /chalicelib/core/sessions_metas.py
/chalicelib/core/sessions_mobs.py
@ -264,5 +265,8 @@ Pipfile.lock
/app_alerts.py
/build_alerts.sh
/build_crons.sh
/run-dev.sh
/run-alerts-dev.sh
/routers/subs/health.py
/routers/subs/v1_api.py
#exp /chalicelib/core/dashboards.py

View file

@ -1,5 +1,6 @@
import logging
import queue
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from decouple import config
@ -10,17 +11,54 @@ from starlette import status
from starlette.responses import StreamingResponse, JSONResponse
from chalicelib.core import traces
from chalicelib.utils import events_queue
from chalicelib.utils import helper
from chalicelib.utils import pg_client
from chalicelib.utils import events_queue
from routers import core, core_dynamic, ee, saml
from routers.crons import core_crons
from routers.crons import core_dynamic_crons
from routers.crons import ee_crons
from routers.subs import insights, metrics, v1_api_ee
from routers.subs import v1_api
from routers.subs import v1_api, health
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
loglevel = config("LOGLEVEL", default=logging.INFO)
print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logging.info(">>>>> starting up <<<<<")
ap_logger = logging.getLogger('apscheduler')
ap_logger.setLevel(loglevel)
app.schedule = AsyncIOScheduler()
app.queue_system = queue.Queue()
await pg_client.init()
await events_queue.init()
app.schedule.start()
for job in core_crons.cron_jobs + core_dynamic_crons.cron_jobs + traces.cron_jobs + ee_crons.ee_cron_jobs:
app.schedule.add_job(id=job["func"].__name__, **job)
ap_logger.info(">Scheduled jobs:")
for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
# App listening
yield
# Shutdown
logging.info(">>>>> shutting down <<<<<")
app.schedule.shutdown(wait=True)
await traces.process_traces_queue()
await events_queue.terminate()
await pg_client.terminate()
app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""),
lifespan=lifespan)
app.add_middleware(GZipMiddleware, minimum_size=1000)
@ -68,43 +106,6 @@ app.include_router(metrics.app)
app.include_router(insights.app)
app.include_router(v1_api.app_apikey)
app.include_router(v1_api_ee.app_apikey)
loglevel = config("LOGLEVEL", default=logging.INFO)
print(f">Loglevel set to: {loglevel}")
logging.basicConfig(level=loglevel)
ap_logger = logging.getLogger('apscheduler')
ap_logger.setLevel(loglevel)
app.schedule = AsyncIOScheduler()
app.queue_system = queue.Queue()
@app.on_event("startup")
async def startup():
logging.info(">>>>> starting up <<<<<")
await pg_client.init()
await events_queue.init()
app.schedule.start()
for job in core_crons.cron_jobs + core_dynamic_crons.cron_jobs + traces.cron_jobs + ee_crons.ee_cron_jobs:
app.schedule.add_job(id=job["func"].__name__, **job)
ap_logger.info(">Scheduled jobs:")
for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
@app.on_event("shutdown")
async def shutdown():
logging.info(">>>>> shutting down <<<<<")
app.schedule.shutdown(wait=True)
await traces.process_traces_queue()
await events_queue.terminate()
await pg_client.terminate()
@app.get('/private/shutdown', tags=["private"])
async def stop_server():
logging.info("Requested shutdown")
await shutdown()
import os, signal
os.kill(1, signal.SIGTERM)
app.include_router(health.public_app)
app.include_router(health.app)
app.include_router(health.app_apikey)

View file

@ -0,0 +1,228 @@
from urllib.parse import urlparse
import redis
import requests
# from confluent_kafka.admin import AdminClient
from decouple import config
from chalicelib.utils import pg_client, ch_client
if config("LOCAL_DEV", cast=bool, default=False):
HEALTH_ENDPOINTS = {
"alerts": "http://127.0.0.1:8888/metrics",
"assets": "http://127.0.0.1:8888/metrics",
"assist": "http://127.0.0.1:8888/metrics",
"chalice": "http://127.0.0.1:8888/metrics",
"db": "http://127.0.0.1:8888/metrics",
"ender": "http://127.0.0.1:8888/metrics",
"heuristics": "http://127.0.0.1:8888/metrics",
"http": "http://127.0.0.1:8888/metrics",
"ingress-nginx": "http://127.0.0.1:8888/metrics",
"integrations": "http://127.0.0.1:8888/metrics",
"peers": "http://127.0.0.1:8888/metrics",
"quickwit": "http://127.0.0.1:8888/metrics",
"sink": "http://127.0.0.1:8888/metrics",
"sourcemapreader": "http://127.0.0.1:8888/metrics",
"storage": "http://127.0.0.1:8888/metrics",
"utilities": "http://127.0.0.1:8888/metrics"
}
else:
HEALTH_ENDPOINTS = {
"alerts": "http://alerts-openreplay.app.svc.cluster.local:8888/health",
"assets": "http://assets-openreplay.app.svc.cluster.local:8888/metrics",
"assist": "http://assist-openreplay.app.svc.cluster.local:8888/health",
"chalice": "http://chalice-openreplay.app.svc.cluster.local:8888/metrics",
"db": "http://db-openreplay.app.svc.cluster.local:8888/metrics",
"ender": "http://ender-openreplay.app.svc.cluster.local:8888/metrics",
"heuristics": "http://heuristics-openreplay.app.svc.cluster.local:8888/metrics",
"http": "http://http-openreplay.app.svc.cluster.local:8888/metrics",
"ingress-nginx": "http://ingress-nginx-openreplay.app.svc.cluster.local:8888/metrics",
"integrations": "http://integrations-openreplay.app.svc.cluster.local:8888/metrics",
"peers": "http://peers-openreplay.app.svc.cluster.local:8888/health",
"quickwit": "http://quickwit-openreplay.app.svc.cluster.local:8888/metrics",
"sink": "http://sink-openreplay.app.svc.cluster.local:8888/metrics",
"sourcemapreader": "http://sourcemapreader-openreplay.app.svc.cluster.local:8888/health",
"storage": "http://storage-openreplay.app.svc.cluster.local:8888/metrics",
}
def __check_database_pg():
with pg_client.PostgresClient() as cur:
cur.execute("SHOW server_version;")
server_version = cur.fetchone()
cur.execute("SELECT openreplay_version() AS version;")
schema_version = cur.fetchone()
return {
"health": True,
"details": {
"version": server_version["server_version"],
"schema": schema_version["version"]
}
}
def __not_supported():
return {"errors": ["not supported"]}
def __always_healthy():
return {
"health": True,
"details": {}
}
def __always_healthy_with_version():
return {
"health": True,
"details": {"version": config("version_number", default="unknown")}
}
def __check_be_service(service_name):
def fn():
fail_response = {
"health": False,
"details": {
"errors": ["server health-check failed"]
}
}
try:
results = requests.get(HEALTH_ENDPOINTS.get(service_name), timeout=2)
if results.status_code != 200:
print(f"!! issue with the storage-health code:{results.status_code}")
print(results.text)
fail_response["details"]["errors"].append(results.text)
return fail_response
except requests.exceptions.Timeout:
print(f"!! Timeout getting {service_name}-health")
fail_response["details"]["errors"].append("timeout")
return fail_response
except Exception as e:
print("!! Issue getting storage-health response")
print(str(e))
try:
print(results.text)
fail_response["details"]["errors"].append(results.text)
except:
print("couldn't get response")
fail_response["details"]["errors"].append(str(e))
return fail_response
return {
"health": True,
"details": {}
}
return fn
def __check_redis():
fail_response = {
"health": False,
"details": {"errors": ["server health-check failed"]}
}
if config("REDIS_STRING", default=None) is None:
fail_response["details"]["errors"].append("REDIS_STRING not defined in env-vars")
return fail_response
try:
u = urlparse(config("REDIS_STRING"))
r = redis.Redis(host=u.hostname, port=u.port, socket_timeout=2)
r.ping()
except Exception as e:
print("!! Issue getting redis-health response")
print(str(e))
fail_response["details"]["errors"].append(str(e))
return fail_response
return {
"health": True,
"details": {"version": r.execute_command('INFO')['redis_version']}
}
def get_health():
health_map = {
"databases": {
"postgres": __check_database_pg,
"clickhouse": __check_database_ch
},
"ingestionPipeline": {
"redis": __check_redis,
# "kafka": __check_kafka
"kafka": __always_healthy
},
"backendServices": {
"alerts": __check_be_service("alerts"),
"assets": __check_be_service("assets"),
"assist": __check_be_service("assist"),
"chalice": __always_healthy_with_version,
"db": __check_be_service("db"),
"ender": __check_be_service("ender"),
"frontend": __always_healthy,
"heuristics": __check_be_service("heuristics"),
"http": __check_be_service("http"),
"ingress-nginx": __always_healthy,
"integrations": __check_be_service("integrations"),
"peers": __check_be_service("peers"),
"quickwit": __check_be_service("quickwit"),
"sink": __check_be_service("sink"),
"sourcemapreader": __check_be_service("sourcemapreader"),
"storage": __check_be_service("storage")
}
}
for parent_key in health_map.keys():
for element_key in health_map[parent_key]:
health_map[parent_key][element_key] = health_map[parent_key][element_key]()
return health_map
def __check_database_ch():
errors = {}
with ch_client.ClickHouseClient() as ch:
server_version = ch.execute("SELECT version() AS server_version;")
schema_version = ch.execute("""SELECT 1
FROM system.functions
WHERE name = 'openreplay_version';""")
if len(schema_version) > 0:
schema_version = ch.execute("SELECT openreplay_version()() AS version;")
schema_version = schema_version[0]["version"]
else:
schema_version = "unknown"
errors = {"errors": ["clickhouse schema is outdated"]}
return {
"health": True,
"details": {
"version": server_version[0]["server_version"],
"schema": schema_version,
**errors
}
}
# def __check_kafka():
# fail_response = {
# "health": False,
# "details": {"errors": ["server health-check failed"]}
# }
# if config("KAFKA_SERVERS", default=None) is None:
# fail_response["details"]["errors"].append("KAFKA_SERVERS not defined in env-vars")
# return fail_response
#
# try:
# a = AdminClient({'bootstrap.servers': config("KAFKA_SERVERS"), "socket.connection.setup.timeout.ms": 3000})
# topics = a.list_topics().topics
# if not topics:
# raise Exception('topics not found')
#
# except Exception as e:
# print("!! Issue getting kafka-health response")
# print(str(e))
# fail_response["details"]["errors"].append(str(e))
# return fail_response
#
# return {
# "health": True,
# "details": {}
# }

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,8 @@ from typing import List, Union
import schemas
import schemas_ee
from chalicelib.core import events, metadata, events_ios, \
sessions_mobs, issues, projects, resources, assist, performance_event, metrics, sessions_devtool, \
sessions_notes
from chalicelib.utils import pg_client, helper, metrics_helper, ch_client, exp_ch_helper, errors_helper
from chalicelib.utils import sql_helper as sh
from chalicelib.core import events, metadata, projects, performance_event, metrics
from chalicelib.utils import pg_client, helper, metrics_helper, ch_client, exp_ch_helper
SESSION_PROJECTION_COLS_CH = """\
s.project_id,
@ -51,94 +48,6 @@ SESSION_PROJECTION_COLS_CH_MAP = """\
"""
def __group_metadata(session, project_metadata):
meta = {}
for m in project_metadata.keys():
if project_metadata[m] is not None and session.get(m) is not None:
meta[project_metadata[m]] = session[m]
session.pop(m)
return meta
# This function should not use Clickhouse because it doesn't have `file_key`
def get_by_id2_pg(project_id, session_id, context: schemas_ee.CurrentContext, full_data=False, include_fav_viewed=False,
group_metadata=False, live=True):
with pg_client.PostgresClient() as cur:
extra_query = []
if include_fav_viewed:
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_favorite_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS favorite""")
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_viewed_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS viewed""")
query = cur.mogrify(
f"""\
SELECT
s.*,
s.session_id::text AS session_id,
(SELECT project_key FROM public.projects WHERE project_id = %(project_id)s LIMIT 1) AS project_key,
encode(file_key,'hex') AS file_key
{"," if len(extra_query) > 0 else ""}{",".join(extra_query)}
{(",json_build_object(" + ",".join([f"'{m}',p.{m}" for m in metadata.column_names()]) + ") AS project_metadata") if group_metadata else ''}
FROM public.sessions AS s {"INNER JOIN public.projects AS p USING (project_id)" if group_metadata else ""}
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id, "userId": context.user_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
data = cur.fetchone()
if data is not None:
data = helper.dict_to_camel_case(data)
if full_data:
if data["platform"] == 'ios':
data['events'] = events_ios.get_by_sessionId(project_id=project_id, session_id=session_id)
for e in data['events']:
if e["type"].endswith("_IOS"):
e["type"] = e["type"][:-len("_IOS")]
data['crashes'] = events_ios.get_crashes_by_session_id(session_id=session_id)
data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id,
session_id=session_id)
data['mobsUrl'] = sessions_mobs.get_ios(session_id=session_id)
else:
data['events'] = events.get_by_session_id(project_id=project_id, session_id=session_id,
group_clickrage=True)
all_errors = events.get_errors_by_session_id(session_id=session_id, project_id=project_id)
data['stackEvents'] = [e for e in all_errors if e['source'] != "js_exception"]
# to keep only the first stack
# limit the number of errors to reduce the response-body size
data['errors'] = [errors_helper.format_first_stack_frame(e) for e in all_errors
if e['source'] == "js_exception"][:500]
data['userEvents'] = events.get_customs_by_session_id(project_id=project_id,
session_id=session_id)
data['domURL'] = sessions_mobs.get_urls(session_id=session_id, project_id=project_id)
data['mobsUrl'] = sessions_mobs.get_urls_depercated(session_id=session_id)
data['devtoolsURL'] = sessions_devtool.get_urls(session_id=session_id, project_id=project_id,
context=context)
data['resources'] = resources.get_by_session_id(session_id=session_id, project_id=project_id,
start_ts=data["startTs"],
duration=data["duration"])
data['notes'] = sessions_notes.get_session_notes(tenant_id=context.tenant_id, project_id=project_id,
session_id=session_id, user_id=context.user_id)
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id)
data['live'] = live and assist.is_live(project_id=project_id,
session_id=session_id,
project_key=data["projectKey"])
data["inDB"] = True
return data
elif live:
return assist.get_live_session_by_id(project_id=project_id, session_id=session_id)
else:
return None
def __get_sql_operator(op: schemas.SearchEventOperator):
return {
schemas.SearchEventOperator._is: "=",

View file

@ -10,13 +10,15 @@ def add_favorite_session(context: schemas_ee.CurrentContext, project_id, session
cur.execute(
cur.mogrify(f"""\
INSERT INTO public.user_favorite_sessions(user_id, session_id)
VALUES (%(userId)s,%(sessionId)s);""",
{"userId": context.user_id, "sessionId": session_id})
VALUES (%(userId)s,%(session_id)s)
RETURNING session_id;""",
{"userId": context.user_id, "session_id": session_id})
)
row = cur.fetchone()
if row:
sessions_favorite_exp.add_favorite_session(project_id=project_id, user_id=context.user_id, session_id=session_id)
return sessions.get_by_id2_pg(project_id=project_id, session_id=session_id,
full_data=False, include_fav_viewed=True, context=context)
return {"data": {"sessionId": session_id}}
return {"errors": ["something went wrong"]}
def remove_favorite_session(context: schemas_ee.CurrentContext, project_id, session_id):
@ -25,12 +27,15 @@ def remove_favorite_session(context: schemas_ee.CurrentContext, project_id, sess
cur.mogrify(f"""\
DELETE FROM public.user_favorite_sessions
WHERE user_id = %(userId)s
AND session_id = %(sessionId)s;""",
{"userId": context.user_id, "sessionId": session_id})
AND session_id = %(session_id)s
RETURNING session_id;""",
{"userId": context.user_id, "session_id": session_id})
)
row = cur.fetchone()
if row:
sessions_favorite_exp.remove_favorite_session(project_id=project_id, user_id=context.user_id, session_id=session_id)
return sessions.get_by_id2_pg(project_id=project_id, session_id=session_id,
full_data=False, include_fav_viewed=True, context=context)
return {"data": {"sessionId": session_id}}
return {"errors": ["something went wrong"]}
def favorite_session(context: schemas_ee.CurrentContext, project_id, session_id):

View file

@ -0,0 +1,192 @@
import schemas
import schemas_ee
from chalicelib.core import events, metadata, events_ios, \
sessions_mobs, issues, resources, assist, sessions_devtool, sessions_notes
from chalicelib.utils import errors_helper
from chalicelib.utils import pg_client, helper
def __group_metadata(session, project_metadata):
meta = {}
for m in project_metadata.keys():
if project_metadata[m] is not None and session.get(m) is not None:
meta[project_metadata[m]] = session[m]
session.pop(m)
return meta
# for backward compatibility
# This function should not use Clickhouse because it doesn't have `file_key`
def get_by_id2_pg(project_id, session_id, context: schemas_ee.CurrentContext, full_data=False,
include_fav_viewed=False, group_metadata=False, live=True):
with pg_client.PostgresClient() as cur:
extra_query = []
if include_fav_viewed:
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_favorite_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS favorite""")
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_viewed_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS viewed""")
query = cur.mogrify(
f"""\
SELECT
s.*,
s.session_id::text AS session_id,
(SELECT project_key FROM public.projects WHERE project_id = %(project_id)s LIMIT 1) AS project_key,
encode(file_key,'hex') AS file_key
{"," if len(extra_query) > 0 else ""}{",".join(extra_query)}
{(",json_build_object(" + ",".join([f"'{m}',p.{m}" for m in metadata.column_names()]) + ") AS project_metadata") if group_metadata else ''}
FROM public.sessions AS s {"INNER JOIN public.projects AS p USING (project_id)" if group_metadata else ""}
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id, "userId": context.user_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
data = cur.fetchone()
if data is not None:
data = helper.dict_to_camel_case(data)
if full_data:
if data["platform"] == 'ios':
data['events'] = events_ios.get_by_sessionId(project_id=project_id, session_id=session_id)
for e in data['events']:
if e["type"].endswith("_IOS"):
e["type"] = e["type"][:-len("_IOS")]
data['crashes'] = events_ios.get_crashes_by_session_id(session_id=session_id)
data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id,
session_id=session_id)
data['mobsUrl'] = sessions_mobs.get_ios(session_id=session_id)
else:
data['events'] = events.get_by_session_id(project_id=project_id, session_id=session_id,
group_clickrage=True)
all_errors = events.get_errors_by_session_id(session_id=session_id, project_id=project_id)
data['stackEvents'] = [e for e in all_errors if e['source'] != "js_exception"]
# to keep only the first stack
# limit the number of errors to reduce the response-body size
data['errors'] = [errors_helper.format_first_stack_frame(e) for e in all_errors
if e['source'] == "js_exception"][:500]
data['userEvents'] = events.get_customs_by_session_id(project_id=project_id,
session_id=session_id)
data['domURL'] = sessions_mobs.get_urls(session_id=session_id, project_id=project_id)
data['mobsUrl'] = sessions_mobs.get_urls_depercated(session_id=session_id)
data['devtoolsURL'] = sessions_devtool.get_urls(session_id=session_id, project_id=project_id,
context=context)
data['resources'] = resources.get_by_session_id(session_id=session_id, project_id=project_id,
start_ts=data["startTs"], duration=data["duration"])
data['notes'] = sessions_notes.get_session_notes(tenant_id=context.tenant_id, project_id=project_id,
session_id=session_id, user_id=context.user_id)
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id)
data['live'] = live and assist.is_live(project_id=project_id, session_id=session_id,
project_key=data["projectKey"])
data["inDB"] = True
return data
elif live:
return assist.get_live_session_by_id(project_id=project_id, session_id=session_id)
else:
return None
# This function should not use Clickhouse because it doesn't have `file_key`
def get_replay(project_id, session_id, context: schemas.CurrentContext, full_data=False, include_fav_viewed=False,
group_metadata=False, live=True):
with pg_client.PostgresClient() as cur:
extra_query = []
if include_fav_viewed:
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_favorite_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS favorite""")
extra_query.append("""COALESCE((SELECT TRUE
FROM public.user_viewed_sessions AS fs
WHERE s.session_id = fs.session_id
AND fs.user_id = %(userId)s), FALSE) AS viewed""")
query = cur.mogrify(
f"""\
SELECT
s.*,
s.session_id::text AS session_id,
(SELECT project_key FROM public.projects WHERE project_id = %(project_id)s LIMIT 1) AS project_key
{"," if len(extra_query) > 0 else ""}{",".join(extra_query)}
{(",json_build_object(" + ",".join([f"'{m}',p.{m}" for m in metadata.column_names()]) + ") AS project_metadata") if group_metadata else ''}
FROM public.sessions AS s {"INNER JOIN public.projects AS p USING (project_id)" if group_metadata else ""}
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id, "userId": context.user_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
data = cur.fetchone()
if data is not None:
data = helper.dict_to_camel_case(data)
if full_data:
if data["platform"] == 'ios':
data['mobsUrl'] = sessions_mobs.get_ios(session_id=session_id)
else:
data['domURL'] = sessions_mobs.get_urls(session_id=session_id, project_id=project_id)
data['mobsUrl'] = sessions_mobs.get_urls_depercated(session_id=session_id)
data['devtoolsURL'] = sessions_devtool.get_urls(session_id=session_id, project_id=project_id,
context=context)
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
data['live'] = live and assist.is_live(project_id=project_id, session_id=session_id,
project_key=data["projectKey"])
data["inDB"] = True
return data
elif live:
return assist.get_live_session_by_id(project_id=project_id, session_id=session_id)
else:
return None
def get_events(project_id, session_id):
with pg_client.PostgresClient() as cur:
query = cur.mogrify(
f"""SELECT session_id, platform, start_ts, duration
FROM public.sessions AS s
WHERE s.project_id = %(project_id)s
AND s.session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id}
)
# print("===============")
# print(query)
cur.execute(query=query)
s_data = cur.fetchone()
if s_data is not None:
s_data = helper.dict_to_camel_case(s_data)
data = {}
if s_data["platform"] == 'ios':
data['events'] = events_ios.get_by_sessionId(project_id=project_id, session_id=session_id)
for e in data['events']:
if e["type"].endswith("_IOS"):
e["type"] = e["type"][:-len("_IOS")]
data['crashes'] = events_ios.get_crashes_by_session_id(session_id=session_id)
data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id,
session_id=session_id)
else:
data['events'] = events.get_by_session_id(project_id=project_id, session_id=session_id,
group_clickrage=True)
all_errors = events.get_errors_by_session_id(session_id=session_id, project_id=project_id)
data['stackEvents'] = [e for e in all_errors if e['source'] != "js_exception"]
# to keep only the first stack
# limit the number of errors to reduce the response-body size
data['errors'] = [errors_helper.format_first_stack_frame(e) for e in all_errors
if e['source'] == "js_exception"][:500]
data['userEvents'] = events.get_customs_by_session_id(project_id=project_id,
session_id=session_id)
data['resources'] = resources.get_by_session_id(session_id=session_id, project_id=project_id,
start_ts=s_data["startTs"], duration=s_data["duration"])
data['issues'] = issues.get_by_session_id(session_id=session_id, project_id=project_id)
return data
else:
return None

View file

@ -94,7 +94,7 @@ def update(tenant_id, user_id, data: schemas.UpdateTenantSchema):
return edit_client(tenant_id=tenant_id, changes=changes)
def tenants_exists():
with pg_client.PostgresClient() as cur:
def tenants_exists(use_pool=True):
with pg_client.PostgresClient(use_pool=use_pool) as cur:
cur.execute(f"SELECT EXISTS(SELECT 1 FROM public.tenants)")
return cur.fetchone()["exists"]

View file

@ -37,13 +37,16 @@ def get_full_config():
if __get_secret() is not None:
for i in range(len(servers)):
url = servers[i].split(",")[0]
servers[i] = {"url": url} if url.lower().startswith("stun") else {"url": url, **credentials}
# servers[i] = {"url": url} if url.lower().startswith("stun") else {"url": url, **credentials}
servers[i] = {"urls": url} if url.lower().startswith("stun") else {"urls": url, **credentials}
else:
for i in range(len(servers)):
s = servers[i].split(",")
if len(s) == 3:
servers[i] = {"url": s[0], "username": s[1], "credential": s[2]}
# servers[i] = {"url": s[0], "username": s[1], "credential": s[2]}
servers[i] = {"urls": s[0], "username": s[1], "credential": s[2]}
else:
servers[i] = {"url": s[0]}
# servers[i] = {"url": s[0]}
servers[i] = {"urls": s[0]}
return servers

View file

@ -20,8 +20,9 @@ class ClickHouseClient:
def __init__(self):
self.__client = clickhouse_driver.Client(host=config("ch_host"),
database=config("ch_database",default="default", cast=str),
password=config("ch_password",default="", cast=str),
database=config("ch_database", default="default"),
user=config("ch_user", default="default"),
password=config("ch_password", default=""),
port=config("ch_port", cast=int),
settings=settings) \
if self.__client is None else self.__client

View file

@ -35,6 +35,7 @@ rm -rf ./chalicelib/core/log_tool_stackdriver.py
rm -rf ./chalicelib/core/log_tool_sumologic.py
rm -rf ./chalicelib/core/metadata.py
rm -rf ./chalicelib/core/mobile.py
rm -rf ./chalicelib/core/sessions.py
rm -rf ./chalicelib/core/sessions_assignments.py
#exp rm -rf ./chalicelib/core/sessions_metas.py
rm -rf ./chalicelib/core/sessions_mobs.py
@ -78,9 +79,12 @@ rm -rf ./Dockerfile_bundle
rm -rf ./entrypoint.bundle.sh
rm -rf ./chalicelib/core/heatmaps.py
rm -rf ./schemas.py
rm -rf ./routers/subs/health.py
rm -rf ./routers/subs/v1_api.py
#exp rm -rf ./chalicelib/core/custom_metrics.py
rm -rf ./chalicelib/core/performance_event.py
rm -rf ./chalicelib/core/saved_search.py
rm -rf ./app_alerts.py
rm -rf ./build_alerts.sh
rm -rf ./run-dev.sh
rm -rf ./run-alerts-dev.sh

View file

@ -2,4 +2,4 @@
sh env_vars.sh
source /tmp/.env.override
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --reload --proxy-headers
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers

View file

@ -2,4 +2,4 @@
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --reload
uvicorn app:app --host 0.0.0.0 --port 8888

View file

@ -71,3 +71,6 @@ DEVTOOLS_MOB_PATTERN=%(sessionId)s/devtools.mob
PRESIGNED_URL_EXPIRATION=3600
ASSIST_JWT_EXPIRATION=144000
ASSIST_JWT_SECRET=
REDIS_STRING=redis://redis-master.db.svc.cluster.local:6379
KAFKA_SERVERS=kafka.db.svc.cluster.local:9092
KAFKA_USE_SSL=false

View file

@ -8,7 +8,7 @@ jira==3.4.1
fastapi==0.92.0
fastapi==0.94.1
uvicorn[standard]==0.20.0
python-decouple==3.7
pydantic[email]==1.10.4

View file

@ -8,7 +8,7 @@ jira==3.4.1
fastapi==0.92.0
fastapi==0.95.0
uvicorn[standard]==0.20.0
python-decouple==3.7
pydantic[email]==1.10.4
@ -17,3 +17,6 @@ apscheduler==3.10.0
clickhouse-driver==0.2.5
python3-saml==1.15.0
python-multipart==0.0.5
redis==4.5.1
#confluent-kafka==2.0.2

View file

@ -7,7 +7,7 @@ from starlette.responses import RedirectResponse, FileResponse
import schemas
import schemas_ee
from chalicelib.core import sessions, assist, heatmaps, sessions_favorite, sessions_assignments, errors, errors_viewed, \
errors_favorite, sessions_notes, click_maps
errors_favorite, sessions_notes, click_maps, sessions_replay
from chalicelib.core import sessions_viewed
from chalicelib.core import tenants, users, projects, license
from chalicelib.core import webhook
@ -59,7 +59,8 @@ async def edit_account(data: schemas_ee.EditUserSchema = Body(...),
@app.post('/integrations/slack', tags=['integrations'])
@app.put('/integrations/slack', tags=['integrations'])
async def add_slack_client(data: schemas.AddCollaborationSchema, context: schemas.CurrentContext = Depends(OR_context)):
async def add_slack_integration(data: schemas.AddCollaborationSchema,
context: schemas.CurrentContext = Depends(OR_context)):
n = Slack.add(tenant_id=context.tenant_id, data=data)
if n is None:
return {
@ -155,12 +156,14 @@ async def get_projects(context: schemas.CurrentContext = Depends(OR_context)):
stack_integrations=True, user_id=context.user_id)}
@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions"], dependencies=[OR_scope(Permissions.session_replay)])
# for backward compatibility
@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions", "replay"],
dependencies=[OR_scope(Permissions.session_replay)])
async def get_session(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks,
context: schemas.CurrentContext = Depends(OR_context)):
if isinstance(sessionId, str):
return {"errors": ["session not found"]}
data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True,
data = sessions_replay.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True,
include_fav_viewed=True, group_metadata=True, context=context)
if data is None:
return {"errors": ["session not found"]}
@ -172,6 +175,39 @@ async def get_session(projectId: int, sessionId: Union[int, str], background_tas
}
@app.get('/{projectId}/sessions/{sessionId}/replay', tags=["sessions", "replay"],
dependencies=[OR_scope(Permissions.session_replay)])
async def get_session_events(projectId: int, sessionId: Union[int, str], background_tasks: BackgroundTasks,
context: schemas.CurrentContext = Depends(OR_context)):
if isinstance(sessionId, str):
return {"errors": ["session not found"]}
data = sessions_replay.get_replay(project_id=projectId, session_id=sessionId, full_data=True,
include_fav_viewed=True, group_metadata=True, context=context)
if data is None:
return {"errors": ["session not found"]}
if data.get("inDB"):
background_tasks.add_task(sessions_viewed.view_session, project_id=projectId, user_id=context.user_id,
session_id=sessionId)
return {
'data': data
}
@app.get('/{projectId}/sessions/{sessionId}/events', tags=["sessions", "replay"],
dependencies=[OR_scope(Permissions.session_replay)])
async def get_session_events(projectId: int, sessionId: Union[int, str],
context: schemas.CurrentContext = Depends(OR_context)):
if isinstance(sessionId, str):
return {"errors": ["session not found"]}
data = sessions_replay.get_events(project_id=projectId, session_id=sessionId)
if data is None:
return {"errors": ["session not found"]}
return {
'data': data
}
@app.get('/{projectId}/sessions/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"],
dependencies=[OR_scope(Permissions.dev_tools)])
async def get_error_trace(projectId: int, sessionId: int, errorId: str,
@ -250,7 +286,7 @@ async def get_live_session(projectId: int, sessionId: str, background_tasks: Bac
context: schemas_ee.CurrentContext = Depends(OR_context)):
data = assist.get_live_session_by_id(project_id=projectId, session_id=sessionId)
if data is None:
data = sessions.get_by_id2_pg(context=context, project_id=projectId, session_id=sessionId,
data = sessions_replay.get_replay(context=context, project_id=projectId, session_id=sessionId,
full_data=True, include_fav_viewed=True, group_metadata=True, live=False)
if data is None:
return {"errors": ["session not found"]}

View file

@ -1,3 +0,0 @@
#!/bin/zsh
uvicorn app:app --reload

View file

@ -1,12 +1,12 @@
{
"name": "assist-server",
"version": "1.0.0",
"version": "v1.11.0-ee",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "assist-server",
"version": "1.0.0",
"version": "v1.11.0-ee",
"license": "Elastic License 2.0 (ELv2)",
"dependencies": {
"@maxmind/geoip2-node": "^3.5.0",
@ -38,9 +38,9 @@
}
},
"node_modules/@redis/client": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.5.tgz",
"integrity": "sha512-fuMnpDYSjT5JXR9rrCW1YWA4L8N/9/uS4ImT3ZEC/hcaQRI1D/9FvwjriRj1UvepIgzZXthFVKMNRzP/LNL7BQ==",
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.6.tgz",
"integrity": "sha512-dFD1S6je+A47Lj22jN/upVU2fj4huR7S9APd7/ziUXsIXDL+11GPYti4Suv5y8FuXaN+0ZG4JF+y1houEJ7ToA==",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@ -67,9 +67,9 @@
}
},
"node_modules/@redis/search": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.1.tgz",
"integrity": "sha512-pqCXTc5e7wJJgUuJiC3hBgfoFRoPxYzwn0BEfKgejTM7M/9zP3IpUcqcjgfp8hF+LoV8rHZzcNTz7V+pEIY7LQ==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz",
"integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
@ -117,9 +117,9 @@
}
},
"node_modules/@types/node": {
"version": "18.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
"version": "18.15.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.1.tgz",
"integrity": "sha512-U2TWca8AeHSmbpi314QBESRk7oPjSZjDsR+c+H4ECC1l+kFgpZf8Ydhv3SJpPy51VyZHHqxlb6mTTqYNNRVAIw=="
},
"node_modules/accepts": {
"version": "1.3.8",
@ -878,15 +878,15 @@
}
},
"node_modules/redis": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.4.tgz",
"integrity": "sha512-wi2tgDdQ+Q8q+PR5FLRx4QvDiWaA+PoJbrzsyFqlClN5R4LplHqN3scs/aGjE//mbz++W19SgxiEnQ27jnCRaA==",
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.5.tgz",
"integrity": "sha512-O0OWA36gDQbswOdUuAhRL6mTZpHFN525HlgZgDaVNgCJIAZR3ya06NTESb0R+TUZ+BFaDpz6NnnVvoMx9meUFg==",
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.5.5",
"@redis/client": "1.5.6",
"@redis/graph": "1.1.0",
"@redis/json": "1.0.4",
"@redis/search": "1.1.1",
"@redis/search": "1.1.2",
"@redis/time-series": "1.0.4"
}
},
@ -1085,9 +1085,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"funding": [
{
"type": "opencollective",

View file

@ -1,6 +1,6 @@
{
"name": "assist-server",
"version": "1.0.0",
"version": "v1.11.0-ee",
"description": "assist server to get live sessions & sourcemaps reader to get stack trace",
"main": "peerjs-server.js",
"scripts": {

View file

@ -1,2 +1,2 @@
#!/bin/bash
rsync -avr --exclude=".*" --exclude="node_modules" --ignore-existing ../../utilities/* ./
rsync -avr --exclude=".*" --exclude="node_modules" --ignore-existing ../../assist/* ./

View file

@ -1,6 +1,7 @@
const dumps = require('./utils/HeapSnapshot');
const {request_logger} = require('./utils/helper');
const express = require('express');
const health = require("./utils/health");
const assert = require('assert').strict;
let socket;
@ -14,7 +15,7 @@ const HOST = process.env.LISTEN_HOST || '0.0.0.0';
const PORT = process.env.LISTEN_PORT || 9001;
assert.ok(process.env.ASSIST_KEY, 'The "ASSIST_KEY" environment variable is required');
const P_KEY = process.env.ASSIST_KEY;
const PREFIX = process.env.PREFIX || process.env.prefix || `/assist`
const PREFIX = process.env.PREFIX || process.env.prefix || `/assist`;
let debug = process.env.debug === "1";
const heapdump = process.env.heapdump === "1";
@ -31,18 +32,11 @@ if (process.env.uws !== "true") {
);
heapdump && wsapp.use(`${PREFIX}/${P_KEY}/heapdump`, dumps.router);
wsapp.use(`${PREFIX}/${P_KEY}`, socket.wsRouter);
wsapp.get('/private/shutdown', (req, res) => {
console.log("Requested shutdown");
res.statusCode = 200;
res.end("ok!");
process.kill(1, "SIGTERM");
}
);
wsapp.enable('trust proxy');
const wsserver = wsapp.listen(PORT, HOST, () => {
console.log(`WS App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
health.healthApp.listen(health.PORT, HOST, health.listen_cb);
});
socket.start(wsserver);
@ -102,13 +96,6 @@ if (process.env.uws !== "true") {
uapp.post(`${PREFIX}/${P_KEY}/sockets-live/:projectKey`, uWrapper(socket.handlers.socketsLiveByProject));
uapp.get(`${PREFIX}/${P_KEY}/sockets-live/:projectKey/:sessionId`, uWrapper(socket.handlers.socketsLiveByProject));
uapp.get('/private/shutdown', (res, req) => {
console.log("Requested shutdown");
res.writeStatus('200 OK').end("ok!");
process.kill(1, "SIGTERM");
}
);
socket.start(uapp);
uapp.listen(HOST, PORT, (token) => {
@ -116,7 +103,7 @@ if (process.env.uws !== "true") {
console.warn("port already in use");
}
console.log(`WS App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
health.healthApp.listen(health.PORT, HOST, health.listen_cb);
});

View file

@ -34,7 +34,7 @@ const debug = process.env.debug === "1";
const createSocketIOServer = function (server, prefix) {
if (process.env.uws !== "true") {
io = _io(server, {
maxHttpBufferSize: (parseInt(process.env.maxHttpBufferSize) || 5) * 1e6,
maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
@ -43,7 +43,7 @@ const createSocketIOServer = function (server, prefix) {
});
} else {
io = new _io.Server({
maxHttpBufferSize: (parseInt(process.env.maxHttpBufferSize) || 5) * 1e6,
maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
@ -83,6 +83,22 @@ const respond = function (res, data) {
}
}
const countSessions = async function () {
let count = 0;
try {
let rooms = await io.of('/').adapter.allRooms();
for (let i of rooms) {
let {projectKey, sessionId} = extractPeerId(i);
if (projectKey !== undefined && sessionId !== undefined) {
count++;
}
}
} catch (e) {
console.error(e);
}
return count;
}
const socketsList = async function (req, res) {
debug && console.log("[WS]looking for all available sessions");
let filters = await extractPayloadFromRequest(req, res);
@ -417,6 +433,7 @@ module.exports = {
process.exit(2);
});
},
countSessions,
handlers: {
socketsList,
socketsListByProject,

View file

@ -29,7 +29,7 @@ const debug = process.env.debug === "1";
const createSocketIOServer = function (server, prefix) {
if (process.env.uws !== "true") {
io = _io(server, {
maxHttpBufferSize: (parseInt(process.env.maxHttpBufferSize) || 5) * 1e6,
maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
@ -38,7 +38,7 @@ const createSocketIOServer = function (server, prefix) {
});
} else {
io = new _io.Server({
maxHttpBufferSize: (parseInt(process.env.maxHttpBufferSize) || 5) * 1e6,
maxHttpBufferSize: (parseFloat(process.env.maxHttpBufferSize) || 5) * 1e6,
cors: {
origin: "*",
methods: ["GET", "POST", "PUT"]
@ -66,6 +66,23 @@ const respond = function (res, data) {
}
}
const countSessions = async function () {
let count = 0;
try {
const arr = Array.from(io.sockets.adapter.rooms);
const filtered = arr.filter(room => !room[1].has(room[0]));
for (let i of filtered) {
let {projectKey, sessionId} = extractPeerId(i[0]);
if (projectKey !== null && sessionId !== null) {
count++;
}
}
} catch (e) {
console.error(e);
}
return count;
}
const socketsList = async function (req, res) {
debug && console.log("[WS]looking for all available sessions");
let filters = await extractPayloadFromRequest(req, res);
@ -379,6 +396,7 @@ module.exports = {
socketConnexionTimeout(io);
},
countSessions,
handlers: {
socketsList,
socketsListByProject,

61
ee/assist/utils/health.js Normal file
View file

@ -0,0 +1,61 @@
const express = require('express');
let socket;
if (process.env.redis === "true") {
socket = require("../servers/websocket-cluster");
} else {
socket = require("../servers/websocket");
}
const HOST = process.env.LISTEN_HOST || '0.0.0.0';
const PORT = process.env.HEALTH_PORT || 8888;
const {request_logger} = require("./helper");
const debug = process.env.debug === "1";
const respond = function (res, data) {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({"data": data}));
}
const check_health = async function (req, res) {
debug && console.log("[WS]looking for all available sessions");
respond(res, {
"health": true,
"details": {
"version": process.env.npm_package_version,
"connectedSessions": await socket.countSessions(),
"uWebSocket": process.env.uws === "true",
"redis": process.env.redis === "true"
}
});
}
const healthApp = express();
healthApp.use(express.json());
healthApp.use(express.urlencoded({extended: true}));
healthApp.use(request_logger("[healthApp]"));
healthApp.get(['/'], (req, res) => {
res.statusCode = 200;
res.end("healthApp ok!");
}
);
healthApp.get('/health', check_health);
healthApp.get('/shutdown', (req, res) => {
console.log("Requested shutdown");
res.statusCode = 200;
res.end("ok!");
process.kill(1, "SIGTERM");
}
);
const listen_cb = async function () {
console.log(`Health App listening on http://${HOST}:${PORT}`);
console.log('Press Ctrl+C to quit.');
}
module.exports = {
healthApp,
PORT,
listen_cb
};

View file

@ -0,0 +1,8 @@
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.11.0-ee';
ALTER TABLE experimental.events
MODIFY COLUMN issue_type Nullable(Enum8('click_rage'=1,'dead_click'=2,'excessive_scrolling'=3,'bad_request'=4,'missing_resource'=5,'memory'=6,'cpu'=7,'slow_resource'=8,'slow_page_load'=9,'crash'=10,'ml_cpu'=11,'ml_memory'=12,'ml_dead_click'=13,'ml_click_rage'=14,'ml_mouse_thrashing'=15,'ml_excessive_scrolling'=16,'ml_slow_resources'=17,'custom'=18,'js_exception'=19,'mouse_thrashing'=20));
ALTER TABLE experimental.issues
MODIFY COLUMN type Enum8('click_rage'=1,'dead_click'=2,'excessive_scrolling'=3,'bad_request'=4,'missing_resource'=5,'memory'=6,'cpu'=7,'slow_resource'=8,'slow_page_load'=9,'crash'=10,'ml_cpu'=11,'ml_memory'=12,'ml_dead_click'=13,'ml_click_rage'=14,'ml_mouse_thrashing'=15,'ml_excessive_scrolling'=16,'ml_slow_resources'=17,'custom'=18,'js_exception'=19,'mouse_thrashing'=20);

View file

@ -1,3 +1,4 @@
CREATE OR REPLACE FUNCTION openreplay_version AS() -> 'v1.11.0-ee';
CREATE DATABASE IF NOT EXISTS experimental;
CREATE TABLE IF NOT EXISTS experimental.autocomplete
@ -78,7 +79,7 @@ CREATE TABLE IF NOT EXISTS experimental.events
success Nullable(UInt8),
request_body Nullable(String),
response_body Nullable(String),
issue_type Nullable(Enum8('click_rage'=1,'dead_click'=2,'excessive_scrolling'=3,'bad_request'=4,'missing_resource'=5,'memory'=6,'cpu'=7,'slow_resource'=8,'slow_page_load'=9,'crash'=10,'ml_cpu'=11,'ml_memory'=12,'ml_dead_click'=13,'ml_click_rage'=14,'ml_mouse_thrashing'=15,'ml_excessive_scrolling'=16,'ml_slow_resources'=17,'custom'=18,'js_exception'=19)),
issue_type Nullable(Enum8('click_rage'=1,'dead_click'=2,'excessive_scrolling'=3,'bad_request'=4,'missing_resource'=5,'memory'=6,'cpu'=7,'slow_resource'=8,'slow_page_load'=9,'crash'=10,'ml_cpu'=11,'ml_memory'=12,'ml_dead_click'=13,'ml_click_rage'=14,'ml_mouse_thrashing'=15,'ml_excessive_scrolling'=16,'ml_slow_resources'=17,'custom'=18,'js_exception'=19,'mouse_thrashing'=20)),
issue_id Nullable(String),
error_tags_keys Array(String),
error_tags_values Array(Nullable(String)),
@ -200,7 +201,7 @@ CREATE TABLE IF NOT EXISTS experimental.issues
(
project_id UInt16,
issue_id String,
type Enum8('click_rage'=1,'dead_click'=2,'excessive_scrolling'=3,'bad_request'=4,'missing_resource'=5,'memory'=6,'cpu'=7,'slow_resource'=8,'slow_page_load'=9,'crash'=10,'ml_cpu'=11,'ml_memory'=12,'ml_dead_click'=13,'ml_click_rage'=14,'ml_mouse_thrashing'=15,'ml_excessive_scrolling'=16,'ml_slow_resources'=17,'custom'=18,'js_exception'=19),
type Enum8('click_rage'=1,'dead_click'=2,'excessive_scrolling'=3,'bad_request'=4,'missing_resource'=5,'memory'=6,'cpu'=7,'slow_resource'=8,'slow_page_load'=9,'crash'=10,'ml_cpu'=11,'ml_memory'=12,'ml_dead_click'=13,'ml_click_rage'=14,'ml_mouse_thrashing'=15,'ml_excessive_scrolling'=16,'ml_slow_resources'=17,'custom'=18,'js_exception'=19,'mouse_thrashing'=20),
context_string String,
context_keys Array(String),
context_values Array(Nullable(String)),

View file

@ -0,0 +1,42 @@
DO
$$
DECLARE
previous_version CONSTANT text := 'v1.10.0-ee';
next_version CONSTANT text := 'v1.11.0-ee';
BEGIN
IF (SELECT openreplay_version()) = previous_version THEN
raise notice 'valid previous DB version';
ELSEIF (SELECT openreplay_version()) = next_version THEN
raise notice 'new version detected, nothing to do';
ELSE
RAISE EXCEPTION 'upgrade to % failed, invalid previous version, expected %, got %', next_version,previous_version,(SELECT openreplay_version());
END IF;
END ;
$$
LANGUAGE plpgsql;
BEGIN;
CREATE OR REPLACE FUNCTION openreplay_version()
RETURNS text AS
$$
SELECT 'v1.11.0-ee'
$$ LANGUAGE sql IMMUTABLE;
ALTER TABLE events.inputs
ADD COLUMN duration integer NULL,
ADD COLUMN hesitation integer NULL;
ALTER TABLE public.projects
ALTER COLUMN gdpr SET DEFAULT '{
"maskEmails": true,
"sampleRate": 33,
"maskNumbers": false,
"defaultInputMode": "obscured"
}'::jsonb;
ALTER TYPE issue_type ADD VALUE IF NOT EXISTS 'mouse_thrashing';
ALTER TABLE events.clicks
ADD COLUMN hesitation integer NULL;
COMMIT;

View file

@ -253,7 +253,7 @@ $$
"maskEmails": true,
"sampleRate": 33,
"maskNumbers": false,
"defaultInputMode": "plain"
"defaultInputMode": "obscured"
}'::jsonb,
first_recorded_session_at timestamp without time zone NULL DEFAULT NULL,
sessions_last_check_at timestamp without time zone NULL DEFAULT NULL,
@ -954,6 +954,7 @@ $$
url text DEFAULT '' NOT NULL,
path text,
selector text DEFAULT '' NOT NULL,
hesitation integer DEFAULT NULL,
PRIMARY KEY (session_id, message_id)
);
CREATE INDEX IF NOT EXISTS clicks_session_id_idx ON events.clicks (session_id);
@ -976,6 +977,8 @@ $$
timestamp bigint NOT NULL,
label text DEFAULT NULL,
value text DEFAULT NULL,
duration integer DEFAULT NULL,
hesitation integer DEFAULT NULL,
PRIMARY KEY (session_id, message_id)
);
CREATE INDEX IF NOT EXISTS inputs_session_id_idx ON events.inputs (session_id);

View file

@ -19,6 +19,7 @@ import UserMenu from './UserMenu';
import SettingsMenu from './SettingsMenu';
import DefaultMenuView from './DefaultMenuView';
import PreferencesView from './PreferencesView';
import HealthStatus from './HealthStatus'
const CLIENT_PATH = client(CLIENT_DEFAULT_TAB);
@ -78,6 +79,8 @@ const Header = (props) => {
</Tooltip>
</div>
<HealthStatus />
<div className={cn(styles.userDetails, 'group cursor-pointer')}>
<div className="flex items-center">
<div className="w-10 h-10 bg-tealx rounded-full flex items-center justify-center color-white">

View file

@ -0,0 +1,43 @@
import React from 'react';
import { Icon } from 'UI';
import cn from 'classnames'
function Footer({ isSetup }: { isSetup?: boolean }) {
return (
<div className={cn(
'flex w-full p-4 items-center justify-center',
'bg-gray-lightest gap-4',
!isSetup ? 'border-t border-figmaColors-divider' : ''
)}>
<a
href={'https://docs.openreplay.com/en/troubleshooting/'}
target="_blank"
rel="noreferrer noopener"
className={'flex items-center gap-2 hover:underline'}
>
<Icon name={'tools'} size={16} />
Troubleshooting guide
</a>
<a
href={'https://slack.openreplay.com/'}
target="_blank"
rel="noreferrer noopener"
className={'flex items-center gap-2 hover:underline'}
>
<Icon name={'slack'} size={16} />
Ask slack community
</a>
<a
href={'https://github.com/openreplay/openreplay/issues/new/choose'}
target="_blank"
rel="noreferrer noopener"
className={'flex items-center gap-2 hover:underline'}
>
<Icon name={'github'} size={16} />
Raise an issue
</a>
</div>
);
}
export default Footer;

View file

@ -0,0 +1,159 @@
import React from 'react';
// @ts-ignore
import slide from 'App/svg/cheers.svg';
import { Button } from 'UI';
import Footer from './Footer';
import { getHighest } from 'App/constants/zindex';
import Category from 'Components/Header/HealthStatus/ServiceCategory';
import SubserviceHealth from 'Components/Header/HealthStatus/SubserviceHealth/SubserviceHealth';
import { IServiceStats } from '../HealthStatus';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
function HealthModal({
getHealth,
isLoading,
healthResponse,
setShowModal,
setPassed,
}: {
getHealth: () => void;
isLoading: boolean;
healthResponse: { overallHealth: boolean; healthMap: Record<string, IServiceStats> };
setShowModal: (isOpen: boolean) => void;
setPassed?: () => void;
}) {
const [selectedService, setSelectedService] = React.useState('');
React.useEffect(() => {
if (!healthResponse?.overallHealth) {
if (healthResponse?.healthMap) {
setSelectedService(
Object.keys(healthResponse.healthMap).filter(
(s) => !healthResponse.healthMap[s].healthOk
)[0]
);
}
}
}, [healthResponse]);
const handleClose = () => {
setShowModal(false);
};
const isSetup = document.location.pathname.includes('/signup')
return (
<div
style={{
width: '100vw',
height: '100vh',
position: 'fixed',
background: 'rgba(0, 0, 0, 0.5)',
top: 0,
left: 0,
zIndex: getHighest(),
}}
onClick={handleClose}
>
<div
style={{
width: 640,
position: 'absolute',
top: '50%',
left: '50%',
height: isSetup ? '600px' : '535px',
transform: 'translate(-50%, -50%)',
}}
onClick={(e) => e.stopPropagation()}
className={'flex flex-col bg-white rounded border border-figmaColors-divider'}
>
<div
className={
'flex w-full justify-between items-center p-4 border-b border-figmaColors-divider'
}
>
<div className={'text-xl font-semibold'}>Installation Status</div>
<Button
loading={isLoading}
onClick={getHealth}
icon={'arrow-repeat'}
variant={'text-primary'}
>
Recheck
</Button>
</div>
<div className={'flex w-full'}>
<div className={'flex flex-col h-full'} style={{ flex: 1 }}>
{isLoading ? (
<Category onClick={() => null} name={"Loading health status"} isLoading />
)
: Object.keys(healthResponse.healthMap).map((service) => (
<React.Fragment key={service}>
<Category
onClick={() => setSelectedService(service)}
healthOk={healthResponse.healthMap[service].healthOk}
name={healthResponse.healthMap[service].name}
isSelectable
isSelected={selectedService === service}
/>
</React.Fragment>
))}
</div>
<div
className={
'bg-gray-lightest border-l w-fit border-figmaColors-divider overflow-y-scroll relative'
}
style={{ flex: 2, height: 420 }}
>
{isLoading ? (
<div
style={{
position: 'absolute',
top: 'calc(50% - 28px)',
left: 'calc(50% - 28px)',
}}
>
<AnimatedSVG name={ICONS.LOADER} size={56} />
</div>
) : selectedService ? (
<ServiceStatus service={healthResponse.healthMap[selectedService]} />
) : <img src={slide} width={392} />
}
</div>
</div>
{isSetup ? (
<div className={'p-4 mt-auto w-full border-t border-figmaColors-divider'}>
<Button
disabled={!healthResponse?.overallHealth}
loading={isLoading}
variant={'primary'}
className={'ml-auto'}
onClick={() => setPassed?.()}
>
Create Account
</Button>
</div>
) : null}
<Footer isSetup={isSetup} />
</div>
</div>
);
}
function ServiceStatus({ service }: { service: Record<string, any> }) {
const { subservices } = service;
return (
<div className={'p-2'}>
<div className={'border border-light-gray'}>
{Object.keys(subservices).map((subservice: string) => (
<React.Fragment key={subservice}>
<SubserviceHealth name={subservice} subservice={subservices[subservice]} />
</React.Fragment>
))}
</div>
</div>
);
}
export default HealthModal;

View file

@ -0,0 +1,92 @@
import React from 'react';
import { Icon } from 'UI';
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
import { lastAskedKey, healthResponseKey } from './const';
import HealthWidget from "Components/Header/HealthStatus/HealthWidget";
import { getHealthRequest } from './getHealth'
export interface IServiceStats {
name: 'backendServices' | 'databases' | 'ingestionPipeline' | 'ssl';
serviceName: string;
healthOk: boolean;
subservices: {
health: boolean;
details?: {
errors?: string[];
version?: string;
}
}[]
}
function HealthStatus() {
const healthResponseSaved = localStorage.getItem(healthResponseKey) || '{}';
const [healthResponse, setHealthResponse] = React.useState(JSON.parse(healthResponseSaved));
const [isError, setIsError] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const lastAskedSaved = localStorage.getItem(lastAskedKey);
const [lastAsked, setLastAsked] = React.useState(lastAskedSaved);
const [showModal, setShowModal] = React.useState(false);
const getHealth = async () => {
if (isLoading) return;
try {
setIsLoading(true);
const { healthMap, asked } = await getHealthRequest();
setHealthResponse(healthMap);
setLastAsked(asked.toString());
} catch (e) {
console.error(e);
setIsError(true);
} finally {
setIsLoading(false);
}
};
React.useEffect(() => {
const now = new Date();
const lastAskedDate = lastAsked ? new Date(parseInt(lastAsked, 10)) : null;
const diff = lastAskedDate ? now.getTime() - lastAskedDate.getTime() : 0;
const diffInMinutes = Math.round(diff / 1000 / 60);
if (Object.keys(healthResponse).length === 0 || !lastAskedDate || diffInMinutes > 10) {
void getHealth();
}
}, []);
const icon = !isError && healthResponse?.overallHealth ? 'pulse' : ('exclamation-circle-fill' as const);
return (
<>
<div className={'relative group h-full hover:bg-figmaColors-secondary-outlined-hover-background'}>
<div
className={
'rounded cursor-pointer p-2 flex items-center'
}
>
<div className={'rounded p-2 border border-light-gray bg-white flex items-center '}>
<Icon name={icon} size={18} />
</div>
</div>
<HealthWidget
healthResponse={healthResponse}
getHealth={getHealth}
isLoading={isLoading}
lastAsked={lastAsked}
setShowModal={setShowModal}
isError={isError}
/>
</div>
{showModal ? (
<HealthModal
setShowModal={setShowModal}
healthResponse={healthResponse}
getHealth={getHealth}
isLoading={isLoading}
/>
) : null}
</>
);
}
export default HealthStatus;

View file

@ -0,0 +1,98 @@
import React from 'react'
import { Icon } from "UI";
import ServiceCategory from "Components/Header/HealthStatus/ServiceCategory";
import cn from 'classnames'
import { IServiceStats } from './HealthStatus'
function HealthWidget({
healthResponse,
getHealth,
isLoading,
lastAsked,
setShowModal,
isError,
}: {
healthResponse: { overallHealth: boolean; healthMap: Record<string, IServiceStats> };
getHealth: Function;
isLoading: boolean;
lastAsked: string | null;
setShowModal: (visible: boolean) => void;
isError?: boolean;
}) {
const [lastAskedDiff, setLastAskedDiff] = React.useState(0);
const healthOk = healthResponse?.overallHealth;
React.useEffect(() => {
const now = new Date();
const lastAskedDate = lastAsked ? new Date(parseInt(lastAsked, 10)) : null;
const diff = lastAskedDate ? now.getTime() - lastAskedDate.getTime() : 0;
const diffInMinutes = Math.round(diff / 1000 / 60);
setLastAskedDiff(diffInMinutes);
}, [lastAsked]);
const title = !isError && healthOk ? 'All Systems Operational' : 'Service disruption';
const icon = !isError && healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const);
const problematicServices = Object.values(healthResponse?.healthMap || {}).filter(
(service: Record<string, any>) => !service.healthOk
)
return (
<div
style={{ width: 220, top: '100%', right: '-30%', height: '110%' }}
className={'absolute group invisible group-hover:visible pt-4'}
>
<div
className={
'w-full flex flex-col border border-light-gray gap-2 rounded items-center p-4 bg-white'
}
>
<div
className={cn(
'p-2 gap-2 w-full font-semibold flex items-center rounded',
healthOk
? 'color-green bg-figmaColors-secondary-outlined-hover-background'
: 'bg-red-lightest'
)}
>
<Icon name={icon} size={16} color={'green'} />
<span>{title}</span>
</div>
<div className={'text-secondary flex w-full justify-between items-center text-sm'}>
<span>Last checked {lastAskedDiff} mins. ago </span>
<div
className={cn('cursor-pointer', isLoading ? 'animate-spin' : '')}
onClick={() => getHealth()}
>
<Icon name={'arrow-repeat'} size={16} color={'main'} />
</div>
</div>
{isError && <div className={'text-secondary text-sm'}>Error getting service health status</div>}
<div className={'divider w-full border border-b-light-gray'} />
<div className={'w-full'}>
{!isError && !healthOk ? (
<>
<div className={'text-secondary pb-2'}>
Observed installation Issue with the following
</div>
{problematicServices.map((service) => (
<React.Fragment key={service.serviceName}>
<ServiceCategory
onClick={() => setShowModal(true)}
healthOk={false}
name={service.name}
isSelectable
/>
</React.Fragment>
))}
</>
) : null}
</div>
</div>
</div>
);
}
export default HealthWidget

View file

@ -0,0 +1,49 @@
import { Icon } from 'UI';
import React from 'react';
import cn from 'classnames';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
function Category({
name,
healthOk,
onClick,
isSelectable,
isExpandable,
isExpanded,
isSelected,
isLoading,
}: {
name: string;
healthOk?: boolean;
isLoading?: boolean;
onClick: (args: any) => void;
isSelectable?: boolean;
isExpandable?: boolean;
isExpanded?: boolean;
isSelected?: boolean;
}) {
const icon = healthOk ? ('check-circle-fill' as const) : ('exclamation-circle-fill' as const);
return (
<div
className={cn(
'px-4 py-2 flex items-center gap-2 border-b cursor-pointer',
isExpandable || isSelectable ? 'hover:bg-active-blue' : '',
isSelected ? 'bg-active-blue' : ''
)}
onClick={onClick}
>
{isLoading ? (
<AnimatedSVG name={ICONS.LOADER} size={20} />
) : <Icon name={icon} size={20} color={'green'} />}
{name}
{isSelectable ? <Icon name={'chevron-right'} size={16} className={'ml-auto'} /> : null}
{isExpandable ? (
<Icon name={isExpanded ? 'chevron-up' : 'chevron-down'} size={16} className={'ml-auto'} />
) : null}
</div>
);
}
export default Category

View file

@ -0,0 +1,48 @@
import React from 'react';
import Category from 'Components/Header/HealthStatus/ServiceCategory';
import cn from 'classnames';
function SubserviceHealth({
subservice,
name,
}: {
name: string;
subservice: { health: boolean; details: { errors?: string[]; version?: string } };
}) {
const [isExpanded, setIsExpanded] = React.useState(!subservice?.health);
const isExpandable = subservice?.details && Object.keys(subservice?.details).length > 0;
return (
<div className={cn(isExpanded && isExpandable ? 'bg-active-blue' : 'bg-white')}>
<Category
onClick={() => (isExpandable ? setIsExpanded(!isExpanded) : null)}
name={name}
healthOk={subservice?.health}
isExpandable={isExpandable}
isExpanded={isExpanded}
/>
{isExpanded ? (
<div className={'p-3'}>
{subservice?.details?.version ? (
<div className="flex items-center justify-between mt-2 px-2">
<div className="py-1 px-2 font-medium">Version</div>
<div className="code-font text-black rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
{subservice?.details?.version}
</div>
</div>
) : null}
{subservice?.details?.errors?.length ? (
<div className={'py-2 px-4 bg-white rounded-xl border border-light-gray'}>
<div>Error log:</div>
{subservice.details.errors.toString()}
</div>
) : subservice?.health ? null : (
'Service not responding'
)}
</div>
) : null}
</div>
);
}
export default SubserviceHealth;

View file

@ -0,0 +1,9 @@
export const categoryKeyNames = {
backendServices: 'Backend Services',
databases: 'Databases',
ingestionPipeline: 'Ingestion Pipeline',
ssl: 'SSL',
} as const
export const lastAskedKey = '__openreplay_health_status';
export const healthResponseKey = '__openreplay_health_response';

View file

@ -0,0 +1,36 @@
import { healthService } from 'App/services';
import { categoryKeyNames, lastAskedKey, healthResponseKey } from "Components/Header/HealthStatus/const";
import { IServiceStats } from "Components/Header/HealthStatus/HealthStatus";
function mapResponse(resp: Record<string, any>) {
const services = Object.keys(resp);
const healthMap: Record<string, IServiceStats> = {};
services.forEach((service) => {
healthMap[service] = {
// @ts-ignore
name: categoryKeyNames[service],
healthOk: true,
subservices: resp[service],
serviceName: service,
};
Object.values(healthMap[service].subservices).forEach((subservice: Record<string, any>) => {
if (!subservice?.health) healthMap[service].healthOk = false;
});
});
const overallHealth = Object.values(healthMap).every(
(service: Record<string, any>) => service.healthOk
);
return { overallHealth, healthMap };
}
export async function getHealthRequest() {
const r = await healthService.fetchStatus();
const healthMap = mapResponse(r);
const asked = new Date().getTime();
localStorage.setItem(healthResponseKey, JSON.stringify(healthMap));
localStorage.setItem(lastAskedKey, asked.toString());
return { healthMap, asked }
}

View file

@ -0,0 +1 @@
export { default } from './HealthStatus'

View file

@ -4,12 +4,18 @@ import NoSessionsMessage from 'Shared/NoSessionsMessage';
import MainSearchBar from 'Shared/MainSearchBar';
import SessionSearch from 'Shared/SessionSearch';
import SessionListContainer from 'Shared/SessionListContainer/SessionListContainer';
import cn from 'classnames';
import OverviewMenu from 'Shared/OverviewMenu';
function Overview() {
return (
<div className="page-margin container-90 flex relative">
<div className="flex-1 flex">
<div className={'w-full mx-auto'} style={{ maxWidth: '1300px' }}>
<div className="page-margin container-90">
<div className={cn('side-menu')}>
<OverviewMenu />
</div>
<div
className={cn("side-menu-margined")}
>
<NoSessionsMessage />
<div className="mb-5">
@ -21,7 +27,6 @@ function Overview() {
</div>
</div>
</div>
</div>
);
}

View file

@ -24,11 +24,9 @@ export default function MetaInfo({
{Object.keys(envObject).map((envTag) => (
<div key={envTag} className="flex items-center">
<div className="py-1 px-2 font-medium">{envTag}</div>
<div className="rounded bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
<span className="text-base">
<div className="rounded text-base bg-active-blue px-2 py-1 whitespace-nowrap overflow-hidden text-clip">
{/* @ts-ignore */}
{envObject[envTag]}
</span>
</div>
</div>
))}

View file

@ -6,6 +6,8 @@ import stl from './signup.module.css';
import cn from 'classnames';
import SignupForm from './SignupForm';
import RegisterBg from '../../svg/register.svg';
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
import { getHealthRequest } from 'Components/Header/HealthStatus/getHealth';
const BulletItem = ({ text }) => (
<div className="flex items-center mb-4">
@ -15,9 +17,45 @@ const BulletItem = ({ text }) => (
<div>{text}</div>
</div>
);
const healthStatusCheck_key = '__or__healthStatusCheck_key'
@withPageTitle('Signup - OpenReplay')
export default class Signup extends React.Component {
state = {
healthModalPassed: localStorage.getItem(healthStatusCheck_key === 'true'),
healthStatusLoading: true,
healthStatus: null,
}
getHealth = async () => {
this.setState({ healthStatusLoading: true });
const { healthMap } = await getHealthRequest();
this.setState({ healthStatus: healthMap, healthStatusLoading: false });
}
componentDidMount() {
if (!this.state.healthModalPassed) void this.getHealth();
}
setHealthModalPassed = () => {
localStorage.setItem(healthStatusCheck_key, 'true');
this.setState({ healthModalPassed: true });
}
render() {
if (!this.state.healthModalPassed) {
return (
<HealthModal
setShowModal={() => null}
healthResponse={this.state.healthStatus}
getHealth={this.getHealth}
isLoading={this.state.healthStatusLoading}
setPassed={this.setHealthModalPassed}
/>
)
}
return (
<div className="flex justify-center items-center gap-6" style={{ height: '100vh' }}>
<div className={cn('relative overflow-hidden')}>

View file

@ -0,0 +1,52 @@
import React from 'react';
import { SideMenuitem } from 'UI';
import { connect } from 'react-redux';
import { setActiveTab } from 'Duck/search';
interface Props {
setActiveTab: (tab: any) => void;
activeTab: string;
isEnterprise: boolean;
}
function OverviewMenu(props: Props) {
const { activeTab, isEnterprise } = props;
return (
<div>
<div className="w-full">
<SideMenuitem
active={activeTab === 'all'}
id="menu-manage-alerts"
title="Sessions"
iconName="play-circle-bold"
onClick={() => props.setActiveTab({ type: 'all' })}
/>
</div>
<div className="w-full my-2" />
<div className="w-full">
<SideMenuitem
active={activeTab === 'bookmark'}
id="menu-manage-alerts"
title={`${isEnterprise ? 'Vault' : 'Bookmarks'}`}
iconName={ isEnterprise ? "safe" : "star" }
onClick={() => props.setActiveTab({ type: 'bookmark' })}
/>
</div>
<div className="w-full my-2" />
<div className="w-full">
<SideMenuitem
active={activeTab === 'notes'}
id="menu-manage-alerts"
title="Notes"
iconName="stickies"
onClick={() => props.setActiveTab({ type: 'notes' })}
/>
</div>
</div>
);
}
export default connect((state: any) => ({
activeTab: state.getIn(['search', 'activeTab', 'type']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}), { setActiveTab })(OverviewMenu);

View file

@ -0,0 +1 @@
export { default } from './OverviewMenu';

View file

@ -47,40 +47,24 @@ function SessionHeader(props: Props) {
};
return (
<div className="flex items-center px-4 justify-between">
<div className="flex items-center justify-between">
<div className="mr-3 text-base flex items-center gap-4">
<Tab onClick={() => props.setActiveTab({ type: 'all' })} addBorder={activeTab === 'all'}>
<span className="font-bold">SESSIONS</span>
</Tab>
<Tab
onClick={() => props.setActiveTab({ type: 'bookmark' })}
addBorder={activeTab === 'bookmark'}
>
<span className="font-bold">{`${isEnterprise ? 'VAULT' : 'BOOKMARKS'}`}</span>
</Tab>
<Tab
addBorder={activeTab === 'notes'}
onClick={() => props.setActiveTab({ type: 'notes' })}
>
<span className="font-bold">NOTES</span>
</Tab>
</div>
</div>
{activeTab !== 'notes' && activeTab !== 'bookmark' ? (
<div className="flex items-center">
<div className="flex items-center px-4 py-1 justify-between w-full">
{activeTab !== 'notes' ? (
<div className="flex items-center w-full justify-end">
{activeTab !== 'bookmark' && (
<>
<SessionTags />
<div className="mx-4" />
<div className="mr-auto" />
<SelectDateRange period={period} onChange={onDateChange} right={true} />
<div className="mx-2" />
</>
)}
<SessionSort />
<SessionSettingButton />
</div>
) : null}
{activeTab === 'notes' && (
<div className="flex items-center">
<div className="flex items-center justify-end w-full">
<NoteTags />
</div>
)}

View file

@ -78,6 +78,7 @@ const SVG = (props: Props) => {
case 'bell-slash': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M5.164 14H15c-.299-.199-.557-.553-.78-1-.9-1.8-1.22-5.12-1.22-6 0-.264-.02-.523-.06-.776l-.938.938c.02.708.157 2.154.457 3.58.161.767.377 1.566.663 2.258H6.164l-1 1zm5.581-9.91a3.986 3.986 0 0 0-1.948-1.01L8 2.917l-.797.161A4.002 4.002 0 0 0 4 7c0 .628-.134 2.197-.459 3.742-.05.238-.105.479-.166.718l-1.653 1.653c.02-.037.04-.074.059-.113C2.679 11.2 3 7.88 3 7c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0c.942.19 1.788.645 2.457 1.284l-.707.707zM10 15a2 2 0 1 1-4 0h4zm-9.375.625a.53.53 0 0 0 .75.75l14.75-14.75a.53.53 0 0 0-.75-.75L.625 15.625z"/></svg>;
case 'bell': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zM8 1.918l-.797.161A4.002 4.002 0 0 0 4 6c0 .628-.134 2.197-.459 3.742-.16.767-.376 1.566-.663 2.258h10.244c-.287-.692-.502-1.49-.663-2.258C12.134 8.197 12 6.628 12 6a4.002 4.002 0 0 0-3.203-3.92L8 1.917zM14.22 12c.223.447.481.801.78 1H1c.299-.199.557-.553.78-1C2.68 10.2 3 6.88 3 6c0-2.42 1.72-4.44 4.005-4.901a1 1 0 1 1 1.99 0A5.002 5.002 0 0 1 13 6c0 .88.32 4.2 1.22 6z"/></svg>;
case 'binoculars': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M3 2.5A1.5 1.5 0 0 1 4.5 1h1A1.5 1.5 0 0 1 7 2.5V5h2V2.5A1.5 1.5 0 0 1 10.5 1h1A1.5 1.5 0 0 1 13 2.5v2.382a.5.5 0 0 0 .276.447l.895.447A1.5 1.5 0 0 1 15 7.118V14.5a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 14.5v-3a.5.5 0 0 1 .146-.354l.854-.853V9.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v.793l.854.853A.5.5 0 0 1 7 11.5v3A1.5 1.5 0 0 1 5.5 16h-3A1.5 1.5 0 0 1 1 14.5V7.118a1.5 1.5 0 0 1 .83-1.342l.894-.447A.5.5 0 0 0 3 4.882V2.5zM4.5 2a.5.5 0 0 0-.5.5V3h2v-.5a.5.5 0 0 0-.5-.5h-1zM6 4H4v.882a1.5 1.5 0 0 1-.83 1.342l-.894.447A.5.5 0 0 0 2 7.118V13h4v-1.293l-.854-.853A.5.5 0 0 1 5 10.5v-1A1.5 1.5 0 0 1 6.5 8h3A1.5 1.5 0 0 1 11 9.5v1a.5.5 0 0 1-.146.354l-.854.853V13h4V7.118a.5.5 0 0 0-.276-.447l-.895-.447A1.5 1.5 0 0 1 12 4.882V4h-2v1.5a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V4zm4-1h2v-.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5V3zm4 11h-4v.5a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5V14zm-8 0H2v.5a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5V14z"/></svg>;
case 'book-doc': return <svg viewBox="0 0 17 17" width={ `${ width }px` } height={ `${ height }px` } ><g clipPath="url(#a)"><path d="M1.5 3.328c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V3.328ZM9 3.187c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V3.187Zm-.5-.904c-.985-.847-2.413-.973-3.713-.843-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 .5 3v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16.5 14V3a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105-1.3-.131-2.728-.004-3.713.843Z"/></g><defs><clipPath id="a"><path fill="#fff" transform="translate(.5 .5)" d="M0 0h16v16H0z"/></clipPath></defs></svg>;
case 'book': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M5 0h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2 2 2 0 0 1-2 2H3a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1H1a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v9a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1H3a2 2 0 0 1 2-2z"/><path d="M1 6v-.5a.5.5 0 0 1 1 0V6h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V9h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 2.5v.5H.5a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1H2v-.5a.5.5 0 0 0-1 0z"/></svg>;
case 'browser/browser': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855.173-.324.33-.682.468-1.068H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"/></svg>;
case 'browser/chrome': return <svg viewBox="0 0 517 517" width={ `${ width }px` } height={ `${ height }px` } ><path d="M148.978 223c8.725-25.136 24.616-44.93 47.674-59.383 23.058-14.453 48.297-21.05 75.718-19.794L464 154.191c-20.565-41.474-50.79-73.835-90.674-97.086C337.181 36.368 298.232 26 256.478 26c-34.275 0-67.304 7.54-99.087 22.622C125.61 63.703 98.811 85.068 77 112.718L148.978 223Zm61.483-37.353c-18.07 11.327-30.15 26.373-36.92 45.879l-17.375 50.057-111.15-170.3 11.571-14.668C77 67 110.845 41.93 146.245 25.132 181.488 8.41 218.321 0 256.478 0c46.292 0 89.702 11.556 129.787 34.553l.155.09c44.282 25.814 78.05 61.971 100.874 107.998l19.794 39.92-236.01-12.77c-22.108-.994-42.08 4.237-60.617 15.856ZM179 258.5c0 21.806 7.583 40.34 22.75 55.604C216.917 329.368 235.333 337 257 337s40.083-7.632 55.25-22.896C327.417 298.84 335 280.306 335 258.5c0-21.806-7.583-40.34-22.75-55.604C297.083 187.632 278.667 180 257 180s-40.083 7.632-55.25 22.896C186.583 218.16 179 236.694 179 258.5Zm-26 0c0-28.647 10.282-53.777 30.307-73.93C203.363 164.384 228.422 154 257 154c28.578 0 53.637 10.384 73.693 30.57C350.718 204.723 361 229.853 361 258.5c0 28.647-10.282 53.777-30.307 73.93C310.637 352.616 285.578 363 257 363c-28.578 0-53.637-10.384-73.693-30.57C163.282 312.277 153 287.147 153 258.5ZM474.853 175l-129.839 6.535c17.437 20.54 26.466 44.348 27.089 71.424.623 27.075-6.539 52.128-21.484 75.157L246 489.636c46.082 2.49 89.05-7.78 128.905-30.81 33.005-19.296 59.47-44.504 79.398-75.625 19.928-31.121 31.76-65.043 35.496-101.766 3.736-36.724-1.245-72.201-14.946-106.435Zm24.14-11.66c15.26 38.132 20.834 79.828 16.672 120.726-4.149 40.78-17.341 78.602-39.466 113.155-22.164 34.616-51.648 62.698-88.172 84.05l-.113.067c-44.226 25.555-92.201 37.021-143.317 34.26-10.485-.58-18.184-1.113-23.097-1.598-5.048-.498-12.29-1.44-21.725-2.824l24.403-35.675 104.63-161.538c12.111-18.661 17.804-38.576 17.302-60.406-.486-21.128-7.342-39.204-20.917-55.195l-34.08-40.146 200.98-10.117 6.9 15.24ZM151.164 304.41 63.363 132C38.454 170.73 26 213.207 26 259.433c0 38.105 8.562 73.555 25.687 106.35 17.124 32.796 40.632 60.125 70.522 81.989 29.89 21.863 63.205 35.606 99.945 41.228L281 371.874c-26.154 4.997-51.218 1.093-75.192-11.713-23.975-12.805-42.189-31.39-54.643-55.752Zm66.892 32.818c18.76 10.02 37.782 12.983 58.063 9.108l51.778-9.894L239.5 516.5l-21.279-1.8c-40.865-6.253-78.097-21.61-111.362-45.943-33.108-24.217-59.249-54.608-78.22-90.94C9.56 341.28 0 301.698 0 259.434c0-51.208 13.886-98.568 41.495-141.497L70.5 79.5l103.824 213.092c10.042 19.637 24.454 34.338 43.733 44.636Z"/></svg>;
@ -172,6 +173,7 @@ const SVG = (props: Props) => {
case 'event/mouse_thrashing': return <svg viewBox="0 0 32 32" width={ `${ width }px` } height={ `${ height }px` } ><path fill="transparent" d="M0 0h32v32H0z"/><g opacity=".5"><path fill="transparent" d="M15.225-3.633 37.32 6.27l-9.903 22.096L5.32 18.463z"/><path d="m18.722 2.844 10.6 10.955a1.045 1.045 0 0 1-.706 1.77l-4.363.184-.512.021.182.48 2.151 5.648a1.046 1.046 0 0 1-1.954.746L21.97 17l-.182-.48-.398.325-3.379 2.765a1.046 1.046 0 0 1-1.707-.851l.622-15.231a1.046 1.046 0 0 1 1.796-.685Z" stroke="#000" stroke-width=".74"/></g><path clipRule="evenodd" d="M8.25 5.522A1.913 1.913 0 0 0 5.761 8.01l7.653 19.132a1.913 1.913 0 0 0 3.487.143l2.64-5.278 5.774 5.777a1.913 1.913 0 1 0 2.705-2.707L22.245 19.3l5.28-2.639a1.914 1.914 0 0 0-.145-3.485L8.25 5.522Z"/><path d="M3.838 12.892a1.153 1.153 0 0 0-1.126 1.796l9.578 13.815a1.153 1.153 0 0 0 2.068-.386l1.135-4.68.134-.55.48.301 5.645 3.54L3.838 12.892Zm0 0 16.608 2.592-16.608-2.592Zm19.138 11.88-5.648-3.539-.48-.3.438-.36 3.716-3.061a1.152 1.152 0 0 0-.555-2.028l2.529 9.289Zm0 0a1.153 1.153 0 0 1-1.224 1.956l1.224-1.955Z" stroke="#000" stroke-width=".816" opacity=".3"/></svg>;
case 'event/resize': return <svg viewBox="0 0 30 30" width={ `${ width }px` } height={ `${ height }px` } ><path d="M1.746 15.029a.544.544 0 0 0 0 .837l7.752 6.342a.517.517 0 0 0 .327.117.53.53 0 0 0 .524-.536V9.105c0-.122-.04-.24-.115-.335a.516.516 0 0 0-.736-.083l-7.752 6.342ZM8.844 7.85a1.548 1.548 0 0 1 2.208.251c.223.285.345.64.345 1.004V21.79c0 .888-.704 1.608-1.572 1.608a1.55 1.55 0 0 1-.981-.353l-7.752-6.342a1.632 1.632 0 0 1 0-2.51L8.844 7.85ZM28.234 15.782a.544.544 0 0 0-.082-.753l-7.751-6.342a.517.517 0 0 0-.327-.118.53.53 0 0 0-.524.536v12.684c0 .122.04.24.115.335.18.231.51.269.736.084l7.751-6.342a.53.53 0 0 0 .082-.084Zm-7.179 7.262a1.548 1.548 0 0 1-2.209-.25 1.63 1.63 0 0 1-.344-1.005V9.105c0-.888.704-1.607 1.572-1.607.356 0 .703.124.981.352l7.752 6.342a1.632 1.632 0 0 1 0 2.51l-7.752 6.342ZM14.414 4h1.071v22h-1.071z"/></svg>;
case 'event/view': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/><path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/></svg>;
case 'exclamation-circle-fill': return <svg viewBox="0 0 18 18" width={ `${ width }px` } height={ `${ height }px` } ><g clipPath="url(#a)"><path d="M17.48 9A8.48 8.48 0 1 1 .52 9 8.48 8.48 0 0 1 17.48 9ZM9 4.76a.96.96 0 0 0-.954 1.055l.371 3.717a.585.585 0 0 0 1.166 0l.371-3.717A.96.96 0 0 0 9 4.76Zm.002 6.36a1.06 1.06 0 1 0 0 2.12 1.06 1.06 0 0 0 0-2.12Z" fill="#C00"/></g><defs><clipPath id="a"><path fill="#fff" transform="translate(.52 .52)" d="M0 0h16.959v16.959H0z"/></clipPath></defs></svg>;
case 'exclamation-circle': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/></svg>;
case 'expand-wide': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h13A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5v-9zM1.5 3a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-13z"/><path d="M2 4.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H3v2.5a.5.5 0 0 1-1 0v-3zm12 7a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1 0-1H13V8.5a.5.5 0 0 1 1 0v3z"/></svg>;
case 'explosion': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M0 10.5a.5.5 0 0 1 .5-.5h15a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zM12 0H4a2 2 0 0 0-2 2v7h1V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v7h1V2a2 2 0 0 0-2-2zm2 12h-1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-2H2v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2z"/></svg>;
@ -371,6 +373,7 @@ const SVG = (props: Props) => {
case 'plus': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/></svg>;
case 'pointer-sessions-search': return <svg viewBox="0 0 157 200" width={ `${ width }px` } height={ `${ height }px` } ><path d="M156.877 1.647a1 1 0 0 0-1.415-.96l-3.852 1.76a1 1 0 0 0-.072 1.782l3.662 2.047a1 1 0 0 0 1.487-.823l.19-3.806ZM.372 100.542c7.18-.574 15.4-.41 23.76-.369 8.333.041 16.793-.039 24.361-1.12 7.564-1.08 14.325-3.171 19.195-7.208 4.894-4.057 7.792-10.005 7.792-18.627h-1.395c0 8.364-2.8 13.96-7.354 17.737-4.579 3.795-11.02 5.827-18.462 6.89-7.439 1.062-15.791 1.146-24.13 1.105-8.314-.042-16.5 1.011-23.767 1.592Zm67.68-14.233c2.937 2.554 6.59 3.334 10.667 2.6 4.036-.726 8.484-2.932 13.149-6.323 9.338-6.788 19.76-18.48 29.779-33.325 0 0-.039-.38-.508-.624-9.983 14.792-21.001 26.348-30.16 33.006-4.585 3.332-8.825 6.174-12.54 6.843-3.675.661-6.838-.04-9.406-2.271l-.982.094Zm53.595-37.048c25.462-37.726 24.623-33.749 32.521-44.276l-.472-.673c-7.82 10.423-7.082 6.58-32.557 44.325.469.244.508.624.508.624ZM75.48 73.218c0-.428-.047-.805-.156-1.121-.107-.314-.291-.612-.601-.816-.675-.443-1.469-.18-2.005.116-1.135.63-2.463 2.104-3.575 3.823-1.127 1.742-2.103 3.845-2.466 5.824-.358 1.955-.142 3.947 1.374 5.265l.982-.094c-1.034-.899-1.312-3.16-.979-4.977.33-1.795.539-3.76 1.61-5.414 1.086-1.68 2.966-2.93 3.799-3.392.447-.248.5-.121.425-.17-.01-.007.045.025.1.188.056.161.097.409.097.768h1.395Z" fill="#000"/></svg>;
case 'prev1': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/></svg>;
case 'pulse': return <svg viewBox="0 0 26 20" width={ `${ width }px` } height={ `${ height }px` } ><path clipRule="evenodd" d="M1.117.398a.813.813 0 0 0-1.23.388L-3.17 9.187h-5.118a.812.812 0 1 0 0 1.626H-2.6a.813.813 0 0 0 .764-.535L.65 3.44l5.736 15.773a.813.813 0 0 0 1.528 0l3.055-8.401H18.2a.812.812 0 0 0 .764-.535L21.45 3.44l5.736 15.773a.812.812 0 0 0 1.528 0l3.055-8.401h5.119c.055 0 .109-.006.162-.017.053.01.108.017.163.017H42.9a.812.812 0 0 0 .764-.535L46.15 3.44l5.736 15.773a.813.813 0 0 0 1.528 0l3.055-8.401h5.118a.812.812 0 1 0 0-1.626H55.9a.812.812 0 0 0-.764.534l-2.486 6.837L46.914.786a.813.813 0 0 0-1.528 0l-3.055 8.401h-5.119a.813.813 0 0 0-.162.017.813.813 0 0 0-.162-.017H31.2a.812.812 0 0 0-.764.534l-2.486 6.837L22.214.786a.813.813 0 0 0-1.528 0l-3.055 8.401H10.4a.812.812 0 0 0-.764.534L7.15 16.557 1.414.786a.813.813 0 0 0-.297-.388Z" fill="#42AE5E"/></svg>;
case 'puzzle-piece': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M6.75 1a.75.75 0 0 1 .75.75V8a.5.5 0 0 0 1 0V5.467l.086-.004c.317-.012.637-.008.816.027.134.027.294.096.448.182.077.042.15.147.15.314V8a.5.5 0 1 0 1 0V6.435a4.9 4.9 0 0 1 .106-.01c.316-.024.584-.01.708.04.118.046.3.207.486.43.081.096.15.19.2.259V8.5a.5.5 0 0 0 1 0v-1h.342a1 1 0 0 1 .995 1.1l-.271 2.715a2.5 2.5 0 0 1-.317.991l-1.395 2.442a.5.5 0 0 1-.434.252H6.035a.5.5 0 0 1-.416-.223l-1.433-2.15a1.5 1.5 0 0 1-.243-.666l-.345-3.105a.5.5 0 0 1 .399-.546L5 8.11V9a.5.5 0 0 0 1 0V1.75A.75.75 0 0 1 6.75 1zM8.5 4.466V1.75a1.75 1.75 0 1 0-3.5 0v5.34l-1.2.24a1.5 1.5 0 0 0-1.196 1.636l.345 3.106a2.5 2.5 0 0 0 .405 1.11l1.433 2.15A1.5 1.5 0 0 0 6.035 16h6.385a1.5 1.5 0 0 0 1.302-.756l1.395-2.441a3.5 3.5 0 0 0 .444-1.389l.271-2.715a2 2 0 0 0-1.99-2.199h-.581a5.114 5.114 0 0 0-.195-.248c-.191-.229-.51-.568-.88-.716-.364-.146-.846-.132-1.158-.108l-.132.012a1.26 1.26 0 0 0-.56-.642 2.632 2.632 0 0 0-.738-.288c-.31-.062-.739-.058-1.05-.046l-.048.002zm2.094 2.025z"/></svg>;
case 'puzzle': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M3.112 3.645A1.5 1.5 0 0 1 4.605 2H7a.5.5 0 0 1 .5.5v.382c0 .696-.497 1.182-.872 1.469a.459.459 0 0 0-.115.118.113.113 0 0 0-.012.025L6.5 4.5v.003l.003.01c.004.01.014.028.036.053a.86.86 0 0 0 .27.194C7.09 4.9 7.51 5 8 5c.492 0 .912-.1 1.19-.24a.86.86 0 0 0 .271-.194.213.213 0 0 0 .039-.063v-.009a.112.112 0 0 0-.012-.025.459.459 0 0 0-.115-.118c-.375-.287-.872-.773-.872-1.469V2.5A.5.5 0 0 1 9 2h2.395a1.5 1.5 0 0 1 1.493 1.645L12.645 6.5h.237c.195 0 .42-.147.675-.48.21-.274.528-.52.943-.52.568 0 .947.447 1.154.862C15.877 6.807 16 7.387 16 8s-.123 1.193-.346 1.638c-.207.415-.586.862-1.154.862-.415 0-.733-.246-.943-.52-.255-.333-.48-.48-.675-.48h-.237l.243 2.855A1.5 1.5 0 0 1 11.395 14H9a.5.5 0 0 1-.5-.5v-.382c0-.696.497-1.182.872-1.469a.459.459 0 0 0 .115-.118.113.113 0 0 0 .012-.025L9.5 11.5v-.003a.214.214 0 0 0-.039-.064.859.859 0 0 0-.27-.193C8.91 11.1 8.49 11 8 11c-.491 0-.912.1-1.19.24a.859.859 0 0 0-.271.194.214.214 0 0 0-.039.063v.003l.001.006a.113.113 0 0 0 .012.025c.016.027.05.068.115.118.375.287.872.773.872 1.469v.382a.5.5 0 0 1-.5.5H4.605a1.5 1.5 0 0 1-1.493-1.645L3.356 9.5h-.238c-.195 0-.42.147-.675.48-.21.274-.528.52-.943.52-.568 0-.947-.447-1.154-.862C.123 9.193 0 8.613 0 8s.123-1.193.346-1.638C.553 5.947.932 5.5 1.5 5.5c.415 0 .733.246.943.52.255.333.48.48.675.48h.238l-.244-2.855zM4.605 3a.5.5 0 0 0-.498.55l.001.007.29 3.4A.5.5 0 0 1 3.9 7.5h-.782c-.696 0-1.182-.497-1.469-.872a.459.459 0 0 0-.118-.115.112.112 0 0 0-.025-.012L1.5 6.5h-.003a.213.213 0 0 0-.064.039.86.86 0 0 0-.193.27C1.1 7.09 1 7.51 1 8c0 .491.1.912.24 1.19.07.14.14.225.194.271a.213.213 0 0 0 .063.039H1.5l.006-.001a.112.112 0 0 0 .025-.012.459.459 0 0 0 .118-.115c.287-.375.773-.872 1.469-.872H3.9a.5.5 0 0 1 .498.542l-.29 3.408a.5.5 0 0 0 .497.55h1.878c-.048-.166-.195-.352-.463-.557-.274-.21-.52-.528-.52-.943 0-.568.447-.947.862-1.154C6.807 10.123 7.387 10 8 10s1.193.123 1.638.346c.415.207.862.586.862 1.154 0 .415-.246.733-.52.943-.268.205-.415.39-.463.557h1.878a.5.5 0 0 0 .498-.55l-.001-.007-.29-3.4A.5.5 0 0 1 12.1 8.5h.782c.696 0 1.182.497 1.469.872.05.065.091.099.118.115.013.008.021.01.025.012a.02.02 0 0 0 .006.001h.003a.214.214 0 0 0 .064-.039.86.86 0 0 0 .193-.27c.14-.28.24-.7.24-1.191 0-.492-.1-.912-.24-1.19a.86.86 0 0 0-.194-.271.215.215 0 0 0-.063-.039H14.5l-.006.001a.113.113 0 0 0-.025.012.459.459 0 0 0-.118.115c-.287.375-.773.872-1.469.872H12.1a.5.5 0 0 1-.498-.543l.29-3.407a.5.5 0 0 0-.497-.55H9.517c.048.166.195.352.463.557.274.21.52.528.52.943 0 .568-.447.947-.862 1.154C9.193 5.877 8.613 6 8 6s-1.193-.123-1.638-.346C5.947 5.447 5.5 5.068 5.5 4.5c0-.415.246-.733.52-.943.268-.205.415-.39.463-.557H4.605z"/></svg>;
case 'question-circle': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/></svg>;
@ -406,6 +409,7 @@ const SVG = (props: Props) => {
case 'star-solid': return <svg viewBox="0 0 576 512" width={ `${ width }px` } height={ `${ height }px` } ><path d="M259.3 17.8 194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"/></svg>;
case 'star': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.523-3.356c.329-.314.158-.888-.283-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288l1.847-3.658 1.846 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.564.564 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/></svg>;
case 'step-forward': return <svg viewBox="0 0 448 512" width={ `${ width }px` } height={ `${ height }px` } ><path d="M372 31h-8c-6.6 0-12 5.4-12 12v190.3c-1.1-1.2-2.2-2.4-3.5-3.4l-232-191.4C95.9 21.3 64 35.6 64 63v384c0 27.4 31.9 41.8 52.5 24.6l232-192.6c1.3-1.1 2.4-2.2 3.5-3.4V467c0 6.6 5.4 12 12 12h8c6.6 0 12-5.4 12-12V43c0-6.6-5.4-12-12-12zm-40.5 223.4L96.2 446.8l-.1.1-.1.1V63l.1.1.2.1 235.2 191.2z"/></svg>;
case 'stickies': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M1.5 0A1.5 1.5 0 0 0 0 1.5V13a1 1 0 0 0 1 1V1.5a.5.5 0 0 1 .5-.5H14a1 1 0 0 0-1-1H1.5z"/><path d="M3.5 2A1.5 1.5 0 0 0 2 3.5v11A1.5 1.5 0 0 0 3.5 16h6.086a1.5 1.5 0 0 0 1.06-.44l4.915-4.914A1.5 1.5 0 0 0 16 9.586V3.5A1.5 1.5 0 0 0 14.5 2h-11zM3 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5V9h-4.5A1.5 1.5 0 0 0 9 10.5V15H3.5a.5.5 0 0 1-.5-.5v-11zm7 11.293V10.5a.5.5 0 0 1 .5-.5h4.293L10 14.793z"/></svg>;
case 'stop-record-circle': return <svg viewBox="0 0 24 24" width={ `${ width }px` } height={ `${ height }px` } ><path clipRule="evenodd" fillRule="evenodd" d="M8 16h8V8H8v8Zm4-14C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Z"/></svg>;
case 'stopwatch': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M8.5 5.6a.5.5 0 1 0-1 0v2.9h-3a.5.5 0 0 0 0 1H8a.5.5 0 0 0 .5-.5V5.6z"/><path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64a.715.715 0 0 1 .012-.013l.354-.354-.354-.353a.5.5 0 0 1 .707-.708l1.414 1.415a.5.5 0 1 1-.707.707l-.353-.354-.354.354a.512.512 0 0 1-.013.012A7 7 0 1 1 7 2.071V1.5a.5.5 0 0 1-.5-.5zM8 3a6 6 0 1 0 .001 12A6 6 0 0 0 8 3z"/></svg>;
case 'store': return <svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M4 4v2H2V4h2zm1 7V9a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1zm0-5V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1zm5 5V9a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1zm0-5V4a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1zM9 4v2H7V4h2zm5 0h-2v2h2V4zM4 9v2H2V9h2zm5 0v2H7V9h2zm5 0v2h-2V9h2zm-3-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V4zm1 4a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2z"/></svg>;

View file

@ -24,18 +24,27 @@ function error(...args) {
}
let groupTm = {};
let groupedLogs = {};
function group(groupName, ...args) {
if (!window.env.PRODUCTION || options.verbose) {
if (!groupTm[groupName]) {
if (groupTm[groupName]) {
clearTimeout(groupTm[groupName])
groupTm[groupName] = null
} else {
groupedLogs[groupName] = []
}
groupedLogs[groupName].push(args);
groupTm[groupName] = setTimeout(() => {
console.groupCollapsed(groupName)
groupedLogs[groupName].forEach((log) => {
console.log(...log)
})
console.groupEnd()
delete groupTm[groupName]
}, 500);
console.groupCollapsed(groupName);
}
console.log(...args);
delete groupedLogs[groupName]
}, 500)
options.exceptionsLogs.push(args)
}
}

View file

@ -7,7 +7,7 @@ export interface Indexed {
}
export interface Moveable {
move(time: number, isJump?: boolean): void
move(time: number): void
}
export interface Cleanable {

View file

@ -1,4 +1,5 @@
import type { Store, Moveable, Interval } from '../common/types';
import MessageManager from 'App/player/web/MessageManager'
const fps = 60
const performance: { now: () => number } = window.performance || { now: Date.now.bind(Date) }
@ -54,18 +55,18 @@ export default class Animator {
private animationFrameRequestId: number = 0
constructor(private store: Store<GetState>, private mm: Moveable) {
constructor(private store: Store<GetState>, private mm: MessageManager) {
// @ts-ignore
window.playerJump = this.jump.bind(this)
}
private setTime(time: number, isJump?: boolean) {
private setTime(time: number) {
this.store.update({
time,
completed: false,
})
this.mm.move(time, isJump)
this.mm.move(time)
}
private startAnimation() {
@ -183,11 +184,11 @@ export default class Animator {
jump = (time: number) => {
if (this.store.get().playing) {
cancelAnimationFrame(this.animationFrameRequestId)
this.setTime(time, true)
this.setTime(time)
this.startAnimation()
this.store.update({ livePlay: time === this.store.get().endTime })
} else {
this.setTime(time, true)
this.setTime(time)
this.store.update({ livePlay: time === this.store.get().endTime })
}
}

View file

@ -289,7 +289,7 @@ export default class MessageManager {
this.activityManager = new ActivityManager(this.session.duration.milliseconds);
}
move(t: number, isJump?: boolean, index?: number): void {
move(t: number, index?: number): void {
const stateToUpdate: Partial<State> = {};
/* == REFACTOR_ME == */
const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index);
@ -339,7 +339,7 @@ export default class MessageManager {
if (!!lastResize) {
this.setSize(lastResize)
}
this.pagesManager.moveReady(t, isJump).then(() => {
this.pagesManager.moveReady(t).then(() => {
const lastScroll = this.scrollManager.moveGetLast(t, index);
if (!!lastScroll && this.screen.window) {

View file

@ -56,7 +56,7 @@ export default class WebLivePlayer extends WebPlayer {
const bytes = await requestEFSDom(this.session.sessionId)
const fileReader = new MFileReader(bytes, this.session.startedAt)
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.messageManager.distributeMessage(msg, msg._index)
this.messageManager.distributeMessage(msg)
}
this.wpState.update({
liveTimeTravel: true,

View file

@ -142,7 +142,7 @@ export default class DOMManager extends ListWalker<Message> {
private setNodeAttribute(msg: { id: number, name: string, value: string }) {
let { name, value } = msg;
const vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("SetNodeAttribute: Node not found", msg); return }
if (vn.node.tagName === "INPUT" && name === "name") {
// Otherwise binds local autocomplete values (maybe should ignore on the tracker level)
@ -169,7 +169,7 @@ export default class DOMManager extends ListWalker<Message> {
this.removeBodyScroll(msg.id, vn)
}
private applyMessage = (msg: Message, isJump?: boolean): Promise<any> | undefined => {
private applyMessage = (msg: Message): Promise<any> | undefined => {
let vn: VNode | undefined
let doc: Document | null
let styleSheet: CSSStyleSheet | PostponedStyleSheet | undefined
@ -230,14 +230,14 @@ export default class DOMManager extends ListWalker<Message> {
return
case MType.RemoveNode:
vn = this.vElements.get(msg.id) || this.vTexts.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn.parentNode) { logger.error("Parent node not found", msg); return }
if (!vn) { logger.error("RemoveNode: Node not found", msg); return }
if (!vn.parentNode) { logger.error("RemoveNode: Parent node not found", msg); return }
vn.parentNode.removeChild(vn)
this.vElements.delete(msg.id)
this.vTexts.delete(msg.id)
return
case MType.SetNodeAttribute:
if (isJump && msg.name === 'href') this.attrsBacktrack.push(msg)
if (msg.name === 'href') this.attrsBacktrack.push(msg)
else this.setNodeAttribute(msg)
return
case MType.StringDict:
@ -247,7 +247,7 @@ export default class DOMManager extends ListWalker<Message> {
this.stringDict[msg.nameKey] === undefined && logger.error("No dictionary key for msg 'name': ", msg)
this.stringDict[msg.valueKey] === undefined && logger.error("No dictionary key for msg 'value': ", msg)
if (this.stringDict[msg.nameKey] === undefined || this.stringDict[msg.valueKey] === undefined ) { return }
if (isJump && this.stringDict[msg.nameKey] === 'href') this.attrsBacktrack.push(msg)
if (this.stringDict[msg.nameKey] === 'href') this.attrsBacktrack.push(msg)
else {
this.setNodeAttribute({
id: msg.id,
@ -258,12 +258,12 @@ export default class DOMManager extends ListWalker<Message> {
return
case MType.RemoveNodeAttribute:
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("RemoveNodeAttribute: Node not found", msg); return }
vn.removeAttribute(msg.name)
return
case MType.SetInputValue:
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("SetInoputValue: Node not found", msg); return }
const nodeWithValue = vn.node
if (!(nodeWithValue instanceof HTMLInputElement
|| nodeWithValue instanceof HTMLTextAreaElement
@ -283,13 +283,13 @@ export default class DOMManager extends ListWalker<Message> {
return
case MType.SetInputChecked:
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("SetInputChecked: Node not found", msg); return }
(vn.node as HTMLInputElement).checked = msg.checked
return
case MType.SetNodeData:
case MType.SetCssData: // mbtodo: remove css transitions when timeflow is not natural (on jumps)
vn = this.vTexts.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("SetCssData: Node not found", msg); return }
vn.setData(msg.data)
if (vn.node instanceof HTMLStyleElement) {
doc = this.screen.document
@ -304,7 +304,7 @@ export default class DOMManager extends ListWalker<Message> {
// @deprecated since 4.0.2 in favor of adopted_ss_insert/delete_rule + add_owner as being common case for StyleSheets
case MType.CssInsertRule:
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("CssInsertRule: Node not found", msg); return }
if (!(vn instanceof VStyleElement)) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn);
return
@ -313,7 +313,7 @@ export default class DOMManager extends ListWalker<Message> {
return
case MType.CssDeleteRule:
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("CssDeleteRule: Node not found", msg); return }
if (!(vn instanceof VStyleElement)) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn);
return
@ -324,7 +324,7 @@ export default class DOMManager extends ListWalker<Message> {
case MType.CreateIFrameDocument:
vn = this.vElements.get(msg.frameID)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("CreateIFrameDocument: Node not found", msg); return }
vn.enforceInsertion()
const host = vn.node
if (host instanceof HTMLIFrameElement) {
@ -384,7 +384,7 @@ export default class DOMManager extends ListWalker<Message> {
if (!vn) {
// non-constructed case
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("AdoptedSsAddOwner: Node not found", msg); return }
if (!(vn instanceof VStyleElement)) { logger.error("Non-style owner", msg); return }
this.ppStyleSheets.set(msg.sheetID, new PostponedStyleSheet(vn.node))
return
@ -411,13 +411,13 @@ export default class DOMManager extends ListWalker<Message> {
return
}
vn = this.vRoots.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("AdoptedSsRemoveOwner: Node not found", msg); return }
//@ts-ignore
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets].filter(s => s !== styleSheet)
return
case MType.LoadFontFace:
vn = this.vRoots.get(msg.parentID)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn) { logger.error("LoadFontFace: Node not found", msg); return }
if (vn instanceof VShadowRoot) { logger.error(`Node ${vn} expected to be a Document`, msg); return }
let descr: Object
try {
@ -460,7 +460,7 @@ export default class DOMManager extends ListWalker<Message> {
}
}
async moveReady(t: number, isJump?: boolean): Promise<void> {
async moveReady(t: number): Promise<void> {
// MBTODO (back jump optimisation):
// - store intemediate virtual dom state
// - cancel previous moveReady tasks (is it possible?) if new timestamp is less
@ -472,15 +472,16 @@ export default class DOMManager extends ListWalker<Message> {
* are applied, so it won't try to download and then cancel when node is created in msg N and removed in msg N+2
* which produces weird bug when asset is cached (10-25ms delay)
* */
await this.moveWait(t, (msg) => this.applyMessage(msg, isJump))
if (isJump) {
// http://0.0.0.0:3333/5/session/8452905874437457
// 70 iframe, 8 create element - STYLE tag
await this.moveWait(t, this.applyMessage)
this.attrsBacktrack.forEach(msg => {
this.applyBacktrack(msg)
})
this.attrsBacktrack = []
}
this.vRoots.forEach(rt => rt.applyChanges()) // MBTODO (optimisation): affected set
this.vRoots.forEach(rt => rt.applyChanges()) // MBTODO (optimisation): affected set
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?
return this.stylesManager.moveReady(t).then(() => {

View file

@ -33,14 +33,14 @@ export default class PagesManager extends ListWalker<DOMManager> {
this.forEach(page => page.sort(comparator))
}
moveReady(t: number, isJump?: boolean): Promise<void> {
moveReady(t: number): Promise<void> {
const requiredPage = this.moveGetLast(t)
if (requiredPage != null) {
this.currentPage = requiredPage
this.currentPage.reset() // Otherwise it won't apply create_document
}
if (this.currentPage != null) {
return this.currentPage.moveReady(t, isJump)
return this.currentPage.moveReady(t)
}
return Promise.resolve()
}

View file

@ -3,7 +3,7 @@ import type { RawMessage } from './raw.gen';
import { MType } from './raw.gen';
import RawMessageReader from './RawMessageReader.gen';
import resolveURL from './urlBasedResolver'
import Logger from 'App/logger'
// TODO: composition instead of inheritance
// needSkipMessage() and next() methods here use buf and p protected properties,
@ -59,10 +59,8 @@ export default class MFileReader extends RawMessageReader {
if (!skippedMessage) {
return null
}
this.logger.group("Openreplay: Skipping messages ", skippedMessage)
Logger.group("Openreplay: Skipping messages ", skippedMessage)
}
this.pLastMessageID = this.p
const rMsg = this.readRawMessage()

View file

@ -0,0 +1,9 @@
import BaseService from './BaseService';
export default class HealthService extends BaseService {
fetchStatus(): Promise<any> {
return this.client.get('/health')
.then(r => r.json())
.then(j => j.data || {})
}
}

View file

@ -10,6 +10,7 @@ import RecordingsService from "./RecordingsService";
import ConfigService from './ConfigService'
import AlertsService from './AlertsService'
import WebhookService from './WebhookService'
import HealthService from "./HealthService";
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
@ -24,6 +25,8 @@ export const configService = new ConfigService();
export const alertsService = new AlertsService();
export const webhookService = new WebhookService();
export const healthService = new HealthService();
export const services = [
dashboardService,
metricService,
@ -37,4 +40,5 @@ export const services = [
configService,
alertsService,
webhookService,
healthService,
]

193
frontend/app/svg/cheers.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -0,0 +1,10 @@
<svg viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2633_13173)">
<path d="M1.5 3.328C2.385 2.958 3.654 2.559 4.888 2.435C6.218 2.301 7.346 2.498 8 3.187V12.933C7.065 12.403 5.88 12.33 4.787 12.44C3.607 12.56 2.417 12.901 1.5 13.251V3.328ZM9 3.187C9.654 2.498 10.782 2.301 12.112 2.435C13.346 2.559 14.615 2.958 15.5 3.328V13.251C14.582 12.901 13.393 12.559 12.213 12.441C11.119 12.33 9.935 12.402 9 12.933V3.187ZM8.5 2.283C7.515 1.436 6.087 1.31 4.787 1.44C3.273 1.593 1.745 2.112 0.793 2.545C0.705649 2.58473 0.631575 2.64875 0.579621 2.72943C0.527667 2.81011 0.500027 2.90404 0.5 3V14C0.500023 14.0837 0.521037 14.166 0.561117 14.2394C0.601197 14.3128 0.659062 14.375 0.729411 14.4203C0.79976 14.4656 0.880345 14.4925 0.963783 14.4985C1.04722 14.5046 1.13085 14.4896 1.207 14.455C2.089 14.055 3.51 13.574 4.887 13.435C6.296 13.293 7.477 13.522 8.11 14.312C8.15685 14.3704 8.21622 14.4175 8.28372 14.4499C8.35122 14.4823 8.42513 14.4991 8.5 14.4991C8.57487 14.4991 8.64878 14.4823 8.71628 14.4499C8.78378 14.4175 8.84315 14.3704 8.89 14.312C9.523 13.522 10.704 13.293 12.112 13.435C13.49 13.574 14.912 14.055 15.793 14.455C15.8692 14.4896 15.9528 14.5046 16.0362 14.4985C16.1197 14.4925 16.2002 14.4656 16.2706 14.4203C16.3409 14.375 16.3988 14.3128 16.4389 14.2394C16.479 14.166 16.5 14.0837 16.5 14V3C16.5 2.90404 16.4723 2.81011 16.4204 2.72943C16.3684 2.64875 16.2944 2.58473 16.207 2.545C15.255 2.112 13.727 1.593 12.213 1.44C10.913 1.309 9.485 1.436 8.5 2.283Z" />
</g>
<defs>
<clipPath id="clip0_2633_13173">
<rect width="16" height="16" fill="white" transform="translate(0.5 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,10 @@
<svg width="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2633_13471)">
<path d="M17.4795 9.00002C17.4795 11.2489 16.5862 13.4057 14.9959 14.9959C13.4057 16.5862 11.2489 17.4795 9.00002 17.4795C6.75112 17.4795 4.59432 16.5862 3.0041 14.9959C1.41388 13.4057 0.520508 11.2489 0.520508 9.00002C0.520508 6.75112 1.41388 4.59432 3.0041 3.0041C4.59432 1.41388 6.75112 0.520508 9.00002 0.520508C11.2489 0.520508 13.4057 1.41388 14.9959 3.0041C16.5862 4.59432 17.4795 6.75112 17.4795 9.00002ZM9.00002 4.76027C8.86605 4.76034 8.73359 4.78848 8.61115 4.84286C8.48872 4.89725 8.37904 4.97668 8.28917 5.07603C8.1993 5.17539 8.13124 5.29247 8.08937 5.41972C8.0475 5.54698 8.03276 5.6816 8.04608 5.81491L8.41706 9.53211C8.42952 9.67814 8.49634 9.81418 8.60429 9.91331C8.71224 10.0124 8.85346 10.0674 9.00002 10.0674C9.14658 10.0674 9.28781 10.0124 9.39576 9.91331C9.50371 9.81418 9.57053 9.67814 9.58299 9.53211L9.95397 5.81491C9.96729 5.6816 9.95254 5.54698 9.91068 5.41972C9.86881 5.29247 9.80075 5.17539 9.71088 5.07603C9.62101 4.97668 9.51133 4.89725 9.38889 4.84286C9.26646 4.78848 9.13399 4.76034 9.00002 4.76027ZM9.00214 11.1199C8.72103 11.1199 8.45143 11.2316 8.25265 11.4304C8.05388 11.6291 7.9422 11.8987 7.9422 12.1798C7.9422 12.461 8.05388 12.7306 8.25265 12.9293C8.45143 13.1281 8.72103 13.2398 9.00214 13.2398C9.28326 13.2398 9.55286 13.1281 9.75163 12.9293C9.95041 12.7306 10.0621 12.461 10.0621 12.1798C10.0621 11.8987 9.95041 11.6291 9.75163 11.4304C9.55286 11.2316 9.28326 11.1199 9.00214 11.1199Z" fill="#CC0000"/>
</g>
<defs>
<clipPath id="clip0_2633_13471">
<rect width="16.959" height="16.959" fill="white" transform="translate(0.520508 0.520508)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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