Compare commits
1 commit
main
...
kube-chang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5ef6329a |
7425 changed files with 297011 additions and 468251 deletions
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,33 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report an issue and help improve OpenReplay
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: estradino
|
||||
|
||||
---
|
||||
|
||||
**Describe the issue**
|
||||
A short description of what the issue is.
|
||||
|
||||
**Steps to reproduce the issue**
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. You got it :)
|
||||
|
||||
**Expected behavior**
|
||||
What you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If possible, that would be make our life easier.
|
||||
|
||||
**OpenReplay Environment**
|
||||
- Frontend stack: [e.g. React/Axios/MobX, Next]
|
||||
- OpenReplay version: [e.g. 1.6.0]
|
||||
- Tracker version: [e.g. 3.5.10]
|
||||
- Plugins used: [e.g. Fetch, Redux]
|
||||
- Cloud provider: [e.g. AWS, GCP]
|
||||
- System specs: [e.g. 2vCPU/16Gb with 50Gb of storage]
|
||||
|
||||
**Additional context**
|
||||
Add additional information you think might be relevant for this behavior.
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,11 +0,0 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Documentation Request
|
||||
url: https://github.com/openreplay/documentation/issues
|
||||
about: Report a mistake or suggest anything we might be missing in the docs
|
||||
- name: Discussions
|
||||
url: https://github.com/openreplay/openreplay/discussions
|
||||
about: Ask and answer various questions on GitHub Discussions
|
||||
- name: Join our Slack Community
|
||||
url: https://slack.openreplay.com
|
||||
about: Take the discussion further by joining our community on Slack
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea or a feature to improve OpenReplay
|
||||
title: ''
|
||||
labels: feature-request
|
||||
assignees: estradino
|
||||
|
||||
---
|
||||
|
||||
Briefly describe the feature you would like to see shipped with the upcoming versions of OpenReplay, the use-case (very important to us) and the alternative solutions you've considered so far.
|
||||
74
.github/composite-actions/update-keys/action.yml
vendored
74
.github/composite-actions/update-keys/action.yml
vendored
|
|
@ -1,74 +0,0 @@
|
|||
name: 'Update Keys'
|
||||
description: 'Updates keys'
|
||||
inputs:
|
||||
domain_name:
|
||||
required: true
|
||||
description: 'Domain Name'
|
||||
license_key:
|
||||
required: true
|
||||
description: 'License Key'
|
||||
jwt_secret:
|
||||
required: true
|
||||
description: 'JWT Secret'
|
||||
jwt_spot_secret:
|
||||
required: true
|
||||
description: 'JWT spot Secret'
|
||||
minio_access_key:
|
||||
required: true
|
||||
description: 'MinIO Access Key'
|
||||
minio_secret_key:
|
||||
required: true
|
||||
description: 'MinIO Secret Key'
|
||||
pg_password:
|
||||
required: true
|
||||
description: 'PostgreSQL Password'
|
||||
registry_url:
|
||||
required: true
|
||||
description: 'Registry URL'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Downloading yq
|
||||
run: |
|
||||
VERSION="v4.42.1"
|
||||
sudo wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_linux_amd64 -O /usr/bin/yq
|
||||
sudo chmod +x /usr/bin/yq
|
||||
shell: bash
|
||||
|
||||
- name: "Updating OSS secrets"
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
vars=(
|
||||
"ASSIST_JWT_SECRET:.global.assistJWTSecret"
|
||||
"ASSIST_KEY:.global.assistKey"
|
||||
"DOMAIN_NAME:.global.domainName"
|
||||
"JWT_REFRESH_SECRET:.chalice.env.JWT_REFRESH_SECRET"
|
||||
"JWT_SECRET:.global.jwtSecret"
|
||||
"JWT_SPOT_REFRESH_SECRET:.chalice.env.JWT_SPOT_REFRESH_SECRET"
|
||||
"JWT_SPOT_SECRET:.global.jwtSpotSecret"
|
||||
"LICENSE_KEY:.global.enterpriseEditionLicense"
|
||||
"MINIO_ACCESS_KEY:.global.s3.accessKey"
|
||||
"MINIO_SECRET_KEY:.global.s3.secretKey"
|
||||
"PG_PASSWORD:.postgresql.postgresqlPassword"
|
||||
"REGISTRY_URL:.global.openReplayContainerRegistry"
|
||||
)
|
||||
for var in "${vars[@]}"; do
|
||||
IFS=":" read -r env_var yq_path <<<"$var"
|
||||
yq e -i "${yq_path} = strenv(${env_var})" vars.yaml
|
||||
done
|
||||
shell: bash
|
||||
env:
|
||||
ASSIST_JWT_SECRET: ${{ inputs.assist_jwt_secret }}
|
||||
ASSIST_KEY: ${{ inputs.assist_key }}
|
||||
DOMAIN_NAME: ${{ inputs.domain_name }}
|
||||
JWT_REFRESH_SECRET: ${{ inputs.jwt_refresh_secret }}
|
||||
JWT_SECRET: ${{ inputs.jwt_secret }}
|
||||
JWT_SPOT_REFRESH_SECRET: ${{inputs.jwt_spot_refresh_secret}}
|
||||
JWT_SPOT_SECRET: ${{ inputs.jwt_spot_secret }}
|
||||
LICENSE_KEY: ${{ inputs.license_key }}
|
||||
MINIO_ACCESS_KEY: ${{ inputs.minio_access_key }}
|
||||
MINIO_SECRET_KEY: ${{ inputs.minio_secret_key }}
|
||||
PG_PASSWORD: ${{ inputs.pg_password }}
|
||||
REGISTRY_URL: ${{ inputs.registry_url }}
|
||||
|
||||
12
.github/dependabot.yaml
vendored
12
.github/dependabot.yaml
vendored
|
|
@ -1,12 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
162
.github/workflows/alerts-ee.yaml
vendored
162
.github/workflows/alerts-ee.yaml
vendored
|
|
@ -1,162 +0,0 @@
|
|||
# This action will push the alerts 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
|
||||
- api-*
|
||||
paths:
|
||||
- "ee/api/**"
|
||||
- "api/**"
|
||||
- "!api/.gitignore"
|
||||
- "!api/routers"
|
||||
- "!api/app.py"
|
||||
- "!api/*-dev.sh"
|
||||
- "!api/requirements.txt"
|
||||
- "!api/requirements-crons.txt"
|
||||
- "!ee/api/.gitignore"
|
||||
- "!ee/api/routers"
|
||||
- "!ee/api/app.py"
|
||||
- "!ee/api/*-dev.sh"
|
||||
- "!ee/api/requirements.txt"
|
||||
- "!ee/api/requirements-crons.txt"
|
||||
|
||||
name: Build and Deploy Alerts EE
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 api image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd api
|
||||
PUSH_IMAGE=0 bash -x ./build_alerts.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("alerts")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("alerts")
|
||||
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/
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/alerts/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,alerts,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
- 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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
160
.github/workflows/alerts.yaml
vendored
160
.github/workflows/alerts.yaml
vendored
|
|
@ -1,160 +0,0 @@
|
|||
# This action will push the alerts 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
|
||||
- api-*
|
||||
paths:
|
||||
- "api/**"
|
||||
- "!api/.gitignore"
|
||||
- "!api/routers"
|
||||
- "!api/app.py"
|
||||
- "!api/*-dev.sh"
|
||||
- "!api/requirements.txt"
|
||||
- "!api/requirements-crons.txt"
|
||||
|
||||
name: Build and Deploy Alerts
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 Alerts 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 api
|
||||
PUSH_IMAGE=0 bash -x ./build_alerts.sh
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("alerts")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("alerts")
|
||||
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/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 "/alerts/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,alerts,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | 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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
175
.github/workflows/api-ee.yaml
vendored
175
.github/workflows/api-ee.yaml
vendored
|
|
@ -1,27 +1,10 @@
|
|||
# This action will push the chalice 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
|
||||
- api-*
|
||||
paths:
|
||||
- "ee/api/**"
|
||||
- "api/**"
|
||||
- "!api/.gitignore"
|
||||
- "!api/app_alerts.py"
|
||||
- "!api/*-dev.sh"
|
||||
- "!api/requirements-*.txt"
|
||||
- "!ee/api/.gitignore"
|
||||
- "!ee/api/app_alerts.py"
|
||||
- "!ee/api/app_crons.py"
|
||||
- "!ee/api/*-dev.sh"
|
||||
- "!ee/api/requirements-*.txt"
|
||||
- ee/api/**
|
||||
|
||||
name: Build and Deploy Chalice EE
|
||||
|
||||
|
|
@ -31,129 +14,51 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 api image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd api
|
||||
PUSH_IMAGE=0 bash -x ./build.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("chalice")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("chalice")
|
||||
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/
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/chalice/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,chalice,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
- 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: Building and Pusing api image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ee-${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
cd api
|
||||
PUSH_IMAGE=1 bash build.sh ee
|
||||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
cd scripts/helm/
|
||||
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: \"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 chalice
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ee-${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# IMAGE_TAG: ee-${{ github.sha }}
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
#
|
||||
|
|
|
|||
176
.github/workflows/api.yaml
vendored
176
.github/workflows/api.yaml
vendored
|
|
@ -1,21 +1,10 @@
|
|||
# This action will push the chalice 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
|
||||
- api-*
|
||||
paths:
|
||||
- "api/**"
|
||||
- "!api/.gitignore"
|
||||
- "!api/app_alerts.py"
|
||||
- "!api/*-dev.sh"
|
||||
- "!api/requirements-*.txt"
|
||||
- api/**
|
||||
|
||||
name: Build and Deploy Chalice
|
||||
|
||||
|
|
@ -25,126 +14,51 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
- name: Building and Pusing api image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
cd api
|
||||
PUSH_IMAGE=1 bash build.sh
|
||||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
cd scripts/helm/
|
||||
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 chalice
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# 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 api 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 api
|
||||
PUSH_IMAGE=0 bash -x ./build.sh
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("chalice")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("chalice")
|
||||
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 changed image tag
|
||||
sed -i "/chalice/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,chalice,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||
env:
|
||||
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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}
|
||||
# ENVIRONMENT: staging
|
||||
#
|
||||
|
|
|
|||
134
.github/workflows/assist-ee.yaml
vendored
134
.github/workflows/assist-ee.yaml
vendored
|
|
@ -1,134 +0,0 @@
|
|||
# This action will push the assist changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||
required: false
|
||||
default: "false"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "ee/assist/**"
|
||||
- "assist/**"
|
||||
- "!assist/.gitignore"
|
||||
- "!assist/*-dev.sh"
|
||||
|
||||
name: Build and Deploy Assist EE
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Building and Pushing Assist image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd assist
|
||||
PUSH_IMAGE=0 bash -x ./build.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("assist")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("assist")
|
||||
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/
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/assist/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,assist,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
122
.github/workflows/assist-server-ee.yaml
vendored
122
.github/workflows/assist-server-ee.yaml
vendored
|
|
@ -1,122 +0,0 @@
|
|||
# This action will push the assist changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||
required: false
|
||||
default: "false"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "ee/assist-server/**"
|
||||
|
||||
name: Build and Deploy Assist-Server EE
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Building and Pushing Assist-Server image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd assist-server
|
||||
PUSH_IMAGE=0 bash -x ./build.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("assist-server")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("assist-server")
|
||||
for image in ${images[*]};do
|
||||
docker push $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
- name: Creating old image input
|
||||
run: |
|
||||
#
|
||||
# Create yaml with existing image tags
|
||||
#
|
||||
kubectl get pods -n app -o jsonpath="{.items[*].spec.containers[*].image}" |\
|
||||
tr -s '[[:space:]]' '\n' | sort | uniq -c | grep '/foss/' | cut -d '/' -f3 > /tmp/image_tag.txt
|
||||
|
||||
echo > /tmp/image_override.yaml
|
||||
|
||||
for line in `cat /tmp/image_tag.txt`;
|
||||
do
|
||||
image_array=($(echo "$line" | tr ':' '\n'))
|
||||
cat <<EOF >> /tmp/image_override.yaml
|
||||
${image_array[0]}:
|
||||
image:
|
||||
# We've to strip off the -ee, as helm will append it.
|
||||
tag: `echo ${image_array[1]} | cut -d '-' -f 1`
|
||||
EOF
|
||||
done
|
||||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
pwd
|
||||
cd scripts/helmcharts/
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/assist-server/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,assist-server,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
162
.github/workflows/assist-stats.yaml
vendored
162
.github/workflows/assist-stats.yaml
vendored
|
|
@ -1,162 +0,0 @@
|
|||
# This action will push the assist-stats changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||
required: false
|
||||
default: "false"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "assist-stats/**"
|
||||
- "!assist-stats/.gitignore"
|
||||
- "!assist-stats/*-dev.sh"
|
||||
- "!assist-stats/requirements-*.txt"
|
||||
|
||||
name: Build and Deploy Assist Stats ee
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 assist-stats image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd assist-stats
|
||||
PUSH_IMAGE=0 bash -x ./build.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("assist-stats")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("assist-stats")
|
||||
for image in ${images[*]};do
|
||||
docker push $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
|
||||
### Enterprise code deployment
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontextee
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Deploy to kubernetes ee
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
cat <<EOF>/tmp/image_override.yaml
|
||||
assist-stats:
|
||||
image:
|
||||
# We've to strip off the -ee, as helm will append it.
|
||||
tag: ${IMAGE_TAG}
|
||||
EOF
|
||||
|
||||
export IMAGE_TAG=${IMAGE_TAG}
|
||||
# Update changed image tag
|
||||
yq '.utilities.apiCrons.assiststats.image.tag = strenv(IMAGE_TAG)' -i /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,assist-stats,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
- 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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
133
.github/workflows/assist.yaml
vendored
133
.github/workflows/assist.yaml
vendored
|
|
@ -1,133 +0,0 @@
|
|||
# This action will push the assist changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||
required: false
|
||||
default: "false"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "assist/**"
|
||||
- "!assist/.gitignore"
|
||||
- "!assist/*-dev.sh"
|
||||
|
||||
name: Build and Deploy Assist
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Building and Pushing Assist 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 assist
|
||||
PUSH_IMAGE=0 bash -x ./build.sh
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("assist")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("assist")
|
||||
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/
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/assist/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,assist,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -1,71 +0,0 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '30 6 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
159
.github/workflows/crons-ee.yaml
vendored
159
.github/workflows/crons-ee.yaml
vendored
|
|
@ -1,159 +0,0 @@
|
|||
# This action will push the crons 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
|
||||
- api-*
|
||||
paths:
|
||||
- "ee/api/**"
|
||||
- "api/**"
|
||||
- "!api/.gitignore"
|
||||
- "!api/app.py"
|
||||
- "!api/app_alerts.py"
|
||||
- "!api/*-dev.sh"
|
||||
- "!api/requirements.txt"
|
||||
- "!api/requirements-alerts.txt"
|
||||
- "!ee/api/.gitignore"
|
||||
- "!ee/api/app.py"
|
||||
- "!ee/api/app_alerts.py"
|
||||
- "!ee/api/*-dev.sh"
|
||||
- "!ee/api/requirements.txt"
|
||||
- "!ee/api/requirements-crons.txt"
|
||||
|
||||
name: Build and Deploy Crons EE
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 api image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd api
|
||||
PUSH_IMAGE=0 bash -x ./build_crons.sh ee
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("crons")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("crons")
|
||||
for image in ${images[*]};do
|
||||
docker push $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
- name: Creating old image input
|
||||
env:
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
cat <<EOF>/tmp/image_override.yaml
|
||||
image: &image
|
||||
tag: "${IMAGE_TAG}"
|
||||
utilities:
|
||||
apiCrons:
|
||||
assiststats:
|
||||
image: *image
|
||||
report:
|
||||
image: *image
|
||||
sessionsCleaner:
|
||||
image: *image
|
||||
projectsStats:
|
||||
image: *image
|
||||
fixProjectsStats:
|
||||
image: *image
|
||||
EOF
|
||||
|
||||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,utilities,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks --kube-version=$k_version | kubectl apply -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
156
.github/workflows/db-migrate.yaml
vendored
156
.github/workflows/db-migrate.yaml
vendored
|
|
@ -1,156 +0,0 @@
|
|||
name: Database migration Deployment
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- ee/scripts/helm/db/init_dbs/**
|
||||
- scripts/helm/db/init_dbs/**
|
||||
|
||||
# Disable previous workflows for this action.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }} #-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
db-migration:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Checking whether migration is needed for OSS
|
||||
id: check-migration
|
||||
run: |-
|
||||
[[ `git --no-pager diff --name-only HEAD HEAD~1 | grep -E "scripts/helm/db/init_dbs" | grep -vE ^ee/` ]] || echo "::set-output name=skip_migration_oss::true"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
if: ${{ steps.check-migration.outputs.skip_migration_oss != 'true' }}
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Creating old image input
|
||||
if: ${{ steps.check-migration.outputs.skip_migration_oss != 'true' }}
|
||||
run: |
|
||||
set -x
|
||||
#
|
||||
# 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
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Deploy to kubernetes foss
|
||||
if: ${{ steps.check-migration.outputs.skip_migration_oss != 'true' }}
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
sed -i "s/domainName: \"\"/domainName: \"${{ secrets.OSS_DOMAIN_NAME }}\"/g" vars.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --atomic --set forceMigration=true --set dbMigrationUpstreamBranch=${IMAGE_TAG}
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
|
||||
### Enterprise code deployment
|
||||
|
||||
- name: cleaning old assets
|
||||
run: |
|
||||
rm -rf /tmp/image_*
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontextee
|
||||
|
||||
- name: Creating old image input
|
||||
env:
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
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
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Deploy to kubernetes ee
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
helm upgrade --install openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --atomic --set forceMigration=true --set dbMigrationUpstreamBranch=${IMAGE_TAG}
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
|
||||
85
.github/workflows/frontend-dev.yaml
vendored
85
.github/workflows/frontend-dev.yaml
vendored
|
|
@ -1,85 +0,0 @@
|
|||
name: Frontend Dev Deployment
|
||||
on: workflow_dispatch
|
||||
# Disable previous workflows for this action.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }} #-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
domain_name: ${{ secrets.DEV_DOMAIN_NAME }}
|
||||
license_key: ${{ secrets.DEV_LICENSE_KEY }}
|
||||
jwt_secret: ${{ secrets.DEV_JWT_SECRET }}
|
||||
minio_access_key: ${{ secrets.DEV_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.DEV_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.DEV_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.DEV_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Building and Pushing frontend image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
set -x
|
||||
cd frontend
|
||||
mv .env.sample .env
|
||||
docker run --rm -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v $(pwd):/home/${USER} -w /home/${USER} --name node_build node:20-slim /bin/bash -c "yarn && yarn build"
|
||||
# https://github.com/docker/cli/issues/1134#issuecomment-613516912
|
||||
DOCKER_BUILDKIT=1 docker build --target=cicd -t $DOCKER_REPO/frontend:${IMAGE_TAG} .
|
||||
docker tag $DOCKER_REPO/frontend:${IMAGE_TAG} $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
|
||||
|
||||
- name: Deploy to kubernetes foss
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
set -x
|
||||
cat <<EOF>>/tmp/image_override.yaml
|
||||
frontend:
|
||||
image:
|
||||
tag: ${IMAGE_TAG}
|
||||
EOF
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/frontend/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
iMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
51
.github/workflows/frontend-ee.yaml
vendored
Normal file
51
.github/workflows/frontend-ee.yaml
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
name: S3 Deploy EE
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- ee/frontend/**
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
- name: Install
|
||||
run: npm install
|
||||
|
||||
- name: Build and deploy
|
||||
run: |
|
||||
cd frontend
|
||||
bash build.sh
|
||||
cp -arl public frontend
|
||||
minio_pod=$(kubectl get po -n db -l app.kubernetes.io/name=minio -n db --output custom-columns=name:.metadata.name | tail -n+2)
|
||||
echo $minio_pod
|
||||
echo copying frontend to container.
|
||||
kubectl -n db cp frontend $minio_pod:/data/
|
||||
rm -rf frontend
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# AWS_REGION: eu-central-1
|
||||
# AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }}
|
||||
174
.github/workflows/frontend.yaml
vendored
174
.github/workflows/frontend.yaml
vendored
|
|
@ -1,159 +1,51 @@
|
|||
name: Frontend Foss Deployment
|
||||
name: S3 Deploy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- frontend/**
|
||||
# Disable previous workflows for this action.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }} #-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/home/runner/work/openreplay/openreplay/frontend/node_modules
|
||||
/home/runner/work/openreplay/openreplay/frontend/.yarn
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('frontend/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
- name: Install
|
||||
run: npm install
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Building and Pushing frontend image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
set -x
|
||||
cd frontend
|
||||
mv .env.sample .env
|
||||
docker run --rm -v /etc/passwd:/etc/passwd -u `id -u`:`id -g` -v $(pwd):/home/${USER} -w /home/${USER} --name node_build node:20-slim /bin/bash -c "yarn && yarn build"
|
||||
# https://github.com/docker/cli/issues/1134#issuecomment-613516912
|
||||
DOCKER_BUILDKIT=1 docker build --target=cicd -t $DOCKER_REPO/frontend:${IMAGE_TAG} .
|
||||
docker tag $DOCKER_REPO/frontend:${IMAGE_TAG} $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}
|
||||
docker push $DOCKER_REPO/frontend:${IMAGE_TAG}-ee
|
||||
|
||||
- name: Deploy to kubernetes foss
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
|
||||
set -x
|
||||
cat <<EOF>>/tmp/image_override.yaml
|
||||
frontend:
|
||||
image:
|
||||
tag: ${IMAGE_TAG}
|
||||
EOF
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/frontend/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
### Enterprise code deployment
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontextee
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Deploy to kubernetes ee
|
||||
run: |
|
||||
cd scripts/helmcharts/
|
||||
cat <<EOF>/tmp/image_override.yaml
|
||||
frontend:
|
||||
image:
|
||||
# We've to strip off the -ee, as helm will append it.
|
||||
tag: ${IMAGE_TAG}
|
||||
EOF
|
||||
|
||||
# Update changed image tag
|
||||
sed -i "/frontend/{n;n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,frontend,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -f -
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# We're not passing -ee flag, because helm will add that.
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
- name: Build and deploy
|
||||
run: |
|
||||
cd frontend
|
||||
bash build.sh
|
||||
cp -arl public frontend
|
||||
minio_pod=$(kubectl get po -n db -l app.kubernetes.io/name=minio -n db --output custom-columns=name:.metadata.name | tail -n+2)
|
||||
echo $minio_pod
|
||||
echo copying frontend to container.
|
||||
kubectl -n db cp frontend $minio_pod:/data/
|
||||
rm -rf frontend
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
# AWS_REGION: eu-central-1
|
||||
# AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }}
|
||||
|
|
|
|||
189
.github/workflows/patch-build-old.yaml
vendored
189
.github/workflows/patch-build-old.yaml
vendored
|
|
@ -1,189 +0,0 @@
|
|||
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
services:
|
||||
description: 'Comma separated names of services to build(in small letters).'
|
||||
required: true
|
||||
default: 'chalice,frontend'
|
||||
tag:
|
||||
description: 'Tag to update.'
|
||||
required: true
|
||||
type: string
|
||||
branch:
|
||||
description: 'Branch to build patches from. Make sure the branch is uptodate with tag. Else itll cause missing commits.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
name: Build patches from tag, rewrite commit HEAD to older timestamp, and Push the tag
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build Patch from old tag
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 4
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
|
||||
- name: Set Remote with GITHUB_TOKEN
|
||||
run: |
|
||||
git config --unset http.https://github.com/.extraheader
|
||||
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
|
||||
- name: Create backup tag with timestamp
|
||||
run: |
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
BACKUP_TAG="${{ github.event.inputs.tag }}-backup-${TIMESTAMP}"
|
||||
echo "BACKUP_TAG=${BACKUP_TAG}" >> $GITHUB_ENV
|
||||
echo "INPUT_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||
git tag $BACKUP_TAG || { echo "Failed to create backup tag"; exit 1; }
|
||||
git push origin $BACKUP_TAG || { echo "Failed to push backup tag"; exit 1; }
|
||||
echo "Created backup tag: $BACKUP_TAG"
|
||||
|
||||
# Get the oldest commit date from the last 3 commits in raw format
|
||||
OLDEST_COMMIT_TIMESTAMP=$(git log -3 --pretty=format:"%at" | tail -1)
|
||||
echo "Oldest commit timestamp: $OLDEST_COMMIT_TIMESTAMP"
|
||||
# Add 1 second to the timestamp
|
||||
NEW_TIMESTAMP=$((OLDEST_COMMIT_TIMESTAMP + 1))
|
||||
echo "NEW_TIMESTAMP=$NEW_TIMESTAMP" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Setup yq
|
||||
uses: mikefarah/yq@master
|
||||
|
||||
# Configure AWS credentials for the first registry
|
||||
- name: Configure AWS credentials for RELEASE_ARM_REGISTRY
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_DEPOT_ACCESS_KEY }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_DEPOT_SECRET_KEY }}
|
||||
aws-region: ${{ secrets.AWS_DEPOT_DEFAULT_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR for RELEASE_ARM_REGISTRY
|
||||
id: login-ecr-arm
|
||||
run: |
|
||||
aws ecr get-login-password --region ${{ secrets.AWS_DEPOT_DEFAULT_REGION }} | docker login --username AWS --password-stdin ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||
|
||||
- uses: depot/setup-action@v1
|
||||
- name: Get HEAD Commit ID
|
||||
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
- name: Define Branch Name
|
||||
run: echo "BRANCH_NAME=${{inputs.branch}}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||
DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||
MSAAS_REPO_FOLDER: /tmp/msaas
|
||||
run: |
|
||||
set -exo pipefail
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git checkout -b $BRANCH_NAME
|
||||
working_dir=$(pwd)
|
||||
function image_version(){
|
||||
local service=$1
|
||||
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
||||
current_version=$(yq eval '.AppVersion' $chart_path)
|
||||
new_version=$(echo $current_version | awk -F. '{$NF += 1 ; print $1"."$2"."$3}')
|
||||
echo $new_version
|
||||
# yq eval ".AppVersion = \"$new_version\"" -i $chart_path
|
||||
}
|
||||
function clone_msaas() {
|
||||
[ -d $MSAAS_REPO_FOLDER ] || {
|
||||
git clone -b $INPUT_TAG --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
|
||||
cd $MSAAS_REPO_FOLDER
|
||||
cd openreplay && git fetch origin && git checkout $INPUT_TAG
|
||||
git log -1
|
||||
cd $MSAAS_REPO_FOLDER
|
||||
bash git-init.sh
|
||||
git checkout
|
||||
}
|
||||
}
|
||||
function build_managed() {
|
||||
local service=$1
|
||||
local version=$2
|
||||
echo building managed
|
||||
clone_msaas
|
||||
if [[ $service == 'chalice' ]]; then
|
||||
cd $MSAAS_REPO_FOLDER/openreplay/api
|
||||
else
|
||||
cd $MSAAS_REPO_FOLDER/openreplay/$service
|
||||
fi
|
||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh >> /tmp/arm.txt
|
||||
}
|
||||
# Checking for backend images
|
||||
ls backend/cmd >> /tmp/backend.txt
|
||||
echo Services: "${{ github.event.inputs.services }}"
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
BUILD_SCRIPT_NAME="build.sh"
|
||||
# Build FOSS
|
||||
for SERVICE in "${SERVICES[@]}"; do
|
||||
# Check if service is backend
|
||||
if grep -q $SERVICE /tmp/backend.txt; then
|
||||
cd backend
|
||||
foss_build_args="nil $SERVICE"
|
||||
ee_build_args="ee $SERVICE"
|
||||
else
|
||||
[[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE
|
||||
[[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh"
|
||||
ee_build_args="ee"
|
||||
fi
|
||||
version=$(image_version $SERVICE)
|
||||
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
echo IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
||||
IMAGE_TAG=$version-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
||||
if [[ "$SERVICE" != "chalice" && "$SERVICE" != "frontend" ]]; then
|
||||
IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
echo IMAGE_TAG=$version DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
else
|
||||
build_managed $SERVICE $version
|
||||
fi
|
||||
cd $working_dir
|
||||
chart_path="$working_dir/scripts/helmcharts/openreplay/charts/$SERVICE/Chart.yaml"
|
||||
yq eval ".AppVersion = \"$version\"" -i $chart_path
|
||||
git add $chart_path
|
||||
git commit -m "Increment $SERVICE chart version"
|
||||
done
|
||||
|
||||
- name: Change commit timestamp
|
||||
run: |
|
||||
# Convert the timestamp to a date format git can understand
|
||||
NEW_DATE=$(perl -le 'print scalar gmtime($ARGV[0])." +0000"' $NEW_TIMESTAMP)
|
||||
echo "Setting commit date to: $NEW_DATE"
|
||||
|
||||
# Amend the commit with the new date
|
||||
GIT_COMMITTER_DATE="$NEW_DATE" git commit --amend --no-edit --date="$NEW_DATE"
|
||||
|
||||
# Verify the change
|
||||
git log -1 --pretty=format:"Commit now dated: %cD"
|
||||
|
||||
# git tag and push
|
||||
git tag $INPUT_TAG -f
|
||||
git push origin $INPUT_TAG -f
|
||||
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||
# DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||
# MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||
# MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||
# MSAAS_REPO_FOLDER: /tmp/msaas
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
261
.github/workflows/patch-build.yaml
vendored
261
.github/workflows/patch-build.yaml
vendored
|
|
@ -1,261 +0,0 @@
|
|||
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
services:
|
||||
description: 'Comma separated names of services to build(in small letters).'
|
||||
required: true
|
||||
default: 'chalice,frontend'
|
||||
|
||||
name: Build patches from main branch, Raise PR to Main, and Push to tag
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build Patch from main
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Rebase with main branch, to make sure the code has latest main changes
|
||||
if: github.ref != 'refs/heads/main'
|
||||
run: |
|
||||
git remote -v
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
git config --global rebase.autoStash true
|
||||
git fetch origin main:main
|
||||
git rebase main
|
||||
git log -3
|
||||
|
||||
- name: Downloading yq
|
||||
run: |
|
||||
VERSION="v4.42.1"
|
||||
sudo wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_linux_amd64 -O /usr/bin/yq
|
||||
sudo chmod +x /usr/bin/yq
|
||||
|
||||
# Configure AWS credentials for the first registry
|
||||
- name: Configure AWS credentials for RELEASE_ARM_REGISTRY
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_DEPOT_ACCESS_KEY }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_DEPOT_SECRET_KEY }}
|
||||
aws-region: ${{ secrets.AWS_DEPOT_DEFAULT_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR for RELEASE_ARM_REGISTRY
|
||||
id: login-ecr-arm
|
||||
run: |
|
||||
aws ecr get-login-password --region ${{ secrets.AWS_DEPOT_DEFAULT_REGION }} | docker login --username AWS --password-stdin ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||
|
||||
- uses: depot/setup-action@v1
|
||||
env:
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
- name: Get HEAD Commit ID
|
||||
run: echo "HEAD_COMMIT_ID=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
- name: Define Branch Name
|
||||
run: echo "BRANCH_NAME=patch/main/${HEAD_COMMIT_ID}" >> $GITHUB_ENV
|
||||
- name: Set Remote with GITHUB_TOKEN
|
||||
run: |
|
||||
git config --unset http.https://github.com/.extraheader
|
||||
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
|
||||
- name: Build
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||
DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||
MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||
MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||
MSAAS_REPO_FOLDER: /tmp/msaas
|
||||
SERVICES_INPUT: ${{ github.event.inputs.services }}
|
||||
run: |
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
readonly WORKING_DIR=$(pwd)
|
||||
readonly BUILD_SCRIPT_NAME="build.sh"
|
||||
readonly BACKEND_SERVICES_FILE="/tmp/backend.txt"
|
||||
|
||||
# Initialize git configuration
|
||||
setup_git() {
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
}
|
||||
|
||||
# Get and increment image version
|
||||
image_version() {
|
||||
local service=$1
|
||||
local chart_path="$WORKING_DIR/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
||||
local current_version new_version
|
||||
|
||||
current_version=$(yq eval '.AppVersion' "$chart_path")
|
||||
new_version=$(echo "$current_version" | awk -F. '{$NF += 1; print $1"."$2"."$3}')
|
||||
echo "$new_version"
|
||||
}
|
||||
|
||||
# Clone MSAAS repository if not exists
|
||||
clone_msaas() {
|
||||
if [[ ! -d "$MSAAS_REPO_FOLDER" ]]; then
|
||||
git clone -b dev --recursive "https://x-access-token:${MSAAS_REPO_CLONE_TOKEN}@${MSAAS_REPO_URL}" "$MSAAS_REPO_FOLDER"
|
||||
cd "$MSAAS_REPO_FOLDER"
|
||||
cd openreplay && git fetch origin && git checkout main
|
||||
git log -1
|
||||
cd "$MSAAS_REPO_FOLDER"
|
||||
bash git-init.sh
|
||||
git checkout
|
||||
fi
|
||||
}
|
||||
|
||||
# Build managed services
|
||||
build_managed() {
|
||||
local service=$1
|
||||
local version=$2
|
||||
|
||||
echo "Building managed service: $service"
|
||||
clone_msaas
|
||||
|
||||
if [[ $service == 'chalice' ]]; then
|
||||
cd "$MSAAS_REPO_FOLDER/openreplay/api"
|
||||
else
|
||||
cd "$MSAAS_REPO_FOLDER/openreplay/$service"
|
||||
fi
|
||||
|
||||
local build_cmd="IMAGE_TAG=$version DOCKER_RUNTIME=depot DOCKER_BUILD_ARGS=--push ARCH=arm64 DOCKER_REPO=$DOCKER_REPO_ARM PUSH_IMAGE=0 bash build.sh"
|
||||
|
||||
echo "Executing: $build_cmd"
|
||||
if ! eval "$build_cmd" 2>&1; then
|
||||
echo "Build failed for $service"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Build service with given arguments
|
||||
build_service() {
|
||||
local service=$1
|
||||
local version=$2
|
||||
local build_args=$3
|
||||
local build_script=${4:-$BUILD_SCRIPT_NAME}
|
||||
|
||||
local command="IMAGE_TAG=$version DOCKER_RUNTIME=depot DOCKER_BUILD_ARGS=--push ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash $build_script $build_args"
|
||||
echo "Executing: $command"
|
||||
eval "$command"
|
||||
}
|
||||
|
||||
# Update chart version and commit changes
|
||||
update_chart_version() {
|
||||
local service=$1
|
||||
local version=$2
|
||||
local chart_path="$WORKING_DIR/scripts/helmcharts/openreplay/charts/$service/Chart.yaml"
|
||||
|
||||
# Ensure we're in the original working directory/repository
|
||||
cd "$WORKING_DIR"
|
||||
yq eval ".AppVersion = \"$version\"" -i "$chart_path"
|
||||
git add "$chart_path"
|
||||
git commit -m "Increment $service chart version to $version"
|
||||
git push --set-upstream origin "$BRANCH_NAME"
|
||||
cd -
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
setup_git
|
||||
|
||||
# Get backend services list
|
||||
ls backend/cmd >"$BACKEND_SERVICES_FILE"
|
||||
|
||||
# Parse services input (fix for GitHub Actions syntax)
|
||||
echo "Services: ${SERVICES_INPUT:-$1}"
|
||||
IFS=',' read -ra services <<<"${SERVICES_INPUT:-$1}"
|
||||
|
||||
# Process each service
|
||||
for service in "${services[@]}"; do
|
||||
echo "Processing service: $service"
|
||||
cd "$WORKING_DIR"
|
||||
|
||||
local foss_build_args="" ee_build_args="" build_script="$BUILD_SCRIPT_NAME"
|
||||
|
||||
# Determine build configuration based on service type
|
||||
if grep -q "$service" "$BACKEND_SERVICES_FILE"; then
|
||||
# Backend service
|
||||
cd backend
|
||||
foss_build_args="nil $service"
|
||||
ee_build_args="ee $service"
|
||||
else
|
||||
# Non-backend service
|
||||
case "$service" in
|
||||
chalice | alerts | crons)
|
||||
cd "$WORKING_DIR/api"
|
||||
;;
|
||||
*)
|
||||
cd "$service"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Special build scripts for alerts/crons
|
||||
if [[ $service == 'alerts' || $service == 'crons' ]]; then
|
||||
build_script="build_${service}.sh"
|
||||
fi
|
||||
|
||||
ee_build_args="ee"
|
||||
fi
|
||||
|
||||
# Get version and build
|
||||
local version
|
||||
version=$(image_version "$service")
|
||||
|
||||
# Build FOSS and EE versions
|
||||
build_service "$service" "$version" "$foss_build_args"
|
||||
build_service "$service" "${version}-ee" "$ee_build_args"
|
||||
|
||||
# Build managed version for specific services
|
||||
if [[ "$service" != "chalice" && "$service" != "frontend" ]]; then
|
||||
echo "Nothing to build in managed for service $service"
|
||||
else
|
||||
build_managed "$service" "$version"
|
||||
fi
|
||||
|
||||
# Update chart and commit
|
||||
update_chart_version "$service" "$version"
|
||||
done
|
||||
cd "$WORKING_DIR"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$BACKEND_SERVICES_FILE"
|
||||
}
|
||||
|
||||
echo "Working directory: $WORKING_DIR"
|
||||
# Run main function with all arguments
|
||||
main "$SERVICES_INPUT"
|
||||
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
github_token: ${{ secrets.ACTIONS_COMMMIT_TOKEN }}
|
||||
source_branch: ${{ env.BRANCH_NAME }}
|
||||
destination_branch: "main"
|
||||
pr_title: "Updated patch build from main ${{ env.HEAD_COMMIT_ID }}"
|
||||
pr_body: |
|
||||
This PR updates the Helm chart version after building the patch from $HEAD_COMMIT_ID.
|
||||
Once this PR is merged, tag update job will run automatically.
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO_ARM: ${{ secrets.RELEASE_ARM_REGISTRY }}
|
||||
# DOCKER_REPO_OSS: ${{ secrets.RELEASE_OSS_REGISTRY }}
|
||||
# MSAAS_REPO_CLONE_TOKEN: ${{ secrets.MSAAS_REPO_CLONE_TOKEN }}
|
||||
# MSAAS_REPO_URL: ${{ secrets.MSAAS_REPO_URL }}
|
||||
# MSAAS_REPO_FOLDER: /tmp/msaas
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
86
.github/workflows/pr-env-delete.yaml
vendored
86
.github/workflows/pr-env-delete.yaml
vendored
|
|
@ -1,86 +0,0 @@
|
|||
name: PR-Env-Delete
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
env_origin_url:
|
||||
description: |
|
||||
URL of the origin of the PR env to be deleted. Example: https://pr-1717-ee.openreplay.tools
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
create-vcluster-pr:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
build_service: ${{ github.event.inputs.build_service }}
|
||||
env_flavour: ${{ github.event.inputs.env_flavour }}
|
||||
steps:
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.OR_PR_AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.OR_PR_AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.OR_PR_AWS_DEFAULT_REGION}}
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.PR_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
- name: Install vCluster CLI
|
||||
run: |
|
||||
# Replace with the command to install vCluster CLI
|
||||
curl -s -L "https://github.com/loft-sh/vcluster/releases/download/v0.16.4/vcluster-linux-amd64" -o /usr/local/bin/vcluster
|
||||
chmod +x /usr/local/bin/vcluster
|
||||
- name: Deleting vcluster
|
||||
run: |
|
||||
url=${{ github.event.inputs.env_origin_url }}
|
||||
# Remove the protocol part of the URL
|
||||
url_no_protocol=${url#*//}
|
||||
|
||||
# Extract the subdomain and domain
|
||||
subdomain=$(echo $url_no_protocol | cut -d"." -f1)
|
||||
domain=$(echo $url_no_protocol | cut -d"." -f2-)
|
||||
echo "subdomain=$subdomain" >> $GITHUB_ENV
|
||||
echo "domain=$domain" >> $GITHUB_ENV
|
||||
vcluster delete -n $subdomain-vcluster $subdomain-vcluster
|
||||
echo $subdomain $domain
|
||||
- name: Get LoadBalancer IP
|
||||
id: lb-ip
|
||||
run: |
|
||||
LB_IP=$(kubectl get svc ingress-ingress-nginx-controller -n default -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
|
||||
echo "::set-output name=ip::$LB_IP"
|
||||
- name: Delete dns record
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.OR_PR_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.OR_PR_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.OR_PR_AWS_DEFAULT_REGION }}
|
||||
run: |
|
||||
DOMAIN_NAME_1=$subdomain.$domain
|
||||
DOMAIN_NAME_2=$subdomain-vcluster.$domain
|
||||
|
||||
cat <<EOF > route53-changes.json
|
||||
{
|
||||
"Comment": "Create record set for VCluster",
|
||||
"Changes": [
|
||||
{
|
||||
"Action": "DELETE",
|
||||
"ResourceRecordSet": {
|
||||
"Name": "$DOMAIN_NAME_1",
|
||||
"Type": "CNAME",
|
||||
"TTL": 300,
|
||||
"ResourceRecords": [{ "Value": "${{ steps.lb-ip.outputs.ip }}" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Action": "DELETE",
|
||||
"ResourceRecordSet": {
|
||||
"Name": "$DOMAIN_NAME_2",
|
||||
"Type": "CNAME",
|
||||
"TTL": 300,
|
||||
"ResourceRecords": [{ "Value": "${{ steps.lb-ip.outputs.ip }}" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
iws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.OR_PR_HOSTED_ZONE_ID }} --change-batch file://route53-changes.json
|
||||
340
.github/workflows/pr-env.yaml
vendored
340
.github/workflows/pr-env.yaml
vendored
|
|
@ -1,340 +0,0 @@
|
|||
name: PR-Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_service:
|
||||
description: |
|
||||
Name of a single service to build(in small letters), eg: api or frontend etc. backend:sevice-name to build service.
|
||||
If what ever image is not built, it'll be deployed from latest release.
|
||||
Options: none/all/service-name/backend:{app1/app1,app2,app3/all}
|
||||
required: false
|
||||
default: none
|
||||
env_flavour:
|
||||
description: 'Which env to build. Values: foss/ee'
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
create-vcluster-pr:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
build_service: ${{ github.event.inputs.build_service }}
|
||||
env_flavour: ${{ github.event.inputs.env_flavour }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.OR_PR_AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.OR_PR_AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.OR_PR_AWS_DEFAULT_REGION}}
|
||||
- name: Setting up env variables
|
||||
run: |
|
||||
# Fetching details open/draft PR for current branch
|
||||
PR_DATA=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/pulls" \
|
||||
| jq -r --arg BRANCH "${{ github.ref_name }}" '.[] | select((.head.ref==$BRANCH) and (.state=="open") and (.draft==true or .draft==false))')
|
||||
# Extracting PR number
|
||||
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number' | head -n 1)
|
||||
if [ -z $PR_NUMBER ]; then
|
||||
echo "No PR found for ${{ github.ref_name}}"
|
||||
exit 100
|
||||
fi
|
||||
|
||||
echo "PR_NUMBER_PRE=$PR_NUMBER" >> $GITHUB_ENV
|
||||
PR_NUMBER=pr-$PR_NUMBER
|
||||
if [ $env_flavour == "ee" ]; then
|
||||
PR_NUMBER=$PR_NUMBER-ee
|
||||
fi
|
||||
echo "PR number: $PR_NUMBER"
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
|
||||
# Extracting PR status (open, closed, merged)
|
||||
PR_STATUS=$(echo "$PR_DATA" | jq -r '.state' | head -n 1)
|
||||
echo "PR status: $PR_STATUS"
|
||||
echo "PR_STATUS=$PR_STATUS" >> $GITHUB_ENV
|
||||
- name: Install vCluster CLI
|
||||
run: |
|
||||
# Replace with the command to install vCluster CLI
|
||||
curl -s -L "https://github.com/loft-sh/vcluster/releases/download/v0.16.4/vcluster-linux-amd64" -o /usr/local/bin/vcluster
|
||||
chmod +x /usr/local/bin/vcluster
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.PR_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Check existing vcluster
|
||||
id: vcluster_exists
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if ! $(vcluster list | grep $PR_NUMBER &> /dev/null); then
|
||||
echo "no cluster found for $PR_NUMBER"
|
||||
echo "::set-output name=failed::true"
|
||||
exit 100
|
||||
fi
|
||||
DOMAIN_NAME=${PR_NUMBER}-vcluster.${{ secrets.OR_PR_DOMAIN_NAME }}
|
||||
vcluster connect ${PR_NUMBER}-vcluster --update-current=false --server=https://$DOMAIN_NAME
|
||||
mv kubeconfig.yaml /tmp/kubeconfig.yaml
|
||||
|
||||
- name: Get LoadBalancer IP
|
||||
if: steps.vcluster_exists.outputs.failed == 'true'
|
||||
id: lb-ip
|
||||
run: |
|
||||
# LB_IP=$(kubectl get svc ingress-ingress-nginx-controller -n default -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
LB_IP=$(kubectl get svc ingress-ingress-nginx-controller -n default -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
|
||||
echo "::set-output name=ip::$LB_IP"
|
||||
|
||||
- name: Create vCluster
|
||||
if: steps.vcluster_exists.outputs.failed == 'true'
|
||||
run: |
|
||||
# Replace with the actual command to create a vCluster
|
||||
pwd
|
||||
cd scripts/pr-env/
|
||||
bash create.sh ${PR_NUMBER}.${{ secrets.OR_PR_DOMAIN_NAME }}
|
||||
cp kubeconfig.yaml /tmp/
|
||||
|
||||
- name: Update AWS Route53 Record
|
||||
if: steps.vcluster_exists.outputs.failed == 'true'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.OR_PR_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.OR_PR_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.OR_PR_AWS_DEFAULT_REGION }}
|
||||
run: |
|
||||
DOMAIN_NAME_1=$PR_NUMBER-vcluster.${{ secrets.OR_PR_DOMAIN_NAME }}
|
||||
DOMAIN_NAME_2=$PR_NUMBER.${{ secrets.OR_PR_DOMAIN_NAME }}
|
||||
|
||||
cat <<EOF > route53-changes.json
|
||||
{
|
||||
"Comment": "Create record set for VCluster",
|
||||
"Changes": [
|
||||
{
|
||||
"Action": "CREATE",
|
||||
"ResourceRecordSet": {
|
||||
"Name": "$DOMAIN_NAME_1",
|
||||
"Type": "CNAME",
|
||||
"TTL": 300,
|
||||
"ResourceRecords": [{ "Value": "${{ steps.lb-ip.outputs.ip }}" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Action": "CREATE",
|
||||
"ResourceRecordSet": {
|
||||
"Name": "$DOMAIN_NAME_2",
|
||||
"Type": "CNAME",
|
||||
"TTL": 300,
|
||||
"ResourceRecords": [{ "Value": "${{ steps.lb-ip.outputs.ip }}" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
#
|
||||
NEW_IP=${{ steps.lb-ip.outputs.ip }}
|
||||
|
||||
# Get the current IP address associated with the domain
|
||||
CURRENT_IP=$(dig +short $DOMAIN_NAME_1 @1.1.1.1)
|
||||
echo "current ip: $CURRENT_IP"
|
||||
# Check if the domain has no IP association or if the IPs are different
|
||||
if [ -z "$CURRENT_IP" ] || [ "$CURRENT_IP" != "$NEW_IP" ]; then
|
||||
aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.OR_PR_HOSTED_ZONE_ID }} --change-batch file://route53-changes.json
|
||||
fi
|
||||
|
||||
|
||||
- name: Wait for DNS Propagation
|
||||
if: steps.vcluster_exists.outputs.failed == 'true'
|
||||
env:
|
||||
EXPECTED_IP: ${{ steps.lb-ip.outputs.ip }}
|
||||
run: |
|
||||
DOMAIN_NAME="$PR_NUMBER-vcluster.${{ secrets.OR_PR_DOMAIN_NAME }}"
|
||||
MAX_ATTEMPTS=30
|
||||
attempt=1
|
||||
until [[ $attempt -gt $MAX_ATTEMPTS ]]
|
||||
do
|
||||
# Use dig to query DNS records
|
||||
DNS_RESULT=$(dig +short $DOMAIN_NAME @1.1.1.1)
|
||||
|
||||
# Check if DNS result is empty
|
||||
if [ -z "$DNS_RESULT" ]; then
|
||||
echo "No IP or CNAME records found for $DOMAIN_NAME."
|
||||
else
|
||||
echo "DNS records found for $DOMAIN_NAME:"
|
||||
echo "$DNS_RESULT"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for DNS propagation... Attempt $attempt of $MAX_ATTEMPTS"
|
||||
((attempt++))
|
||||
sleep 20
|
||||
done
|
||||
|
||||
if [[ $attempt -gt $MAX_ATTEMPTS ]]; then
|
||||
echo "DNS propagation check failed for $DOMAIN_NAME after $MAX_ATTEMPTS attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install openreplay
|
||||
if: steps.vcluster_exists.outputs.failed == 'true'
|
||||
env:
|
||||
KUBECONFIG: /tmp/kubeconfig.yaml
|
||||
run: |
|
||||
DOMAIN_NAME=$PR_NUMBER.${{ secrets.OR_PR_DOMAIN_NAME }}
|
||||
cd scripts/helmcharts
|
||||
sed -i "s/domainName: \"\"/domainName: \"${DOMAIN_NAME}\"/g" vars.yaml
|
||||
# If ee cluster, enable the following
|
||||
if [ $env_flavour == "ee" ]; then
|
||||
# Explanation for the sed command:
|
||||
# /clickhouse:/: Matches lines containing "clickhouse:".
|
||||
# {:a: Starts a block with label 'a'.
|
||||
# n;: Reads the next line.
|
||||
# /enabled:/s/false/true/: If the line contains 'enabled:', replace 'false' with 'true'.
|
||||
# t done;: If the substitution was made, branch to label 'done'.
|
||||
# ba;: Go back to label 'a' if no substitution was made.
|
||||
# :done}: Label 'done', where the script goes after a successful substitution.
|
||||
sed -i '/clickhouse:/{:a;n;/enabled:/s/false/true/;t done; ba; :done}' vars.yaml
|
||||
sed -i '/kafka:/{:a;n;/# enabled:/s/# enabled: .*/enabled: true/;t done; ba; :done}' vars.yaml
|
||||
sed -i '/redis:/{:a;n;/enabled:/s/true/false/;t done; ba; :done}' vars.yaml
|
||||
sed -i "s/enterpriseEditionLicense: \"\"/enterpriseEditionLicense: \"${{ secrets.EE_LICENSE_KEY }}\"/g" vars.yaml
|
||||
sed -i "s/domainName: \"\"/domainName: \"${DOMAIN_NAME}\"/g" vars.yaml
|
||||
fi
|
||||
helm upgrade -i databases -n db ./databases -f vars.yaml --create-namespace --wait -f ../pr-env/resources.yaml
|
||||
helm upgrade -i openreplay -n app ./openreplay -f vars.yaml --create-namespace --set ingress-nginx.enabled=false -f ../pr-env/resources.yaml --wait
|
||||
|
||||
- name: Build and deploy application
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
env: ${{ github.event.inputs.env_flavour }}
|
||||
run: |
|
||||
set -x
|
||||
|
||||
app_name=${{github.event.inputs.build_service}}
|
||||
echo "building and deploying $app_name"
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
export KUBECONFIG=/tmp/kubeconfig.yaml
|
||||
|
||||
function build_and_deploy {
|
||||
apps_to_build=$1
|
||||
case $apps_to_build in
|
||||
backend*)
|
||||
echo "Building backend build"
|
||||
cd $GITHUB_WORKSPACE/backend
|
||||
components=()
|
||||
if [ $apps_to_build == "backend:all" ]; then
|
||||
# Append all folder names from 'cmd/' directory to the array
|
||||
for folder in cmd/*/; do
|
||||
# Use basename to extract the folder name without path
|
||||
folder_name=$(basename "$folder")
|
||||
components+=("$folder_name")
|
||||
done
|
||||
else
|
||||
# "${apps_to_build#*:}" :: Strip backend: and output app1,app2,app3 to read -ra
|
||||
IFS=',' read -ra components <<< "${apps_to_build#*:}"
|
||||
fi
|
||||
echo "Building components: " ${components[@]}
|
||||
for component in "${components[@]}"; do
|
||||
if [ $(docker manifest inspect ${DOCKER_REPO}/$component:${IMAGE_TAG} > /dev/null) ]; then
|
||||
echo Image present upstream. Skipping build: $component
|
||||
else
|
||||
echo "Building backend:$component"
|
||||
PUSH_IMAGE=1 bash -x ./build.sh $env $component
|
||||
fi
|
||||
kubectl set image -n app deployment/$component-openreplay $component=${DOCKER_REPO}/$component:${IMAGE_TAG}
|
||||
done
|
||||
;;
|
||||
chalice|api)
|
||||
echo "Chalice build"
|
||||
component=chalice
|
||||
cd $GITHUB_WORKSPACE/api || (Nothing to build: $component; exit 100)
|
||||
if [ $(docker manifest inspect ${DOCKER_REPO}/$component:${IMAGE_TAG} > /dev/null) ]; then
|
||||
echo Image present upstream. Skipping build: $component
|
||||
else
|
||||
echo "Building backend:$component"
|
||||
PUSH_IMAGE=1 bash -x ./build.sh $env $component
|
||||
fi
|
||||
kubectl set image -n app deployment/$component-openreplay $component=${DOCKER_REPO}/$component:${IMAGE_TAG}
|
||||
;;
|
||||
*)
|
||||
echo "$apps_to_build build"
|
||||
cd $GITHUB_WORKSPACE/$apps_to_build || (Nothing to build: $apps_to_build; exit 100)
|
||||
component=$apps_to_build
|
||||
if [ $(docker manifest inspect ${DOCKER_REPO}/$component:${IMAGE_TAG} > /dev/null) ]; then
|
||||
echo Image present upstream. Skipping build: $component
|
||||
else
|
||||
echo "Building backend:$component"
|
||||
PUSH_IMAGE=1 bash -x ./build.sh $env $component
|
||||
fi
|
||||
kubectl set image -n app deployment/$apps_to_build-openreplay $apps_to_build=${DOCKER_REPO}/$apps_to_build:${IMAGE_TAG}
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
case $app_name in
|
||||
all)
|
||||
build_and_deploy "backend:all"
|
||||
build_and_deploy "frontend"
|
||||
build_and_deploy "chalice"
|
||||
build_and_deploy "sourcemapreader"
|
||||
build_and_deploy "assist-stats"
|
||||
;;
|
||||
none)
|
||||
echo "Nothing to build"
|
||||
;;
|
||||
*)
|
||||
build_and_deploy $app_name
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Sent results to slack
|
||||
if: steps.vcluster_exists.outputs.failed == 'true'
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.OR_PR_SLACK_BOT_TOKEN }}
|
||||
SLACK_CHANNEL: ${{ secrets.OR_PR_SLACK_CHANNEL }}
|
||||
run: |
|
||||
echo hi ${{ steps.vcluster_exists.outputs.failed }}
|
||||
DOMAIN_NAME=https://$PR_NUMBER.${{ secrets.OR_PR_DOMAIN_NAME }}
|
||||
|
||||
# Variables
|
||||
PR_NUMBER=https://github.com/${{ github.repository }}/pull/${PR_NUMBER_PRE}
|
||||
BRANCH_NAME=${{ github.ref_name }}
|
||||
ORIGIN=$DOMAIN_NAME
|
||||
ASSETS_HOST=$DOMAIN_NAME/assets
|
||||
API_EDP=$DOMAIN_NAME/api
|
||||
INGEST_POINT=$DOMAIN_NAME/ingest
|
||||
|
||||
# File to be uploaded
|
||||
FILE_PATH="/tmp/kubeconfig.yaml"
|
||||
if [! -f $FILE_PATH ]; then
|
||||
echo "Kubeconfig file not found: $FILE_PATH"
|
||||
exit 100
|
||||
fi
|
||||
|
||||
# Form the message payload
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"channel": "$SLACK_CHANNEL",
|
||||
"text": "Deployment Information:\n- PR#: $PR_NUMBER\n- PR Status: $PR_STATUS\n- Branch Name: $BRANCH_NAME\n- Origin: $ORIGIN\n- Assets Host: $ASSETS_HOST\n- API Endpoint: $API_EDP\n- Ingest Point: $INGEST_POINT\n- To use the cluster: download the following file and run the following commands, \n export KUBECONFIG=/path/to/kubeconfig.yaml\n k9s"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Send the message to Slack
|
||||
curl -X POST -H "Authorization: Bearer $SLACK_BOT_TOKEN" -H 'Content-type: application/json' --data "$PAYLOAD" https://slack.com/api/chat.postMessage > /dev/null
|
||||
|
||||
# Upload the file to Slack
|
||||
curl -F file=@"$FILE_PATH" -F channels="$SLACK_CHANNEL" -F token="$SLACK_BOT_TOKEN" https://slack.com/api/files.upload > /dev/null
|
||||
|
||||
# - name: Cleanup
|
||||
# if: always()
|
||||
# run: |
|
||||
# # Add any cleanup commands if necessary
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
103
.github/workflows/release-deployment.yaml
vendored
103
.github/workflows/release-deployment.yaml
vendored
|
|
@ -1,103 +0,0 @@
|
|||
name: Release Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
services:
|
||||
description: 'Comma-separated list of services to deploy. eg: frontend,api,sink'
|
||||
required: true
|
||||
branch:
|
||||
description: 'Branch to deploy (defaults to dev)'
|
||||
required: false
|
||||
default: 'dev'
|
||||
|
||||
env:
|
||||
IMAGE_REGISTRY_URL: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||
DEPOT_TOKEN: ${{ secrets.DEPOT_TOKEN }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login $IMAGE_REGISTRY_URL -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- name: Set image tag with branch info
|
||||
run: |
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
echo "IMAGE_TAG=${{ github.event.inputs.branch }}-${SHORT_SHA}" >> $GITHUB_ENV
|
||||
echo "Using image tag: $IMAGE_TAG"
|
||||
|
||||
- uses: depot/setup-action@v1
|
||||
|
||||
- name: Build and push Docker images
|
||||
run: |
|
||||
# Parse the comma-separated services list into an array
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
working_dir=$(pwd)
|
||||
|
||||
# Define backend services (consider moving this to workflow inputs or repo config)
|
||||
ls backend/cmd >> /tmp/backend.txt
|
||||
BUILD_SCRIPT_NAME="build.sh"
|
||||
|
||||
for SERVICE in "${SERVICES[@]}"; do
|
||||
# Check if service is backend
|
||||
if grep -q $SERVICE /tmp/backend.txt; then
|
||||
cd $working_dir/backend
|
||||
foss_build_args="nil $SERVICE"
|
||||
ee_build_args="ee $SERVICE"
|
||||
else
|
||||
cd $working_dir
|
||||
[[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE
|
||||
[[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh"
|
||||
ee_build_args="ee"
|
||||
fi
|
||||
{
|
||||
echo IMAGE_TAG=$IMAGE_TAG DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$IMAGE_REGISTRY_URL PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
IMAGE_TAG=$IMAGE_TAG DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$IMAGE_REGISTRY_URL PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
|
||||
}&
|
||||
{
|
||||
echo IMAGE_TAG=${IMAGE_TAG}-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$IMAGE_REGISTRY_URL PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
||||
IMAGE_TAG=${IMAGE_TAG}-ee DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$IMAGE_REGISTRY_URL PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $ee_build_args
|
||||
}&
|
||||
done
|
||||
wait
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
name: Using ee release cluster
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_RELEASE_KUBECONFIG }}
|
||||
|
||||
- name: Deploy to ee release Kubernetes
|
||||
run: |
|
||||
echo "Deploying services to EE cluster: ${{ github.event.inputs.services }}"
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
for SERVICE in "${SERVICES[@]}"; do
|
||||
SERVICE=$(echo $SERVICE | xargs) # Trim whitespace
|
||||
echo "Deploying $SERVICE to EE cluster with image tag: ${IMAGE_TAG}"
|
||||
kubectl set image deployment/$SERVICE-openreplay -n app $SERVICE=${IMAGE_REGISTRY_URL}/$SERVICE:${IMAGE_TAG}-ee
|
||||
done
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
name: Using foss release cluster
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.FOSS_RELEASE_KUBECONFIG }}
|
||||
|
||||
- name: Deploy to FOSS release Kubernetes
|
||||
run: |
|
||||
echo "Deploying services to FOSS cluster: ${{ github.event.inputs.services }}"
|
||||
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
|
||||
for SERVICE in "${SERVICES[@]}"; do
|
||||
SERVICE=$(echo $SERVICE | xargs) # Trim whitespace
|
||||
echo "Deploying $SERVICE to FOSS cluster with image tag: ${IMAGE_TAG}"
|
||||
echo "Deploying $SERVICE to FOSS cluster with image tag: ${IMAGE_TAG}"
|
||||
kubectl set image deployment/$SERVICE-openreplay -n app $SERVICE=${IMAGE_REGISTRY_URL}/$SERVICE:${IMAGE_TAG}
|
||||
done
|
||||
150
.github/workflows/sourcemaps-reader-ee.yaml
vendored
150
.github/workflows/sourcemaps-reader-ee.yaml
vendored
|
|
@ -1,150 +0,0 @@
|
|||
# This action will push the sourcemapreader changes to ee
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||
required: false
|
||||
default: "false"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "ee/sourcemap-reader/**"
|
||||
- "sourcemap-reader/**"
|
||||
- "!sourcemap-reader/.gitignore"
|
||||
- "!sourcemap-reader/*-dev.sh"
|
||||
|
||||
name: Build and Deploy sourcemap-reader EE
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 sourcemaps-reader image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
cd sourcemap-reader
|
||||
PUSH_IMAGE=0 bash -x ./build.sh
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("sourcemaps-reader")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("sourcemaps-reader")
|
||||
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 changed image tag
|
||||
sed -i "/sourcemaps-reader/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
sed -i "s/sourcemaps-reader/sourcemapreader/g" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | kubectl apply -n app -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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
149
.github/workflows/sourcemaps-reader.yaml
vendored
149
.github/workflows/sourcemaps-reader.yaml
vendored
|
|
@ -1,149 +0,0 @@
|
|||
# This action will push the sourcemapreader changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_security_checks:
|
||||
description: "Skip Security checks if there is a unfixable vuln or error. Value: true/false"
|
||||
required: false
|
||||
default: "false"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- "sourcemap-reader/**"
|
||||
- "!sourcemap-reader/.gitignore"
|
||||
- "!sourcemap-reader/*-dev.sh"
|
||||
|
||||
name: Build and Deploy sourcemap-reader
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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 sourcemaps-reader 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 sourcemap-reader
|
||||
PUSH_IMAGE=0 bash -x ./build.sh
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
images=("sourcemaps-reader")
|
||||
for image in ${images[*]};do
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --security-checks vuln --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
done
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
images=("sourcemaps-reader")
|
||||
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 changed image tag
|
||||
sed -i "/sourcemaps-reader/{n;n;s/.*/ tag: ${IMAGE_TAG}/}" /tmp/image_override.yaml
|
||||
sed -i "s/sourcemaps-reader/sourcemapreader/g" /tmp/image_override.yaml
|
||||
|
||||
cat /tmp/image_override.yaml
|
||||
# Deploy command
|
||||
mkdir -p /tmp/charts
|
||||
mv openreplay/charts/{ingress-nginx,sourcemapreader,quickwit,connector} /tmp/charts/
|
||||
rm -rf openreplay/charts/*
|
||||
mv /tmp/charts/* openreplay/charts/
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true --no-hooks | 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
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# limit-access-to-actor: true
|
||||
67
.github/workflows/tracker-tests.yaml
vendored
67
.github/workflows/tracker-tests.yaml
vendored
|
|
@ -1,67 +0,0 @@
|
|||
# Checking unit tests for tracker and assist
|
||||
name: Tracker tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main", "dev" ]
|
||||
paths:
|
||||
- tracker/**
|
||||
pull_request:
|
||||
branches: [ "dev", "main" ]
|
||||
paths:
|
||||
- tracker/**
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: macos-latest
|
||||
name: Build and test Tracker
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
- uses: actions/checkout@v3
|
||||
- name: Cache tracker modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tracker/tracker/node_modules
|
||||
key: ${{ runner.OS }}-test_tracker_build-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
test_tracker_build{{ runner.OS }}-build-
|
||||
test_tracker_build{{ runner.OS }}-
|
||||
- name: Cache tracker-assist modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tracker/tracker-assist/node_modules
|
||||
key: ${{ runner.OS }}-test_tracker_build-${{ hashFiles('**/bun.lockb') }}
|
||||
restore-keys: |
|
||||
test_tracker_build{{ runner.OS }}-build-
|
||||
test_tracker_build{{ runner.OS }}-
|
||||
- name: Setup Testing packages
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
bun install
|
||||
- name: Jest tests
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
bun run test:ci
|
||||
- name: Building test
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
bun run build
|
||||
- name: (TA) Setup Testing packages
|
||||
run: |
|
||||
cd tracker/tracker-assist
|
||||
bun install
|
||||
- name: (TA) Jest tests
|
||||
run: |
|
||||
cd tracker/tracker-assist
|
||||
bun run test:ci
|
||||
- name: (TA) Building test
|
||||
run: |
|
||||
cd tracker/tracker-assist
|
||||
bun run build
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: tracker
|
||||
iame: tracker
|
||||
157
.github/workflows/ui-tests.js.yml
vendored
157
.github/workflows/ui-tests.js.yml
vendored
|
|
@ -1,157 +0,0 @@
|
|||
# Checking unit and visual tests locally on every merge rq to dev and main
|
||||
name: Frontend tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- frontend/**
|
||||
- tracker/**
|
||||
pull_request:
|
||||
branches: [ "dev", "main" ]
|
||||
paths:
|
||||
- frontend/**
|
||||
- tracker/**
|
||||
env:
|
||||
API: ${{ secrets.E2E_API_ORIGIN }}
|
||||
ASSETS: ${{ secrets.E2E_ASSETS_ORIGIN }}
|
||||
APIEDP: ${{ secrets.E2E_EDP_ORIGIN }}
|
||||
CY_ACC: ${{ secrets.CYPRESS_ACCOUNT }}
|
||||
CY_PASS: ${{ secrets.CYPRESS_PASSWORD }}
|
||||
FOSS_PROJECT_KEY: ${{ secrets.FOSS_PROJECT_KEY }}
|
||||
FOSS_INGEST: ${{ secrets.FOSS_INGEST }}
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: macos-latest
|
||||
name: Build and test Tracker plus Replayer
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 20.x ]
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache tracker modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tracker/tracker/node_modules
|
||||
key: ${{ runner.OS }}-test_tracker_build-${{ hashFiles('tracker/tracker/bun.lockb') }}
|
||||
restore-keys: |
|
||||
test_tracker_build-{{ runner.OS }}-build-
|
||||
test_tracker_build-{{ runner.OS }}-
|
||||
- name: Setup Testing packages
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
bun install
|
||||
- name: Build tracker inst
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
bun run build
|
||||
- name: Setup Testing UI Env
|
||||
run: |
|
||||
cd tracker/tracker-testing-playground
|
||||
echo "REACT_APP_KEY=$FOSS_PROJECT_KEY" >> .env
|
||||
echo "REACT_APP_INGEST=$FOSS_INGEST" >> .env
|
||||
- name: Cache testing UI node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tracker/tracker-testing-playground/node_modules
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
- name: Setup Testing packages
|
||||
run: |
|
||||
cd tracker/tracker-testing-playground
|
||||
yarn
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: frontend/node_modules
|
||||
key: ${{ runner.OS }}-build-${{ hashFiles('frontend/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-build-
|
||||
${{ runner.OS }}-
|
||||
- name: Setup env
|
||||
run: |
|
||||
cd frontend
|
||||
echo "NODE_ENV=development" >> .env
|
||||
echo "SOURCEMAP=true" >> .env
|
||||
echo "ORIGIN=$API" >> .env
|
||||
echo "ASSETS_HOST=$ASSETS" >> .env
|
||||
echo "API_EDP=$APIEDP" >> .env
|
||||
echo "SENTRY_ENABLED = false" >> .env
|
||||
echo "SENTRY_URL = ''" >> .env
|
||||
echo "CAPTCHA_ENABLED = false" >> .env
|
||||
echo "CAPTCHA_SITE_KEY = 'asdad'" >> .env
|
||||
echo "MINIO_ENDPOINT = ''" >> .env
|
||||
echo "MINIO_PORT = ''" >> .env
|
||||
echo "MINIO_USE_SSL = ''" >> .env
|
||||
echo "MINIO_ACCESS_KEY = ''" >> .env
|
||||
echo "MINIO_SECRET_KEY = ''" >> .env
|
||||
echo "VERSION = '1.15.0'" >> .env
|
||||
echo "TRACKER_VERSION = '10.0.0'" >> .env
|
||||
echo "COMMIT_HASH = 'dev'" >> .env
|
||||
echo "{ \"account\": \"$CY_ACC\", \"password\": \"$CY_PASS\" }" >> cypress.env.json
|
||||
- name: Setup packages
|
||||
run: |
|
||||
cd frontend
|
||||
yarn
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
cd frontend
|
||||
yarn test:ci
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: ui
|
||||
name: ui
|
||||
- name: Run testing frontend
|
||||
run: |
|
||||
cd tracker/tracker-testing-playground
|
||||
yarn start &> testing.log &
|
||||
echo "Started"
|
||||
npm i -g wait-on
|
||||
echo "Got wait on"
|
||||
sleep 30
|
||||
cat testing.log
|
||||
npx wait-on http://localhost:3000
|
||||
echo "Done"
|
||||
timeout-minutes: 4
|
||||
- name: Run Frontend
|
||||
run: |
|
||||
cd frontend
|
||||
bun start &> frontend.log &
|
||||
echo "Started"
|
||||
sleep 30
|
||||
cat frontend.log
|
||||
npx wait-on http://0.0.0.0:3333
|
||||
echo "Done"
|
||||
timeout-minutes: 4
|
||||
- name: (Chrome) Run visual tests
|
||||
run: |
|
||||
cd frontend
|
||||
yarn cy:test
|
||||
# firefox have different viewport somehow
|
||||
# - name: (Firefox) Run visual tests
|
||||
# run: yarn cy:test-firefox
|
||||
# - name: (Edge) Run visual tests
|
||||
# run: yarn cy:test-edge
|
||||
timeout-minutes: 5
|
||||
- name: Upload Debug
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: 'Snapshots'
|
||||
path: |
|
||||
frontend/cypress/videos
|
||||
frontend/cypress/snapshots/replayer.cy.ts
|
||||
frontend/cypress/screenshots
|
||||
frontend/cypress/snapshots/generalStability.cy.ts
|
||||
42
.github/workflows/update-tag.yaml
vendored
42
.github/workflows/update-tag.yaml
vendored
|
|
@ -1,42 +0,0 @@
|
|||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
name: Release tag update --force
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Build Patch from main
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || github.event.inputs.services == 'true' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get latest release tag using GitHub API
|
||||
id: get-latest-tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/latest" \
|
||||
| jq -r .tag_name)
|
||||
|
||||
# Fallback to git command if API doesn't return a tag
|
||||
if [ "$LATEST_TAG" == "null" ] || [ -z "$LATEST_TAG" ]; then
|
||||
echo "Not found latest tag"
|
||||
exit 100
|
||||
fi
|
||||
|
||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
|
||||
- name: Set Remote with GITHUB_TOKEN
|
||||
run: |
|
||||
git config --unset http.https://github.com/.extraheader
|
||||
git remote set-url origin https://x-access-token:${{ secrets.ACTIONS_COMMMIT_TOKEN }}@github.com/${{ github.repository }}
|
||||
|
||||
- name: Push main branch to tag
|
||||
run: |
|
||||
git checkout main
|
||||
echo "Updating tag ${{ env.LATEST_TAG }} to point to latest commit on main"
|
||||
git push origin HEAD:refs/tags/${{ env.LATEST_TAG }} --force
|
||||
64
.github/workflows/utilities.yaml
vendored
Normal file
64
.github/workflows/utilities.yaml
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# This action will push the utilities changes to aws
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- utilities/**
|
||||
|
||||
name: Build and Deploy Utilities
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Building and Pusing api image
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
cd utilities
|
||||
PUSH_IMAGE=1 bash build.sh
|
||||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
cd scripts/helm/
|
||||
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 utilities
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
|
||||
# - name: Debug Job
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}
|
||||
# ENVIRONMENT: staging
|
||||
#
|
||||
225
.github/workflows/workers-ee.yaml
vendored
225
.github/workflows/workers-ee.yaml
vendored
|
|
@ -1,22 +1,11 @@
|
|||
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_service:
|
||||
description: 'Name of a single service to build(in small letters). "all" to build everything'
|
||||
required: false
|
||||
default: "false"
|
||||
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
|
||||
- dev
|
||||
paths:
|
||||
- ee/backend/**
|
||||
- backend/**
|
||||
|
||||
name: Build and deploy workers EE
|
||||
|
||||
|
|
@ -26,168 +15,72 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
# ref: staging
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
# ref: staging
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.EE_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.EE_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.EE_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.EE_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.EE_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.EE_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.EE_REGISTRY_URL }} -u ${{ secrets.EE_DOCKER_USERNAME }} -p "${{ secrets.EE_REGISTRY_TOKEN }}"
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- name: Downloading yq
|
||||
run: |
|
||||
VERSION="v4.42.1"
|
||||
sudo wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_linux_amd64 -O /usr/bin/yq
|
||||
sudo chmod +x /usr/bin/yq
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.EE_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# # 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: Build, tag
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}-ee
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
#
|
||||
# TODO: Check the container tags are same, then skip the build and deployment.
|
||||
#
|
||||
# Build a docker container and push it to Docker Registry so that it can be deployed to Kubernetes cluster.
|
||||
#
|
||||
# Getting the images to build
|
||||
#
|
||||
set -x
|
||||
touch /tmp/images_to_build.txt
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
tmp_param=${{ github.event.inputs.build_service }}
|
||||
build_param=${tmp_param:-'false'}
|
||||
case ${build_param} in
|
||||
false)
|
||||
{
|
||||
git diff --name-only HEAD HEAD~1 | grep -E "backend/pkg|backend/internal" | grep -vE ^ee/ | cut -d '/' -f3 | uniq | while read -r pkg_name ; do
|
||||
grep -rl "pkg/$pkg_name" backend/services backend/cmd | cut -d '/' -f3
|
||||
done
|
||||
} | awk '!seen[$0]++' > /tmp/images_to_build.txt
|
||||
;;
|
||||
all)
|
||||
ls backend/cmd > /tmp/images_to_build.txt
|
||||
;;
|
||||
*)
|
||||
echo ${{github.event.inputs.build_service }} > /tmp/images_to_build.txt
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $(cat /tmp/images_to_build.txt) == "" ]]; then
|
||||
echo "Nothing to build here"
|
||||
touch /tmp/nothing-to-build-here
|
||||
exit 0
|
||||
fi
|
||||
#
|
||||
# Pushing image to registry
|
||||
#
|
||||
cd backend
|
||||
cat /tmp/images_to_build.txt
|
||||
for image in $(cat /tmp/images_to_build.txt);
|
||||
do
|
||||
echo "Bulding $image"
|
||||
PUSH_IMAGE=0 bash -x ./build.sh ee $image
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
docker push $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
|
||||
done
|
||||
|
||||
- name: Deploying to kuberntes
|
||||
env:
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
run: |
|
||||
#
|
||||
# Deploying image to environment.
|
||||
#
|
||||
set -x
|
||||
[[ -f /tmp/nothing-to-build-here ]] && exit 0
|
||||
cd scripts/helmcharts/
|
||||
|
||||
set -x
|
||||
echo > /tmp/image_override.yaml
|
||||
mkdir /tmp/helmcharts
|
||||
mv openreplay/charts/ingress-nginx /tmp/helmcharts/
|
||||
mv openreplay/charts/quickwit /tmp/helmcharts/
|
||||
mv openreplay/charts/connector /tmp/helmcharts/
|
||||
## Update images
|
||||
for image in $(cat /tmp/images_to_build.txt);
|
||||
do
|
||||
mv openreplay/charts/$image /tmp/helmcharts/
|
||||
cat <<EOF>>/tmp/image_override.yaml
|
||||
${image}:
|
||||
image:
|
||||
# We've to strip off the -ee, as helm will append it.
|
||||
tag: ${IMAGE_TAG}
|
||||
EOF
|
||||
done
|
||||
ls /tmp/helmcharts
|
||||
rm -rf openreplay/charts/*
|
||||
ls openreplay/charts
|
||||
mv /tmp/helmcharts/* openreplay/charts/
|
||||
ls openreplay/charts
|
||||
- name: Build, tag, and Deploy to k8s
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
IMAGE_TAG: ee-${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
#
|
||||
# TODO: Check the container tags are same, then skip the build and deployment.
|
||||
#
|
||||
# Build a docker container and push it to Docker Registry so that it can be deployed to Kubernetes cluster.
|
||||
#
|
||||
# Getting the images to build
|
||||
#
|
||||
git diff --name-only HEAD HEAD~1 | grep backend/services | grep -vE ^ee/ | cut -d '/' -f3 | uniq > backend/images_to_build.txt
|
||||
[[ $(cat backend/images_to_build.txt) != "" ]] || (echo "Nothing to build here"; exit 0)
|
||||
#
|
||||
# Pushing image to registry
|
||||
#
|
||||
cd backend
|
||||
for image in $(cat images_to_build.txt);
|
||||
do
|
||||
echo "Bulding $image"
|
||||
PUSH_IMAGE=1 bash -x ./build.sh ee $image
|
||||
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
|
||||
done
|
||||
|
||||
#
|
||||
# Deploying image to environment.
|
||||
#
|
||||
cd ../scripts/helm/
|
||||
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#jwt_secret_key.*#jwt_secret_key: \"${{ secrets.EE_JWT_SECRET }}\" #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
|
||||
for image in $(cat ../../backend/images_to_build.txt);
|
||||
do
|
||||
sed -i "s/image_tag:.*/image_tag: \"$IMAGE_TAG\"/g" vars.yaml
|
||||
# Deploy command
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true | kubectl apply -f -
|
||||
|
||||
- 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:"
|
||||
bash openreplay-cli --install $image
|
||||
done
|
||||
|
||||
# - name: Debug Job
|
||||
# # if: ${{ failure() }}
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# IMAGE_TAG: ${{ github.sha }}
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
#
|
||||
|
|
|
|||
219
.github/workflows/workers.yaml
vendored
219
.github/workflows/workers.yaml
vendored
|
|
@ -1,19 +1,9 @@
|
|||
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_service:
|
||||
description: 'Name of a single service to build(in small letters). "all" to build everything'
|
||||
required: false
|
||||
default: "false"
|
||||
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
|
||||
- dev
|
||||
paths:
|
||||
- backend/**
|
||||
|
||||
|
|
@ -25,162 +15,71 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
# ref: staging
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We need to diff with old commit
|
||||
# to see which workers got changed.
|
||||
fetch-depth: 2
|
||||
# ref: staging
|
||||
|
||||
- uses: ./.github/composite-actions/update-keys
|
||||
with:
|
||||
assist_jwt_secret: ${{ secrets.ASSIST_JWT_SECRET }}
|
||||
assist_key: ${{ secrets.ASSIST_KEY }}
|
||||
domain_name: ${{ secrets.OSS_DOMAIN_NAME }}
|
||||
jwt_refresh_secret: ${{ secrets.JWT_REFRESH_SECRET }}
|
||||
jwt_secret: ${{ secrets.OSS_JWT_SECRET }}
|
||||
jwt_spot_refresh_secret: ${{ secrets.JWT_SPOT_REFRESH_SECRET }}
|
||||
jwt_spot_secret: ${{ secrets.JWT_SPOT_SECRET }}
|
||||
license_key: ${{ secrets.OSS_LICENSE_KEY }}
|
||||
minio_access_key: ${{ secrets.OSS_MINIO_ACCESS_KEY }}
|
||||
minio_secret_key: ${{ secrets.OSS_MINIO_SECRET_KEY }}
|
||||
pg_password: ${{ secrets.OSS_PG_PASSWORD }}
|
||||
registry_url: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
name: Update Keys
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
- uses: azure/k8s-set-context@v1
|
||||
with:
|
||||
method: kubeconfig
|
||||
kubeconfig: ${{ secrets.OSS_KUBECONFIG }} # Use content of kubeconfig in secret.
|
||||
id: setcontext
|
||||
|
||||
# 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: Build, tag
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
#
|
||||
# TODO: Check the container tags are same, then skip the build and deployment.
|
||||
#
|
||||
# Build a docker container and push it to Docker Registry so that it can be deployed to Kubernetes cluster.
|
||||
#
|
||||
# Getting the images to build
|
||||
#
|
||||
set -xe
|
||||
touch /tmp/images_to_build.txt
|
||||
skip_security_checks=${{ github.event.inputs.skip_security_checks }}
|
||||
tmp_param=${{ github.event.inputs.build_service }}
|
||||
build_param=${tmp_param:-'false'}
|
||||
case ${build_param} in
|
||||
false)
|
||||
{
|
||||
git diff --name-only HEAD HEAD~1 | grep -E "backend/pkg|backend/internal" | grep -vE ^ee/ | cut -d '/' -f3 | uniq | while read -r pkg_name ; do
|
||||
grep -rl "pkg/$pkg_name" backend/services backend/cmd | cut -d '/' -f3
|
||||
done
|
||||
} | awk '!seen[$0]++' > /tmp/images_to_build.txt
|
||||
;;
|
||||
all)
|
||||
ls backend/cmd > /tmp/images_to_build.txt
|
||||
;;
|
||||
*)
|
||||
echo ${{github.event.inputs.build_service }} > /tmp/images_to_build.txt
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $(cat /tmp/images_to_build.txt) == "" ]]; then
|
||||
echo "Nothing to build here"
|
||||
touch /tmp/nothing-to-build-here
|
||||
exit 0
|
||||
fi
|
||||
#
|
||||
# Pushing image to registry
|
||||
#
|
||||
cd backend
|
||||
cat /tmp/images_to_build.txt
|
||||
for image in $(cat /tmp/images_to_build.txt);
|
||||
do
|
||||
echo "Bulding $image"
|
||||
PUSH_IMAGE=0 bash -x ./build.sh skip $image
|
||||
[[ "x$skip_security_checks" == "xtrue" ]] || {
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.56.2/trivy_0.56.2_Linux-64bit.tar.gz | tar -xzf - -C ./
|
||||
./trivy image --db-repository ghcr.io/aquasecurity/trivy-db:2 --db-repository public.ecr.aws/aquasecurity/trivy-db:2 --exit-code 1 --vuln-type os,library --severity "HIGH,CRITICAL" --ignore-unfixed $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
err_code=$?
|
||||
[[ $err_code -ne 0 ]] && {
|
||||
exit $err_code
|
||||
}
|
||||
} && {
|
||||
echo "Skipping Security Checks"
|
||||
}
|
||||
docker push $DOCKER_REPO/$image:$IMAGE_TAG
|
||||
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
|
||||
done
|
||||
|
||||
- name: Deploying to kuberntes
|
||||
env:
|
||||
IMAGE_TAG: ${{ github.ref_name }}_${{ github.sha }}
|
||||
run: |
|
||||
#
|
||||
# Deploying image to environment.
|
||||
#
|
||||
set -x
|
||||
[[ -f /tmp/nothing-to-build-here ]] && exit 0
|
||||
cd scripts/helmcharts/
|
||||
|
||||
set -x
|
||||
echo > /tmp/image_override.yaml
|
||||
mkdir /tmp/helmcharts
|
||||
mv openreplay/charts/ingress-nginx /tmp/helmcharts/
|
||||
mv openreplay/charts/quickwit /tmp/helmcharts/
|
||||
mv openreplay/charts/connector /tmp/helmcharts/
|
||||
## Update images
|
||||
for image in $(cat /tmp/images_to_build.txt);
|
||||
do
|
||||
mv openreplay/charts/$image /tmp/helmcharts/
|
||||
cat <<EOF>>/tmp/image_override.yaml
|
||||
${image}:
|
||||
image:
|
||||
# We've to strip off the -ee, as helm will append it.
|
||||
tag: ${IMAGE_TAG}
|
||||
EOF
|
||||
done
|
||||
ls /tmp/helmcharts
|
||||
rm -rf openreplay/charts/*
|
||||
ls openreplay/charts
|
||||
mv /tmp/helmcharts/* openreplay/charts/
|
||||
ls openreplay/charts
|
||||
- name: Build, tag, and Deploy to k8s
|
||||
id: build-image
|
||||
env:
|
||||
DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
ENVIRONMENT: staging
|
||||
run: |
|
||||
#
|
||||
# TODO: Check the container tags are same, then skip the build and deployment.
|
||||
#
|
||||
# Build a docker container and push it to Docker Registry so that it can be deployed to Kubernetes cluster.
|
||||
#
|
||||
# Getting the images to build
|
||||
#
|
||||
git diff --name-only HEAD HEAD~1 | grep backend/services | grep -vE ^ee/ | cut -d '/' -f3 | uniq > backend/images_to_build.txt
|
||||
[[ $(cat backend/images_to_build.txt) != "" ]] || (echo "Nothing to build here"; exit 0)
|
||||
#
|
||||
# Pushing image to registry
|
||||
#
|
||||
cd backend
|
||||
for image in $(cat images_to_build.txt);
|
||||
do
|
||||
echo "Bulding $image"
|
||||
PUSH_IMAGE=1 bash -x ./build.sh skip $image
|
||||
echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG"
|
||||
done
|
||||
|
||||
#
|
||||
# Deploying image to environment.
|
||||
#
|
||||
cd ../scripts/helm/
|
||||
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
|
||||
for image in $(cat ../../backend/images_to_build.txt);
|
||||
do
|
||||
sed -i "s/image_tag:.*/image_tag: \"$IMAGE_TAG\"/g" vars.yaml
|
||||
# Deploy command
|
||||
helm template openreplay -n app openreplay -f vars.yaml -f /tmp/image_override.yaml --set ingress-nginx.enabled=false --set skipMigration=true | kubectl apply -f -
|
||||
bash kube-install.sh --app $image
|
||||
done
|
||||
|
||||
- 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() }}
|
||||
# if: ${{ failure() }}
|
||||
# uses: mxschmitt/action-tmate@v3
|
||||
# env:
|
||||
# DOCKER_REPO: ${{ secrets.EE_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}-ee
|
||||
# DOCKER_REPO: ${{ secrets.OSS_REGISTRY_URL }}
|
||||
# IMAGE_TAG: ${{ github.sha }}
|
||||
# ENVIRONMENT: staging
|
||||
# with:
|
||||
# iimit-access-to-actor: true
|
||||
#
|
||||
#
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -3,7 +3,4 @@ public
|
|||
node_modules
|
||||
*DS_Store
|
||||
*.env
|
||||
*.log
|
||||
**/*.envrc
|
||||
.idea
|
||||
*.mob*
|
||||
.idea
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
repos:
|
||||
- repo: https://github.com/gitguardian/ggshield
|
||||
rev: v1.14.5
|
||||
hooks:
|
||||
- id: ggshield
|
||||
language_version: python3
|
||||
stages: [commit]
|
||||
75
CLA.md
75
CLA.md
|
|
@ -1,75 +0,0 @@
|
|||
## Individual and Entity Contributor License Agreement (CLA)
|
||||
|
||||
Thank you for your interest in contributing to software projects managed by Asayer, Inc. (“We” or “Us”). This Contributor License Agreement (“Agreement”) documents the rights granted by contributors to Us. This Agreement is for your protection as a contributor as well as for our protection; it does not change your rights to use your own Contributions for any other purpose. To make this document effective, please read the Agreement carefully and then sign it. By signing this Agreement (including by clicking “I agree” or "Sign in with GitHub to agree" and submitting it to us electronically), You are creating a legally binding contract which becomes effective upon your signature or agreement. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. This Agreement covers your present, and all future, Contributions from You, and may cover more than one software project managed by Us.
|
||||
|
||||
### 1. Definitions
|
||||
|
||||
“Affiliates” means any Legal Entities that control, are controlled by, or under common control with another Legal Entity. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such Legal Entity, whether by contract or otherwise, (ii) ownership of fifty percent (50%) or more of the outstanding shares or securities which vote to elect the management or other persons who direct such Legal Entity or (iii) beneficial ownership of such entity.
|
||||
|
||||
“Contribution” means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please contact us before Submitting the Contribution.
|
||||
|
||||
“Copyright” means all rights protecting works of authorship owned or controlled by You or your Affiliates (as may be applicable), including copyright, moral and related (or neighboring) rights, as appropriate, for the full term of their existence, including any extensions by You.
|
||||
|
||||
“Legal Entity” means an entity which is not a natural person.
|
||||
|
||||
“Media” means any portion of a Contribution which is not software.
|
||||
|
||||
“Submit” means any form of electronic or written communication, or recorded verbal communication, sent to Us or our representatives at a destination (including websites) that we own or control or that is otherwise registered to us, including but not limited to electronic mailing lists, source code control systems, instant messages or similar communications, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Work, but excluding any communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.”
|
||||
|
||||
“Work” means any of the products or projects owned or managed by Us, and any work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, Work means the work of authorship to which your Contribution was Submitted. After You Submit the Contribution, it may be included in the Work.
|
||||
|
||||
“You” means (i) the individual who Submits a Contribution to Us, if You are an individual acting on your own behalf, or (ii) the Legal Entity on behalf of whom you Submit a Contribution to Us if you are are Submitting any Contribution on behalf of any entity.
|
||||
|
||||
### 2. Grant of Rights
|
||||
|
||||
#### 2.1 Copyright License
|
||||
|
||||
(a) Except for the license granted to Us in this Agreement, You reserve all right, title, and interest in and to Your Contributions. That means that you can keep doing whatever you want with your Contribution, and you can license it to anyone you want under any terms you want.
|
||||
|
||||
(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, no charge and royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform, sublicense and distribute the Contribution as part of the Work; provided that this license is conditioned upon compliance with Section 2.3.
|
||||
|
||||
#### 2.2 Patent License
|
||||
|
||||
For patent claims, including without limitation method, process, and apparatus claims which You (or Your Affiliates, as may be applicable) own, control or have the right to grant, now or in the future, You grant to Us, and to recipients of software distributed by the Us, a perpetual, worldwide, non-exclusive, transferable, no charge and royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution (and the Contribution in combination with the Work, and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. If any person or entity institutes patent litigation against Contributor or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Contributions, or the Work to which the Contributions were submitted, constitutes direct or contributory patent infringement, then any patent licenses granted under this Agreement for that Contribution to the person or entity instituting the litigation, or the Work to which the Contributions were submitted, shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 2.3 Outbound License
|
||||
|
||||
Based on the grant of rights in Sections 2.1 (meaning, no matter what, you can keep licensing your Contribution to others however you want) and 2.2, if We include Your Contribution in any Work, and if We determine that it is appropriate for the purpose of commercializing any Work or any project under Our control, we may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also use reasonable efforts to continue to license Your Contribution (in the same or other projects or Works) under the terms of the license or licenses under which you Submitted Your Contribution.
|
||||
|
||||
#### 2.4 Moral Rights
|
||||
|
||||
We agree to comply with applicable laws regarding your Contribution, including copyright laws and law related to moral rights. If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect.
|
||||
|
||||
#### 2.5 Our Rights
|
||||
|
||||
You acknowledge that We are not obligated to use Your Contribution as part of any Work, and that we and may decide to include any Contribution We consider appropriate.
|
||||
|
||||
#### 2.6 Reservation of Rights
|
||||
|
||||
Any rights in Your Contribution not expressly licensed under this Agreement are expressly reserved by You.
|
||||
|
||||
### 3. Representations
|
||||
|
||||
You represent (promise) that You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that you create that includes your Contributions, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for your Contributions to Us, or that Your employer has executed a separate Corporate Contributor License Agreement with Us.
|
||||
|
||||
You further represent that each of Your Contributions is Your original creation. You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
|
||||
|
||||
You agree to notify Us of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
|
||||
|
||||
### 4. Disclaimer
|
||||
|
||||
EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, YOUR CONTRIBUTION IS PROVIDED "AS IS". YOU EXPRESSLY DISCLAIM ALL OTHER EXPRESS WARRANTIES AND ALL IMPLIED WARRANTIES. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW.
|
||||
|
||||
### 5. Miscellaneous
|
||||
|
||||
5.1 This Agreement will be governed by and construed in accordance with the laws of the State of Delaware, without regard to conflicts of law provisions. If any provision of this Agreement shall be adjudged by any court of competent jurisdiction to be unenforceable or invalid, that provision shall be limited or eliminated to the minimum extent necessary so that this Agreement shall otherwise remain in full force and effect and enforceable. The sole venue for all disputes relating to this Agreement shall be in the New Castle County, Delaware (USA). The rights and obligations of the parties under this Agreement shall not be governed by the 1980 U.N. Convention on Contracts for the International Sale of Goods.
|
||||
|
||||
5.2 This Agreement may be amended only by a written document signed by the party against whom enforcement is sought.
|
||||
|
||||
5.3 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety.
|
||||
|
||||
5.4 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
|
||||
|
||||
**This Agreement contains the entire understanding of the parties regarding the subject matter of this Agreement and supersedes all prior and contemporaneous negotiations and agreements, whether written or oral, between the parties with respect to the subject matter of this Agreement.**
|
||||
|
||||
By signing this agreement, Contributor accepts and agrees to the preceding terms and conditions for Contributor’s present and future Contributions submitted to Us.
|
||||
|
|
@ -6,7 +6,7 @@ By participating in this project, you are expected to uphold our [Code of Conduc
|
|||
|
||||
## First-time Contributors
|
||||
|
||||
We appreciate all contributions, especially those coming from first time contributors. Good first issues is the best way start. If you're not sure how to help, feel free to reach out anytime for assistance via [email](mailto:hey@openreplay.com) or [Slack](https://slack.openreplay.com). All contributors must approve our [Contributor License Agreement](https://cla-assistant.io/openreplay/openreplay).
|
||||
We appreciate all contributions, especially those coming from first time contributors. Good first issues is the best way start. If you're not sure how to help, feel free to reach out anytime for assistance via [email](mailto:hey@openreplay.com) or [Slack](https://slack.openreplay.com).
|
||||
|
||||
## Areas for Contributing
|
||||
|
||||
|
|
@ -70,6 +70,5 @@ We try to answer the below questions when reviewing a PR:
|
|||
- How will it perform with millions of sessions and users events?
|
||||
- Has it been tested?
|
||||
- Is it introducing any security flaws?
|
||||
- Did the contributor approve our CLA?
|
||||
|
||||
Once your PR passes, we will merge it. Otherwise, we'll politely ask you to make a change.
|
||||
|
|
|
|||
678
LICENSE
678
LICENSE
|
|
@ -1,15 +1,11 @@
|
|||
Copyright (c) 2021-2025 Asayer, Inc dba OpenReplay
|
||||
MIT License
|
||||
|
||||
OpenReplay monorepo uses multiple licenses. Portions of this software are licensed as follows:
|
||||
- All content that resides under the "ee/" directory of this repository, is licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the OpenReplay Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Some directories are licensed under the "MIT" license, as defined below.
|
||||
- Content outside of the above mentioned directories or restrictions defaults to the "GNU Affero General Public License Version 3 (AGPL v3)" license, as defined below.
|
||||
Copyright (c) 2021 Asayer SAS.
|
||||
|
||||
Reach out (license@openreplay.com) if you have any questions regarding licenses.
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
MIT LICENSE
|
||||
- All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
@ -28,667 +24,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
|
|
|
|||
46
README.md
46
README.md
|
|
@ -1,53 +1,40 @@
|
|||
<p align="center">
|
||||
<a href="/README_FR.md">Français</a>
|
||||
|
|
||||
<a href="/README_ESP.md">Español</a>
|
||||
|
|
||||
<a href="/README_RU.md">Русский</a>
|
||||
|
|
||||
<a href="/README_AR.md">العربية</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://openreplay.com/#gh-light-mode-only">
|
||||
<img src="static/openreplay-git-banner-light.png" width="100%">
|
||||
</a>
|
||||
<a href="https://openreplay.com/#gh-dark-mode-only">
|
||||
<img src="static/openreplay-git-banner-dark.png" width="100%">
|
||||
<a href="https://openreplay.com">
|
||||
<img src="static/logo.svg" height="70">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">Session replay for developers</h3>
|
||||
<p align="center">The most advanced session replay for building delightful web apps.</p>
|
||||
<p align="center">The most advanced open-source session replay to build delightful web apps.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-aws">
|
||||
<img src="static/btn-deploy-aws.svg" height="40"/>
|
||||
<img src="static/deploy-aws.png" height="35"/>
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-gcp">
|
||||
<img src="static/btn-deploy-google-cloud.svg" height="40" />
|
||||
<img src="static/deploy-gcp.png" height="35" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-azure">
|
||||
<img src="static/btn-deploy-azure.svg" height="40" />
|
||||
<img src="static/deploy-azure.png" height="35" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-digitalocean">
|
||||
<img src="static/btn-deploy-digital-ocean.svg" height="40" />
|
||||
<img src="static/deploy-do.png" height="35" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/openreplay/openreplay">
|
||||
<img src="static/openreplay-git-hero.svg">
|
||||
<img src="static/overview.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
OpenReplay is an open-source session replay suite you can host yourself, that lets you see what users do on your web app, helping you troubleshoot issues faster.
|
||||
OpenReplay is a session replay stack that lets you see what users do on your web app, helping you troubleshoot issues faster. It's the only open-source alternative to products such as FullStory and LogRocket.
|
||||
|
||||
- **Session replay.** OpenReplay replays what users do, but not only. It also shows you what went under the hood, how your website or app behaves by capturing network activity, console logs, JS errors, store actions/state, page speed metrics, cpu/memory usage and much more.
|
||||
- **Low footprint**. With a ~26KB (.br) tracker that asynchronously sends minimal data for a very limited impact on performance.
|
||||
- **Low footprint**. With a ~18KB (.gz) tracker that asynchronously sends minimal data for a very limited impact on performance.
|
||||
- **Self-hosted**. No more security compliance checks, 3rd-parties processing user data. Everything OpenReplay captures stays in your cloud for a complete control over your data.
|
||||
- **Privacy controls**. Fine-grained security features for sanitizing user data.
|
||||
- **Easy deploy**. With support of major public cloud providers (AWS, GCP, Azure, DigitalOcean).
|
||||
|
|
@ -55,13 +42,12 @@ OpenReplay is an open-source session replay suite you can host yourself, that le
|
|||
## Features
|
||||
|
||||
- **Session replay:** Lets you relive your users' experience, see where they struggle and how it affects their behavior. Each session replay is automatically analyzed based on heuristics, for easy triage.
|
||||
- **Spot:** A Chrome extension that lets record bugs directly from your browser — each recording includes all the technical details developers need to fix them.
|
||||
- **DevTools:** It's like debugging in your own browser. OpenReplay provides you with the full context (network activity, JS errors, store actions/state and 40+ metrics) so you can instantly reproduce bugs and understand performance issues.
|
||||
- **Assist:** Helps you support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.
|
||||
- **Omni-search:** Search and filter by almost any user action/criteria, session attribute or technical event, so you can answer any question. No instrumentation required.
|
||||
- **Analytics:** For surfacing the most impactful issues causing conversion and revenue loss.
|
||||
- **Funnels:** For surfacing the most impactful issues causing conversion and revenue loss.
|
||||
- **Fine-grained privacy controls:** Choose what to capture, what to obscure or what to ignore so user data doesn't even reach your servers.
|
||||
- **Plugins oriented:** Get to the root cause even faster by tracking application state (Redux, VueX, MobX, NgRx, Pinia and Zustand) and logging GraphQL queries (Apollo, Relay) and Fetch/Axios requests.
|
||||
- **Plugins oriented:** Get to the root cause even faster by tracking application state (Redux, VueX, MobX, NgRx) and logging GraphQL queries (Apollo, Relay) and Fetch requests.
|
||||
- **Integrations:** Sync your backend logs with your session replays and see what happened front-to-back. OpenReplay supports Sentry, Datadog, CloudWatch, Stackdriver, Elastic and more.
|
||||
|
||||
## Deployment Options
|
||||
|
|
@ -73,7 +59,6 @@ OpenReplay can be deployed anywhere. Follow our step-by-step guides for deployin
|
|||
- [Azure](https://docs.openreplay.com/deployment/deploy-azure)
|
||||
- [Digital Ocean](https://docs.openreplay.com/deployment/deploy-digitalocean)
|
||||
- [Scaleway](https://docs.openreplay.com/deployment/deploy-scaleway)
|
||||
- [OVHcloud](https://docs.openreplay.com/deployment/deploy-ovhcloud)
|
||||
- [Kubernetes](https://docs.openreplay.com/deployment/deploy-kubernetes)
|
||||
|
||||
## OpenReplay Cloud
|
||||
|
|
@ -87,7 +72,6 @@ Please refer to the [official OpenReplay documentation](https://docs.openreplay.
|
|||
- [Slack](https://slack.openreplay.com) (Connect with our engineers and community)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (Bug and issue reports)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (Product updates, Great content)
|
||||
- [YouTube](https://www.youtube.com/channel/UCcnWlW-5wEuuPAwjTR1Ydxw) (How-to tutorials, past Community Calls)
|
||||
- [Website chat](https://openreplay.com) (Talk to us)
|
||||
|
||||
## Contributing
|
||||
|
|
@ -98,6 +82,10 @@ See our [Contributing Guide](CONTRIBUTING.md) for more details.
|
|||
|
||||
Also, feel free to join our [Slack](https://slack.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Check out our [roadmap](https://www.notion.so/openreplay/Roadmap-889d2c3d968b4786ab9b281ab2394a94) and keep an eye on what's coming next. You're free to [submit](https://github.com/openreplay/openreplay/issues/new) new ideas and vote on features.
|
||||
|
||||
## License
|
||||
|
||||
This monorepo uses several licenses. See [LICENSE](/LICENSE) for more details.
|
||||
This repo is entirely MIT licensed, with the exception of the `ee` directory.
|
||||
|
|
|
|||
106
README_AR.md
106
README_AR.md
|
|
@ -1,106 +0,0 @@
|
|||
<p align="center">
|
||||
<a href="/README_FR.md">Français</a>
|
||||
|
|
||||
<a href="/README_ESP.md">Español</a>
|
||||
|
|
||||
<a href="/README_RU.md">Русский</a>
|
||||
|
|
||||
<a href="/README.md">English</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://openreplay.com/#gh-light-mode-only">
|
||||
<img src="static/openreplay-git-banner-light.png" width="100%">
|
||||
</a>
|
||||
<a href="https://openreplay.com/#gh-dark-mode-only">
|
||||
<img src="static/openreplay-git-banner-dark.png" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">إعادة تشغيل الجلسة للمطورين</h3>
|
||||
<p align="center">إعادة تشغيل الجلسة الأكثر تقدمًا لإنشاء تطبيقات ويب رائعة</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-aws">
|
||||
<img src="static/btn-deploy-aws.svg" height="40"/>
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-gcp">
|
||||
<img src="static/btn-deploy-google-cloud.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-azure">
|
||||
<img src="static/btn-deploy-azure.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-digitalocean">
|
||||
<img src="static/btn-deploy-digital-ocean.svg" height="40" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/openreplay/openreplay">
|
||||
<img src="static/openreplay-git-hero.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
OpenReplay هو مجموعة إعادة تشغيل الجلسة التي يمكنك استضافتها بنفسك، والتي تتيح لك رؤية ما يقوم به المستخدمون على تطبيق الويب الخاص بك، مما يساعدك على حل المشكلات بشكل أسرع.
|
||||
|
||||
- **إعادة تشغيل الجلسة.** يقوم OpenReplay بإعادة تشغيل ما يقوم به المستخدمون، وكيف يتصرف موقع الويب الخاص بك أو التطبيق من خلال التقاط النشاط على الشبكة، وسجلات وحدة التحكم، وأخطاء JavaScript، وإجراءات/حالة التخزين، وقياسات سرعة الصفحة، واستخدام وحدة المعالجة المركزية/الذاكرة، وأكثر من ذلك بكثير.
|
||||
- **بصمة منخفضة**. مع متتبع بحجم حوالي 26 كيلوبايت (نوع .br) الذي يرسل بيانات دقيقة بشكل غير متزامن لتأثير محدود جدًا على الأداء.
|
||||
- **مضيف بواسطتك.** لا مزيد من فحوص الامتثال الأمني، ومعالجة بيانات المستخدمين من قبل جهات خارجية. كل ما يتم التقاطه بواسطة OpenReplay يبقى في سحابتك للتحكم الكامل في بياناتك.
|
||||
- **ضوابط الخصوصية.** ميزات أمان دقيقة لتنقية بيانات المستخدم.
|
||||
- **نشر سهل.** بدعم من مزودي الخدمة السحابية العامة الرئيسيين (AWS، GCP، Azure، DigitalOcean).
|
||||
|
||||
## الميزات
|
||||
|
||||
- **إعادة تشغيل الجلسة:** تتيح لك إعادة تشغيل الجلسة إعادة عيش تجربة مستخدميك، ورؤية أين يواجهون صعوبة وكيف يؤثر ذلك على سلوكهم. يتم تحليل كل إعادة تشغيل للجلسة تلقائيًا بناءً على الأساليب الاستدلالية، لسهولة التقييم.
|
||||
- **أدوات التطوير (DevTools):** إنها مثل التصحيح في متصفحك الخاص. يوفر لك OpenReplay السياق الكامل (نشاط الشبكة، أخطاء JavaScript، إجراءات/حالة التخزين وأكثر من 40 مقياسًا) حتى تتمكن من إعادة إنتاج الأخطاء فورًا وفهم مشكلات الأداء.
|
||||
- **المساعدة (Assist):** تساعدك في دعم مستخدميك من خلال رؤية شاشتهم مباشرة والانضمام فورًا إلى مكالمة (WebRTC) معهم دون الحاجة إلى برامج مشاركة الشاشة من جهات خارجية.
|
||||
- **البحث الشامل (Omni-search):** ابحث وفرز حسب أي عملية/معيار للمستخدم تقريبًا، أو سمة الجلسة أو الحدث التقني، حتى تتمكن من الرد على أي سؤال. لا يلزم تجهيز.
|
||||
- **الأنفاق (Funnels):** للكشف عن المشكلات الأكثر تأثيرًا التي تسبب في فقدان التحويل والإيرادات.
|
||||
- **ضوابط الخصوصية الدقيقة:** اختر ماذا تريد التقاطه، ماذا تريد أن تخفي أو تجاهل حتى لا تصل بيانات المستخدم حتى إلى خوادمك.
|
||||
- **موجهة للمكونات الإضافية (Plugins oriented):** تصل إلى السبب الجذري بشكل أسرع عن طريق تتبع حالة التطبيق (Redux، VueX، MobX، NgRx، Pinia، وZustand) وتسجيل استعلامات GraphQL (Apollo، Relay) وطلبات Fetch/Axios.
|
||||
- **التكاملات (Integrations):** مزامنة سجلات الخادم الخلفي مع إعادات التشغيل للجلسات ورؤية ما حدث من الأمام إلى الخلف. يدعم OpenReplay Sentry وDatadog وCloudWatch وStackdriver وElastic والمزيد.
|
||||
|
||||
## خيارات النشر
|
||||
|
||||
يمكن نشر OpenReplay في أي مكان. اتبع دليلنا الخطوة بالخطوة لنشره على خدمات السحابة العامة الرئيسية:
|
||||
|
||||
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
|
||||
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)
|
||||
- [Azure](https://docs.openreplay.com/deployment/deploy-azure)
|
||||
- [Digital Ocean](https://docs.openreplay.com/deployment/deploy-digitalocean)
|
||||
- [Scaleway](https://docs.openreplay.com/deployment/deploy-scaleway)
|
||||
- [OVHcloud](https://docs.openreplay.com/deployment/deploy-ovhcloud)
|
||||
- [Kubernetes](https://docs.openreplay.com/deployment/deploy-kubernetes)
|
||||
|
||||
## سحابة OpenReplay
|
||||
|
||||
لأولئك الذين يرغبون في استخدام OpenReplay كخدمة، [قم بالتسجيل](https://app.openreplay.com/signup) للحصول على حساب مجاني على عرض السحابة لدينا.
|
||||
|
||||
## دعم المجتمع
|
||||
|
||||
يرجى الرجوع إلى [الوثائق الرسمية لـ OpenReplay](https://docs.openreplay.com/). سيساعدك ذلك في حل المشكلات الشائعة. للحصول على مساعدة إضافية، يمكنك الاتصال بنا عبر أحد هذه القنوات:
|
||||
|
||||
- [Slack](https://slack.openreplay.com) (الاتصال مع مهندسينا والمجتمع)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (تقارير الأخطاء والمشكلات)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (تحديثات المنتج، محتوى رائع)
|
||||
- [YouTube](https://www.youtube.com/channel/UCcnWlW-5wEuuPAwjTR1Ydxw) (دروس حول كيفية الاستخدام، مكالمات مجتمع سابقة)
|
||||
- [دردشة الموقع الإلكتروني](https://openreplay.com) (تحدث معنا)
|
||||
|
||||
## المساهمة
|
||||
|
||||
نحن دائمًا في انتظار المساهمات في OpenReplay، ونحن سعداء بأنك تفكر في ذلك! غير متأكد من أين تبدأ؟ ابحث عن المشاكل المفتوحة، وخاصة تلك المُميزة بأنها مناسبة للمبتدئين.
|
||||
|
||||
انظر دليل المساهمة لدينا [دليل المساهمة](CONTRIBUTING.md) لمزيد من التفاصيل.
|
||||
|
||||
كما توجد حرية الانضمام إلى Slack لدينا [Slack](https://slack.openreplay.com) لطرح الأسئلة، مناقشة الأفكار أو التواصل مع مساهمينا.
|
||||
|
||||
## الخارطة الزمنية
|
||||
|
||||
تحقق من [الخارطة الزمنية لدينا](https://www.notion.so/openreplay/Roadmap-889d2c3d968b4786ab9b281ab2394a94) وابق على اطلاع على ما سيأتي لاحقًا. لديك حرية [تقديم أفكار جديدة](https://github.com/openreplay/openreplay/issues/new) والتصويت على الميزات.
|
||||
|
||||
## الترخيص
|
||||
|
||||
يستخدم هذا المستودع المتعدد التراخيص. انظر إلى [LICENSE](/LICENSE) لمزيد من التفاصيل.
|
||||
106
README_ESP.md
106
README_ESP.md
|
|
@ -1,106 +0,0 @@
|
|||
<p align="center">
|
||||
<a href="/README_FR.md">Français</a>
|
||||
|
|
||||
<a href="/README.md">English</a>
|
||||
|
|
||||
<a href="/README_RU.md">Русский</a>
|
||||
|
|
||||
<a href="/README_RU.md">العربية</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://openreplay.com/#gh-light-mode-only">
|
||||
<img src="static/openreplay-git-banner-light.png" width="100%">
|
||||
</a>
|
||||
<a href="https://openreplay.com/#gh-dark-mode-only">
|
||||
<img src="static/openreplay-git-banner-dark.png" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">Reproducción de sesiones para desarrolladores</h3>
|
||||
<p align="center">La reproducción de sesiones más avanzada para crear aplicaciones web encantadoras.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-aws">
|
||||
<img src="static/btn-deploy-aws.svg" height="40"/>
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-gcp">
|
||||
<img src="static/btn-deploy-google-cloud.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-azure">
|
||||
<img src="static/btn-deploy-azure.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-digitalocean">
|
||||
<img src="static/btn-deploy-digital-ocean.svg" height="40" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/openreplay/openreplay">
|
||||
<img src="static/openreplay-git-hero.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
OpenReplay es una suite de retransmisión de sesiones que puedes alojar tú mismo, lo que te permite ver lo que hacen los usuarios en tu aplicación web y ayudarte a solucionar problemas más rápido.
|
||||
|
||||
- **Reproducción de sesiones.** OpenReplay reproduce lo que hacen los usuarios, pero no solo eso. También te muestra lo que ocurre bajo el capó, cómo se comporta tu sitio web o aplicación al capturar la actividad de la red, registros de la consola, errores de JavaScript, acciones/estado del almacén, métricas de velocidad de la página, uso de CPU/memoria y mucho más.
|
||||
- **Huella reducida.** Con un rastreador de aproximadamente 26 KB (.br) que envía datos mínimos de forma asíncrona, lo que tiene un impacto muy limitado en el rendimiento.
|
||||
- **Auto-alojado.** No más verificaciones de cumplimiento de seguridad, procesamiento de datos de usuario por terceros. Todo lo que OpenReplay captura se queda en tu nube para un control completo sobre tus datos.
|
||||
- **Controles de privacidad.** Funciones de seguridad detalladas para desinfectar los datos de usuario.
|
||||
- **Despliegue sencillo.** Con el soporte de los principales proveedores de nube pública (AWS, GCP, Azure, DigitalOcean).
|
||||
|
||||
## Características
|
||||
|
||||
- **Reproducción de sesiones:** Te permite revivir la experiencia de tus usuarios, ver dónde encuentran dificultades y cómo afecta su comportamiento. Cada reproducción de sesión se analiza automáticamente en función de heurísticas, para un triaje sencillo.
|
||||
- **Herramientas de desarrollo (DevTools):** Es como depurar en tu propio navegador. OpenReplay te proporciona el contexto completo (actividad de red, errores de JavaScript, acciones/estado del almacén y más de 40 métricas) para que puedas reproducir instantáneamente errores y entender problemas de rendimiento.
|
||||
- **Asistencia (Assist):** Te ayuda a brindar soporte a tus usuarios al ver su pantalla en tiempo real y unirte instantáneamente a una llamada (WebRTC) con ellos, sin necesidad de software de uso compartido de pantalla de terceros.
|
||||
- **Búsqueda universal (Omni-search):** Busca y filtra por casi cualquier acción/criterio de usuario, atributo de sesión o evento técnico, para que puedas responder a cualquier pregunta. No se requiere instrumentación.
|
||||
- **Embudos (Funnels):** Para resaltar los problemas más impactantes que causan la conversión y la pérdida de ingresos.
|
||||
- **Controles de privacidad detallados:** Elige qué capturar, qué ocultar o qué ignorar para que los datos de usuario ni siquiera lleguen a tus servidores.
|
||||
- **Orientado a complementos (Plugins oriented):** Llega más rápido a la causa raíz siguiendo el estado de la aplicación (Redux, VueX, MobX, NgRx, Pinia y Zustand) y registrando consultas GraphQL (Apollo, Relay) y solicitudes Fetch/Axios.
|
||||
- **Integraciones:** Sincroniza tus registros del servidor con tus repeticiones de sesiones y observa lo que sucedió de principio a fin. OpenReplay es compatible con Sentry, Datadog, CloudWatch, Stackdriver, Elastic y más.
|
||||
|
||||
## Opciones de implementación
|
||||
|
||||
OpenReplay se puede implementar en cualquier lugar. Sigue nuestras guías paso a paso para implementarlo en los principales servicios de nube pública:
|
||||
|
||||
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
|
||||
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)
|
||||
- [Azure](https://docs.openreplay.com/deployment/deploy-azure)
|
||||
- [Digital Ocean](https://docs.openreplay.com/deployment/deploy-digitalocean)
|
||||
- [Scaleway](https://docs.openreplay.com/deployment/deploy-scaleway)
|
||||
- [OVHcloud](https://docs.openreplay.com/deployment/deploy-ovhcloud)
|
||||
- [Kubernetes](https://docs.openreplay.com/deployment/deploy-kubernetes)
|
||||
|
||||
## OpenReplay Cloud
|
||||
|
||||
Para aquellos que desean usar OpenReplay como un servicio, [regístrate](https://app.openreplay.com/signup) para obtener una cuenta gratuita en nuestra oferta en la nube.
|
||||
|
||||
## Soporte de la comunidad
|
||||
|
||||
Consulta la [documentación oficial de OpenReplay](https://docs.openreplay.com/). Eso debería ayudarte a solucionar problemas comunes. Para obtener ayuda adicional, puedes contactarnos a través de uno de estos canales:
|
||||
|
||||
- [Slack](https://slack.openreplay.com) (Conéctate con nuestros ingenieros y la comunidad)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (Informes de errores y problemas)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (Actualizaciones del producto, contenido excelente)
|
||||
- [YouTube](https://www.youtube.com/channel/UCcnWlW-5wEuuPAwjTR1Ydxw) (Tutoriales, reuniones comunitarias anteriores)
|
||||
- [Chat en el sitio web](https://openreplay.com) (Háblanos)
|
||||
|
||||
## Contribución
|
||||
|
||||
Siempre estamos buscando contribuciones para OpenReplay, ¡y nos alegra que lo estés considerando! ¿No estás seguro por dónde empezar? Busca problemas abiertos, preferiblemente aquellos marcados como "buenas primeras contribuciones".
|
||||
|
||||
Consulta nuestra [Guía de Contribución](CONTRIBUTING.md) para obtener más detalles.
|
||||
|
||||
Además, no dudes en unirte a nuestro [Slack](https://slack.openreplay.com) para hacer preguntas, discutir ideas o conectarte con nuestros colaboradores.
|
||||
|
||||
## Hoja de ruta
|
||||
|
||||
Consulta nuestra [hoja de ruta](https://www.notion.so/openreplay/Roadmap-889d2c3d968b4786ab9b281ab2394a94) y mantente atento a lo que viene a continuación. Eres libre de [enviar](https://github.com/openreplay/openreplay/issues/new) nuevas ideas y votar por funciones.
|
||||
|
||||
## Licencia
|
||||
|
||||
Este monorepo utiliza varias licencias. Consulta [LICENSE](/LICENSE) para obtener más detalles.
|
||||
106
README_FR.md
106
README_FR.md
|
|
@ -1,106 +0,0 @@
|
|||
<p align="center">
|
||||
<a href="/README.md">English</a>
|
||||
|
|
||||
<a href="/README_ESP.md">Español</a>
|
||||
|
|
||||
<a href="/README_RU.md">Русский</a>
|
||||
|
|
||||
<a href="/README_RU.md">العربية</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://openreplay.com/#gh-light-mode-only">
|
||||
<img src="static/openreplay-git-banner-light.png" width="100%">
|
||||
</a>
|
||||
<a href="https://openreplay.com/#gh-dark-mode-only">
|
||||
<img src="static/openreplay-git-banner-dark.png" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">Relecture de session pour développeurs</h3>
|
||||
<p align="center">La relecture de session la plus avancée sur le marché pour des applications perfectionnées.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-aws">
|
||||
<img src="static/btn-deploy-aws.svg" height="40"/>
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-gcp">
|
||||
<img src="static/btn-deploy-google-cloud.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-azure">
|
||||
<img src="static/btn-deploy-azure.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-digitalocean">
|
||||
<img src="static/btn-deploy-digital-ocean.svg" height="40" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/openreplay/openreplay">
|
||||
<img src="static/openreplay-git-hero.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
OpenReplay est une suite d'outils de relecture (appelée aussi "replay") de sessions que vous pouvez héberger vous-même, vous permettant de voir ce que les utilisateurs font sur une application web, vous aidant ainsi à résoudre différents types de problèmes plus rapidement.
|
||||
|
||||
- **Relecture de session.** OpenReplay rejoue ce que les utilisateurs font, mais pas seulement. Il vous montre également ce qui se passe en coulisse, comment votre site web ou votre application se comporte en capturant l'activité réseau, les journaux de console, les erreurs JS, les actions/états du store, les métriques de chargement des pages, l'utilisation du CPU/mémoire, et bien plus encore.
|
||||
- **Faible empreinte**. Avec un traqueur d'environ 26 Ko (.br) qui envoie de manière asynchrone des données minimales, ce qui a un impact très limité sur les performances.
|
||||
- **Auto-hébergé**. Plus de vérifications de conformité en matière de sécurité, plus de traitement des données des utilisateurs par des tiers. Tout ce qu'OpenReplay capture reste dans votre cloud pour un contrôle complet sur vos données.
|
||||
- **Contrôles de confidentialité**. Fonctionnalités de sécurité détaillées pour la désinfection des données utilisateur.
|
||||
- **Déploiement facile**. Avec le support des principaux fournisseurs de cloud public (AWS, GCP, Azure, DigitalOcean).
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
- **Relecture de session :** Vous permet de revivre l'expérience de vos utilisateurs, de voir où ils rencontrent des problèmes et comment cela affecte leur comportement. Chaque relecture de session est automatiquement analysée en se basant sur des heuristiques, pour un triage plus facile des problèmes en fonction de l'impact.
|
||||
- **Outils de développement (DevTools) :** C'est comme déboguer dans votre propre navigateur. OpenReplay vous fournit le contexte complet (activité réseau, erreurs JS, actions/états du store et plus de 40 métriques) pour que vous puissiez instantanément reproduire les bugs et comprendre les problèmes de performance.
|
||||
- **Assistance (Assist) :** Vous aide à soutenir vos utilisateurs en voyant leur écran en direct et en vous connectant instantanément avec eux via appel/vidéo (WebRTC), sans nécessiter de logiciel tiers de partage d'écran.
|
||||
- **Recherche universelle (Omni-search) :** Recherchez et filtrez presque n'importe quelle action/critère utilisateur, attribut de session ou événement technique, afin de pouvoir répondre à n'importe quelle question. Aucune instrumentation requise.
|
||||
- **Entonnoirs (Funnels) :** Pour mettre en évidence les problèmes les plus impactants entraînant une conversion et une perte de revenus.
|
||||
- **Contrôles de confidentialité détaillés :** Choisissez ce que vous voulez capturer, ce que vous voulez obscurcir ou ignorer, de sorte que les données utilisateur n'atteignent même pas vos serveurs.
|
||||
- **Orienté vers les plugins :** Corrigez plus rapidement les bogues en suivant l'état de l'application (Redux, VueX, MobX, NgRx, Pinia et Zustand) et enregistrant les requêtes GraphQL (Apollo, Relay) et les requêtes Fetch/Axios.
|
||||
- **Intégrations :** Synchronisez vos journaux backend avec vos relectures de sessions et voyez ce qui s'est passé du début à la fin. OpenReplay prend en charge Sentry, Datadog, CloudWatch, Stackdriver, Elastic et bien d'autres.
|
||||
|
||||
## Options de déploiement
|
||||
|
||||
OpenReplay peut être déployé n'importe où. Suivez nos guides détaillés pour le déployer sur les principaux clouds publics :
|
||||
|
||||
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
|
||||
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)
|
||||
- [Azure](https://docs.openreplay.com/deployment/deploy-azure)
|
||||
- [Digital Ocean](https://docs.openreplay.com/deployment/deploy-digitalocean)
|
||||
- [Scaleway](https://docs.openreplay.com/deployment/deploy-scaleway)
|
||||
- [OVHcloud](https://docs.openreplay.com/deployment/deploy-ovhcloud)
|
||||
- [Kubernetes](https://docs.openreplay.com/deployment/deploy-kubernetes)
|
||||
|
||||
## OpenReplay Cloud
|
||||
|
||||
Pour ceux qui veulent simplement utiliser OpenReplay en tant que service, [inscrivez-vous](https://app.openreplay.com/signup) pour un compte gratuit sur notre offre cloud.
|
||||
|
||||
## Support de la communauté
|
||||
|
||||
Veuillez vous référer à la [documentation officielle d'OpenReplay](https://docs.openreplay.com/). Cela devrait vous aider à résoudre les problèmes courants. Pour toute aide ou question supplémentaire, vous pouvez nous contacter sur l'un des canaux suivants :
|
||||
|
||||
- [Slack](https://slack.openreplay.com) (Connectez-vous avec nos ingénieurs et notre communauté)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (Rapports de bogues et problèmes)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (Mises à jour du produit, articles techniques et autres annonces)
|
||||
- [YouTube](https://www.youtube.com/channel/UCcnWlW-5wEuuPAwjTR1Ydxw) (Tutoriels)
|
||||
- [Chat sur le site Web](https://openreplay.com) (Nous contacter)
|
||||
|
||||
## Contribution
|
||||
|
||||
Nous sommes toujours à la recherche de contributions pour rendre OpenReplay meilleur. Vous ne savez pas par où commencer ? Recherchez dans notre "GitHub Issues" pour trouver des tickets ouverts, de préférence ceux marqués comme "bonnes premières contributions".
|
||||
|
||||
Consultez notre [Guide de contribution](CONTRIBUTING.md) pour plus de détails.
|
||||
|
||||
N'hésitez pas à rejoindre notre [Slack](https://slack.openreplay.com) pour poser des questions, discuter vos idées ou simplement pour vous connecter avec nos contributeurs.
|
||||
|
||||
## Feuille de route
|
||||
|
||||
Consultez notre [feuille de route](https://www.notion.so/openreplay/Roadmap-889d2c3d968b4786ab9b281ab2394a94) et gardez un œil sur ce qui arrive prochainement. Vous êtes libre de [proposer](https://github.com/openreplay/openreplay/issues/new) de nouvelles idées et de voter pour des fonctionnalités.
|
||||
|
||||
## Licence
|
||||
|
||||
Ce monorepo utilise plusieurs licences. Consultez [LICENSE](/LICENSE) pour plus de détails.
|
||||
107
README_RU.md
107
README_RU.md
|
|
@ -1,107 +0,0 @@
|
|||
<p align="center">
|
||||
<a href="/README_FR.md">Français</a>
|
||||
|
|
||||
<a href="/README_ESP.md">Español</a>
|
||||
|
|
||||
<a href="/README.md">English</a>
|
||||
|
|
||||
<a href="/README_RU.md">العربية</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://openreplay.com/#gh-light-mode-only">
|
||||
<img src="static/openreplay-git-banner-light.png" width="100%">
|
||||
</a>
|
||||
<a href="https://openreplay.com/#gh-dark-mode-only">
|
||||
<img src="static/openreplay-git-banner-dark.png" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">Реплей сессий для разработчиков</h3>
|
||||
<p align="center">Самое продвинутое решение для воспроизведения сессий с открытым исходным кодом для создания восхитительных веб-приложений.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-aws">
|
||||
<img src="static/btn-deploy-aws.svg" height="40"/>
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-gcp">
|
||||
<img src="static/btn-deploy-google-cloud.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-azure">
|
||||
<img src="static/btn-deploy-azure.svg" height="40" />
|
||||
</a>
|
||||
|
||||
<a href="https://docs.openreplay.com/deployment/deploy-digitalocean">
|
||||
<img src="static/btn-deploy-digital-ocean.svg" height="40" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/openreplay/openreplay">
|
||||
<img src="static/openreplay-git-hero.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
OpenReplay - это набор инструментов для воспроизведения пользовательских сессий, позволяющий увидеть действия пользователи в вашем веб-приложении, который вы можете разместить в своем облаке или на серверах.
|
||||
|
||||
- **Воспроизведение сессий.** OpenReplay не только воспроизводит действия пользователей, но и показывает, что происходит под капотом сессии, как ведет себя ваш сайт или приложение, фиксируя сетевую активность, логи консоли, JS-ошибки, действия/состояние стейт менеджеров, показатели скорости страницы, использование процессора/памяти и многое другое.
|
||||
- **Компактность**. Размером всего в ~26 КБ (.br), трекер асинхронно отправляет минимальное количество данных, оказывая очень незначительное влияние на производительность вашего приложения.
|
||||
- **Self-hosted**. Больше никаких проверок на соответствие требованиям безопасности или обработки данных ваших пользователей третьими сторонами. Все, что фиксирует OpenReplay, остается в вашем облаке, что обеспечивает полный контроль над вашими данными.
|
||||
- **Контроль над приватностью**. Тонкие настройки приватности позволяют записывать только действительно необходимые данные.
|
||||
- **Легкая установка**. Мы поддерживаем всех крупных поставщиков облачных услуг (AWS, GCP, Azure, DigitalOcean).
|
||||
|
||||
## Особенности
|
||||
|
||||
- **Session Replay:** Позволяет повторить опыт пользователей, увидеть, где они испытывают трудности и как это влияет на конверсию. Каждый реплей автоматически анализируется на наличие ошибок и аномалий, что значительно облегчает сортировку и поиск проблемных сессий.
|
||||
- **DevTools:** Прямо как отладка в вашем собственном браузере. OpenReplay предоставляет вам полный контекст (сетевая активность, JS ошибки, действия/состояние стейт менеджеров и более 40 метрик), чтобы вы могли мгновенно воспроизвести ошибки и найти проблемы с производительностью.
|
||||
- **Assist:** Позволяет вам помочь вашим пользователям, наблюдая их экран в настоящем времени и мгновенно переходя на звонок (WebRTC) с ними, не требуя стороннего программного обеспечения для совместного просмотра экрана.
|
||||
- **Omni-search:** Поиск и фильтрация практически любого действия пользователя/критерия, атрибута сессии или технического события, чтобы вы могли ответить на любой вопрос.
|
||||
- **Воронки:** Для выявления наиболее влияющих на конверсию мест.
|
||||
- **Тонкая настройка приватности:** Выбирайте, что записывать, а что игнорировать, чтобы данные пользователя даже не отправлялись на ваши сервера.
|
||||
- **Ориентирован на плагины:** С помощью плагинов можно отслеживать состояние приложения (Redux, VueX, MobX, NgRx, Pinia, и Zustand), регистрировать запросы GraphQL (Apollo, Relay) и многое другое.
|
||||
- **Интеграции:** OpenReplay поддерживает интеграции с Sentry, Datadog, CloudWatch, Stackdriver, Elastic и другими провайдерами, позволяя получать еще больше информации о пользовательской сессии.
|
||||
|
||||
## Варианты развертывания
|
||||
|
||||
OpenReplay можно развернуть где угодно. Следуйте нашим пошаговым руководствам по развертыванию на основных публичных облаках:
|
||||
|
||||
- [AWS](https://docs.openreplay.com/deployment/deploy-aws)
|
||||
- [Google Cloud](https://docs.openreplay.com/deployment/deploy-gcp)
|
||||
- [Azure](https://docs.openreplay.com/deployment/deploy-azure)
|
||||
- [Digital Ocean](https://docs.openreplay.com/deployment/deploy-digitalocean)
|
||||
- [Scaleway](https://docs.openreplay.com/deployment/deploy-scaleway)
|
||||
- [OVHcloud](https://docs.openreplay.com/deployment/deploy-ovhcloud)
|
||||
- [Kubernetes](https://docs.openreplay.com/deployment/deploy-kubernetes)
|
||||
|
||||
## OpenReplay Cloud
|
||||
|
||||
Для тех, кто просто хочет использовать OpenReplay как сервис, [зарегистрируйте](https://app.openreplay.com/signup) бесплатную учетную запись в нашем приложении.
|
||||
|
||||
## Поддержка сообщества
|
||||
|
||||
В случае возникновения проблем, вы можете обратиться к [официальной документации OpenReplay](https://docs.openreplay.com/). Это поможет вам решить наиболее распространенные проблемы.
|
||||
Для дополнительной помощи, вы можете связаться с нами через один из этих каналов:
|
||||
|
||||
- [Slack](https://slack.openreplay.com) (Свяжитесь с нашими инженерами и сообществом)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (Отчеты о багах и проблемах)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (Обновления продукта)
|
||||
- [YouTube](https://www.youtube.com/channel/UCcnWlW-5wEuuPAwjTR1Ydxw) (Учебные пособия, прошлые комьюнити-звонки)
|
||||
- [Чат на веб-сайте](https://openreplay.com) (Общайтесь с нами)
|
||||
|
||||
## Содействие
|
||||
|
||||
Мы всегда рады любой помощи в создании OpenReplay, и готовы услышать ваши идеи. Не уверены, с чего начать? Ищите открытые задачи, особенно те, которые отмечены как "good first issue".
|
||||
|
||||
Смотрите наше [руководство по содействию](CONTRIBUTING.md) для более подробной информации.
|
||||
|
||||
Также не стесняйтесь присоединиться к нашему [Slack](https://slack.openreplay.com), чтобы задавать вопросы, обсуждать идеи или связываться с нашими участниками.
|
||||
|
||||
## План развития
|
||||
|
||||
Ознакомьтесь с нашим [планом развития](https://www.notion.so/openreplay/Roadmap-889d2c3d968b4786ab9b281ab2394a94) и следите за тем, что будет далее. Вы можете свободно [предложить](https://github.com/openreplay/openreplay/issues/new) новые идеи и голосовать за функции.
|
||||
|
||||
## Лицензия
|
||||
|
||||
В этом монорепозитории используются разные лицензии. См. [LICENSE](/LICENSE) для получения более подробной информации.
|
||||
67
api/.chalice/config.bundle.json
Normal file
67
api/.chalice/config.bundle.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"app_name": "parrot",
|
||||
"environment_variables": {
|
||||
},
|
||||
"stages": {
|
||||
"default-foss": {
|
||||
"api_gateway_stage": "default-fos",
|
||||
"manage_iam_role": false,
|
||||
"iam_role_arn": "",
|
||||
"autogen_policy": true,
|
||||
"environment_variables": {
|
||||
"isFOS": "true",
|
||||
"isEE": "false",
|
||||
"stage": "default-foss",
|
||||
"jwt_issuer": "openreplay-default-foss",
|
||||
"sentryURL": "",
|
||||
"pg_host": "postgresql.db.svc.cluster.local",
|
||||
"pg_port": "5432",
|
||||
"pg_dbname": "postgres",
|
||||
"pg_user": "postgres",
|
||||
"pg_password": "asayerPostgres",
|
||||
"alert_ntf": "http://127.0.0.1:8000/async/alerts/notifications/%s",
|
||||
"email_signup": "http://127.0.0.1:8000/async/email_signup/%s",
|
||||
"email_funnel": "http://127.0.0.1:8000/async/funnel/%s",
|
||||
"email_basic": "http://127.0.0.1:8000/async/basic/%s",
|
||||
"assign_link": "http://127.0.0.1:8000/async/email_assignment",
|
||||
"captcha_server": "",
|
||||
"captcha_key": "",
|
||||
"sessions_bucket": "mobs",
|
||||
"sessions_region": "us-east-1",
|
||||
"put_S3_TTL": "20",
|
||||
"sourcemaps_reader": "http://0.0.0.0:9000/sourcemaps",
|
||||
"sourcemaps_bucket": "sourcemaps",
|
||||
"js_cache_bucket": "sessions-assets",
|
||||
"peers": "http://0.0.0.0:9000/assist/peers",
|
||||
"async_Token": "",
|
||||
"EMAIL_HOST": "",
|
||||
"EMAIL_PORT": "587",
|
||||
"EMAIL_USER": "",
|
||||
"EMAIL_PASSWORD": "",
|
||||
"EMAIL_USE_TLS": "true",
|
||||
"EMAIL_USE_SSL": "false",
|
||||
"EMAIL_SSL_KEY": "",
|
||||
"EMAIL_SSL_CERT": "",
|
||||
"EMAIL_FROM": "OpenReplay<do-not-reply@openreplay.com>",
|
||||
"SITE_URL": "",
|
||||
"announcement_url": "",
|
||||
"jwt_secret": "",
|
||||
"jwt_algorithm": "HS512",
|
||||
"jwt_exp_delta_seconds": "2592000",
|
||||
"S3_HOST": "",
|
||||
"S3_KEY": "",
|
||||
"S3_SECRET": "",
|
||||
"invitation_link": "/api/users/invitation?token=%s",
|
||||
"change_password_link": "/reset-password?invitation=%s&&pass=%s",
|
||||
"version_number": "1.2.0"
|
||||
},
|
||||
"lambda_timeout": 150,
|
||||
"lambda_memory_size": 400,
|
||||
"subnet_ids": [
|
||||
],
|
||||
"security_group_ids": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
67
api/.chalice/config.json
Normal file
67
api/.chalice/config.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"app_name": "parrot",
|
||||
"environment_variables": {
|
||||
},
|
||||
"stages": {
|
||||
"default-foss": {
|
||||
"api_gateway_stage": "default-fos",
|
||||
"manage_iam_role": false,
|
||||
"iam_role_arn": "",
|
||||
"autogen_policy": true,
|
||||
"environment_variables": {
|
||||
"isFOS": "true",
|
||||
"isEE": "false",
|
||||
"stage": "default-foss",
|
||||
"jwt_issuer": "openreplay-default-foss",
|
||||
"sentryURL": "",
|
||||
"pg_host": "postgresql.db.svc.cluster.local",
|
||||
"pg_port": "5432",
|
||||
"pg_dbname": "postgres",
|
||||
"pg_user": "postgres",
|
||||
"pg_password": "asayerPostgres",
|
||||
"alert_ntf": "http://127.0.0.1:8000/async/alerts/notifications/%s",
|
||||
"email_signup": "http://127.0.0.1:8000/async/email_signup/%s",
|
||||
"email_funnel": "http://127.0.0.1:8000/async/funnel/%s",
|
||||
"email_basic": "http://127.0.0.1:8000/async/basic/%s",
|
||||
"assign_link": "http://127.0.0.1:8000/async/email_assignment",
|
||||
"captcha_server": "",
|
||||
"captcha_key": "",
|
||||
"sessions_bucket": "mobs",
|
||||
"sessions_region": "us-east-1",
|
||||
"put_S3_TTL": "20",
|
||||
"sourcemaps_reader": "http://utilities-openreplay.app.svc.cluster.local:9000/sourcemaps",
|
||||
"sourcemaps_bucket": "sourcemaps",
|
||||
"js_cache_bucket": "sessions-assets",
|
||||
"peers": "http://utilities-openreplay.app.svc.cluster.local:9000/assist/%s/peers",
|
||||
"async_Token": "",
|
||||
"EMAIL_HOST": "",
|
||||
"EMAIL_PORT": "587",
|
||||
"EMAIL_USER": "",
|
||||
"EMAIL_PASSWORD": "",
|
||||
"EMAIL_USE_TLS": "true",
|
||||
"EMAIL_USE_SSL": "false",
|
||||
"EMAIL_SSL_KEY": "",
|
||||
"EMAIL_SSL_CERT": "",
|
||||
"EMAIL_FROM": "OpenReplay<do-not-reply@openreplay.com>",
|
||||
"SITE_URL": "",
|
||||
"announcement_url": "",
|
||||
"jwt_secret": "",
|
||||
"jwt_algorithm": "HS512",
|
||||
"jwt_exp_delta_seconds": "2592000",
|
||||
"S3_HOST": "",
|
||||
"S3_KEY": "",
|
||||
"S3_SECRET": "",
|
||||
"invitation_link": "/api/users/invitation?token=%s",
|
||||
"change_password_link": "/reset-password?invitation=%s&&pass=%s",
|
||||
"version_number": "1.2.0"
|
||||
},
|
||||
"lambda_timeout": 150,
|
||||
"lambda_memory_size": 400,
|
||||
"subnet_ids": [
|
||||
],
|
||||
"security_group_ids": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
api/.gitignore
vendored
8
api/.gitignore
vendored
|
|
@ -83,7 +83,7 @@ wheels/
|
|||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
Pipfile.lock
|
||||
Pipfile
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
|
@ -143,7 +143,7 @@ celerybeat-schedule
|
|||
|
||||
# Environments
|
||||
.env
|
||||
.venv/*
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
|
|
@ -174,6 +174,4 @@ logs*.txt
|
|||
SUBNETS.json
|
||||
|
||||
./chalicelib/.configs
|
||||
README/*
|
||||
.local
|
||||
/.dev/
|
||||
README/*
|
||||
|
|
@ -1 +0,0 @@
|
|||
.venv
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Accept the risk until
|
||||
# python setup tools recently fixed. Not yet available in distros.
|
||||
CVE-2023-5363 exp:2023-12-31
|
||||
|
|
@ -1,31 +1,16 @@
|
|||
FROM python:3.12-alpine AS builder
|
||||
LABEL maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
|
||||
RUN apk add --no-cache build-base
|
||||
WORKDIR /work
|
||||
COPY requirements.txt ./requirements.txt
|
||||
RUN pip install --no-cache-dir --upgrade uv && \
|
||||
export UV_SYSTEM_PYTHON=true && \
|
||||
uv pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
uv pip install --no-cache-dir --upgrade -r requirements.txt
|
||||
|
||||
FROM python:3.12-alpine
|
||||
ARG GIT_SHA
|
||||
ARG envarg
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
ENV SOURCE_MAP_VERSION=0.7.4 \
|
||||
APP_NAME=chalice \
|
||||
LISTEN_PORT=8000 \
|
||||
PRIVATE_ENDPOINTS=false \
|
||||
ENTERPRISE_BUILD=${envarg} \
|
||||
GIT_SHA=$GIT_SHA
|
||||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||
FROM python:3.6-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
RUN apk add --no-cache tini && mv env.default .env
|
||||
RUN pip install -r requirements.txt -t ./vendor --upgrade
|
||||
RUN pip install chalice==1.22.2
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ["./entrypoint.sh"]
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
ENV TINI_VERSION v0.19.0
|
||||
ARG envarg
|
||||
ENV ENTERPRISE_BUILD ${envarg}
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
CMD ./entrypoint.sh
|
||||
27
api/Dockerfile.bundle
Normal file
27
api/Dockerfile.bundle
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
FROM python:3.6-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
COPY ../utilities ./utilities
|
||||
RUN rm entrypoint.sh && rm .chalice/config.json
|
||||
RUN mv entrypoint.bundle.sh entrypoint.sh && mv .chalice/config.bundle.json .chalice/config.json
|
||||
RUN pip install -r requirements.txt -t ./vendor --upgrade
|
||||
RUN pip install chalice==1.22.2
|
||||
# Installing Nodejs
|
||||
RUN apt update && apt install -y curl && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_12.x | bash - && \
|
||||
apt install -y nodejs && \
|
||||
apt remove --purge -y curl && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
cd utilities && \
|
||||
npm install
|
||||
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
ENV TINI_VERSION v0.19.0
|
||||
ARG envarg
|
||||
ENV ENTERPRISE_BUILD ${envarg}
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
CMD ./entrypoint.sh
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# ignore .git and .cache folders
|
||||
.git
|
||||
.cache
|
||||
**/build.sh
|
||||
**/build_*.sh
|
||||
**/*deploy.sh
|
||||
Dockerfile*
|
||||
|
||||
app_alerts.py
|
||||
requirements-alerts.txt
|
||||
entrypoint_alerts.sh
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
ARG GIT_SHA
|
||||
LABEL GIT_SHA=$GIT_SHA
|
||||
|
||||
RUN apk add --no-cache build-base tini
|
||||
ARG envarg
|
||||
ENV APP_NAME=alerts \
|
||||
PG_MINCONN=1 \
|
||||
PG_MAXCONN=10 \
|
||||
LISTEN_PORT=8000 \
|
||||
PRIVATE_ENDPOINTS=true \
|
||||
GIT_SHA=$GIT_SHA \
|
||||
ENTERPRISE_BUILD=${envarg}
|
||||
|
||||
WORKDIR /work
|
||||
COPY requirements-alerts.txt ./requirements.txt
|
||||
RUN pip install --no-cache-dir --upgrade uv
|
||||
RUN uv pip install --no-cache-dir --upgrade pip setuptools wheel --system
|
||||
RUN uv pip install --no-cache-dir --upgrade -r requirements.txt --system
|
||||
|
||||
COPY . .
|
||||
RUN mv env.default .env && mv app_alerts.py app.py && mv entrypoint_alerts.sh entrypoint.sh
|
||||
|
||||
RUN adduser -u 1001 openreplay -D
|
||||
USER 1001
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD ./entrypoint.sh
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# ignore .git and .cache folders
|
||||
.git
|
||||
.cache
|
||||
**/build.sh
|
||||
**/build_*.sh
|
||||
**/*deploy.sh
|
||||
Dockerfile*
|
||||
|
||||
app.py
|
||||
entrypoint.sh
|
||||
requirements.txt
|
||||
43
api/NOTES.md
43
api/NOTES.md
|
|
@ -1,43 +0,0 @@
|
|||
#### autogenerated api frontend
|
||||
|
||||
API can autogenerate a frontend that documents, and allows to play
|
||||
with, in a limited way, its interface. Make sure you have the
|
||||
following variables inside the current `.env`:
|
||||
|
||||
```
|
||||
docs_url=/docs
|
||||
root_path=''
|
||||
```
|
||||
|
||||
If the `.env` that is in-use is based on `env.default` then it is
|
||||
already the case. Start, or restart the http server, then go to
|
||||
`https://127.0.0.1:8000/docs`. That is autogenerated documentation
|
||||
based on pydantic schema, fastapi routes, and docstrings :wink:.
|
||||
|
||||
Happy experiments, and then documentation!
|
||||
|
||||
#### psycopg3 API
|
||||
|
||||
I mis-remember the psycopg v2 vs. v3 API.
|
||||
|
||||
For the record, the expected psycopg3's async api looks like the
|
||||
following pseudo code:
|
||||
|
||||
```python
|
||||
async with app.state.postgresql.connection() as cnx:
|
||||
async with cnx.transaction():
|
||||
row = await cnx.execute("SELECT EXISTS(SELECT 1 FROM public.tenants)")
|
||||
row = await row.fetchone()
|
||||
return row["exists"]
|
||||
```
|
||||
|
||||
Mind the following:
|
||||
|
||||
- Where `app.state.postgresql` is the postgresql connection pooler.
|
||||
- Wrap explicit transaction with `async with cnx.transaction():
|
||||
foobar()`
|
||||
- Most of the time the transaction object is not used;
|
||||
- Do execute await operation against `cnx`;
|
||||
- `await cnx.execute` returns a cursor object;
|
||||
- Do the `await cursor.fetchqux...` calls against the object return by
|
||||
a call to execute.
|
||||
29
api/Pipfile
29
api/Pipfile
|
|
@ -1,29 +0,0 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
urllib3 = "==2.3.0"
|
||||
requests = "==2.32.3"
|
||||
boto3 = "==1.36.12"
|
||||
pyjwt = "==2.10.1"
|
||||
psycopg2-binary = "==2.9.10"
|
||||
psycopg = {extras = ["pool", "binary"], version = "==3.2.4"}
|
||||
clickhouse-driver = {extras = ["lz4"], version = "==0.2.9"}
|
||||
clickhouse-connect = "==0.8.15"
|
||||
elasticsearch = "==8.17.1"
|
||||
jira = "==3.8.0"
|
||||
cachetools = "==5.5.1"
|
||||
fastapi = "==0.115.8"
|
||||
uvicorn = {extras = ["standard"], version = "==0.34.0"}
|
||||
python-decouple = "==3.8"
|
||||
pydantic = {extras = ["email"], version = "==2.10.6"}
|
||||
apscheduler = "==3.11.0"
|
||||
redis = "==5.2.1"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.12"
|
||||
python_full_version = "3.12.8"
|
||||
208
api/app.py
208
api/app.py
|
|
@ -1,134 +1,110 @@
|
|||
import logging
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import psycopg_pool
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from decouple import config
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from psycopg import AsyncConnection
|
||||
from psycopg.rows import dict_row
|
||||
from starlette.responses import StreamingResponse
|
||||
import sentry_sdk
|
||||
from chalice import Chalice, Response
|
||||
from sentry_sdk import configure_scope
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.blueprints import bp_authorizers
|
||||
from chalicelib.blueprints import bp_core, bp_core_crons
|
||||
from chalicelib.blueprints.app import v1_api
|
||||
from chalicelib.blueprints import bp_core_dynamic, bp_core_dynamic_crons
|
||||
from chalicelib.blueprints.subs import bp_dashboard,bp_insights
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client, ch_client
|
||||
from crons import core_crons, core_dynamic_crons
|
||||
from routers import core, core_dynamic
|
||||
from routers.subs import insights, metrics, v1_api, health, usability_tests, spot, product_anaytics
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
loglevel = config("LOGLEVEL", default=logging.WARNING)
|
||||
print(f">Loglevel set to: {loglevel}")
|
||||
logging.basicConfig(level=loglevel)
|
||||
app = Chalice(app_name='parrot')
|
||||
app.debug = not helper.is_production() or helper.is_local()
|
||||
|
||||
sentry_sdk.init(environ["sentryURL"])
|
||||
|
||||
# Monkey-patch print for DataDog hack
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
old_tb = traceback.print_exception
|
||||
old_f = sys.stdout
|
||||
old_e = sys.stderr
|
||||
OR_SESSION_TOKEN = None
|
||||
|
||||
|
||||
class ORPYAsyncConnection(AsyncConnection):
|
||||
class F:
|
||||
def write(self, x):
|
||||
if OR_SESSION_TOKEN is not None and x != '\n' and not helper.is_local():
|
||||
old_f.write(f"[or_session_token={OR_SESSION_TOKEN}] {x}")
|
||||
else:
|
||||
old_f.write(x)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, row_factory=dict_row, **kwargs)
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
logging.info(">>>>> starting up <<<<<")
|
||||
ap_logger = logging.getLogger('apscheduler')
|
||||
ap_logger.setLevel(loglevel)
|
||||
def tb_print_exception(etype, value, tb, limit=None, file=None, chain=True):
|
||||
if OR_SESSION_TOKEN is not None and not helper.is_local():
|
||||
value = type(value)(f"[or_session_token={OR_SESSION_TOKEN}] " + str(value))
|
||||
|
||||
app.schedule = AsyncIOScheduler()
|
||||
await pg_client.init()
|
||||
await ch_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)})
|
||||
|
||||
database = {
|
||||
"host": config("pg_host", default="localhost"),
|
||||
"dbname": config("pg_dbname", default="orpy"),
|
||||
"user": config("pg_user", default="orpy"),
|
||||
"password": config("pg_password", default="orpy"),
|
||||
"port": config("pg_port", cast=int, default=5432),
|
||||
"application_name": "AIO" + config("APP_NAME", default="PY"),
|
||||
}
|
||||
|
||||
database = psycopg_pool.AsyncConnectionPool(kwargs=database, connection_class=ORPYAsyncConnection,
|
||||
min_size=config("PG_AIO_MINCONN", cast=int, default=1),
|
||||
max_size=config("PG_AIO_MAXCONN", cast=int, default=5), )
|
||||
app.state.postgresql = database
|
||||
|
||||
# App listening
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
await database.close()
|
||||
logging.info(">>>>> shutting down <<<<<")
|
||||
app.schedule.shutdown(wait=False)
|
||||
await pg_client.terminate()
|
||||
old_tb(etype, value, tb, limit, file, chain)
|
||||
|
||||
|
||||
app = FastAPI(root_path=config("root_path", default="/api"), docs_url=config("docs_url", default=""),
|
||||
redoc_url=config("redoc_url", default=""), lifespan=lifespan)
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
if helper.is_production():
|
||||
traceback.print_exception = tb_print_exception
|
||||
|
||||
sys.stdout = F()
|
||||
sys.stderr = F()
|
||||
# ---End Monkey-patch
|
||||
|
||||
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
async def or_middleware(request: Request, call_next):
|
||||
if helper.TRACK_TIME:
|
||||
now = time.time()
|
||||
def or_middleware(event, get_response):
|
||||
global OR_SESSION_TOKEN
|
||||
OR_SESSION_TOKEN = app.current_request.headers.get('vnd.openreplay.com.sid',
|
||||
app.current_request.headers.get('vnd.asayer.io.sid'))
|
||||
if "authorizer" in event.context and event.context["authorizer"] is None:
|
||||
print("Deleted user!!")
|
||||
pg_client.close()
|
||||
return Response(body={"errors": ["Deleted user"]}, status_code=403)
|
||||
|
||||
try:
|
||||
response: StreamingResponse = await call_next(request)
|
||||
except:
|
||||
logging.error(f"{request.method}: {request.url.path} FAILED!")
|
||||
raise
|
||||
if response.status_code // 100 != 2:
|
||||
logging.warning(f"{request.method}:{request.url.path} {response.status_code}!")
|
||||
if helper.TRACK_TIME:
|
||||
now = time.time() - now
|
||||
if now > 2:
|
||||
now = round(now, 2)
|
||||
logging.warning(f"Execution time: {now} s for {request.method}: {request.url.path}")
|
||||
response.headers["x-robots-tag"] = 'noindex, nofollow'
|
||||
if helper.TRACK_TIME:
|
||||
import time
|
||||
now = int(time.time() * 1000)
|
||||
response = get_response(event)
|
||||
|
||||
if response.status_code == 200 and response.body is not None and response.body.get("errors") is not None:
|
||||
if "not found" in response.body["errors"][0]:
|
||||
response = Response(status_code=404, body=response.body)
|
||||
else:
|
||||
response = Response(status_code=400, body=response.body)
|
||||
if response.status_code // 100 == 5 and helper.allow_sentry() and OR_SESSION_TOKEN is not None and not helper.is_local():
|
||||
with configure_scope() as scope:
|
||||
scope.set_tag('stage', environ["stage"])
|
||||
scope.set_tag('openReplaySessionToken', OR_SESSION_TOKEN)
|
||||
scope.set_extra("context", event.context)
|
||||
sentry_sdk.capture_exception(Exception(response.body))
|
||||
if helper.TRACK_TIME:
|
||||
print(f"Execution time: {int(time.time() * 1000) - now} ms")
|
||||
except Exception as e:
|
||||
if helper.allow_sentry() and OR_SESSION_TOKEN is not None and not helper.is_local():
|
||||
with configure_scope() as scope:
|
||||
scope.set_tag('stage', environ["stage"])
|
||||
scope.set_tag('openReplaySessionToken', OR_SESSION_TOKEN)
|
||||
scope.set_extra("context", event.context)
|
||||
sentry_sdk.capture_exception(e)
|
||||
response = Response(body={"Code": "InternalServerError",
|
||||
"Message": "An internal server error occurred [level=Fatal]."},
|
||||
status_code=500)
|
||||
pg_client.close()
|
||||
return response
|
||||
|
||||
|
||||
origins = [
|
||||
"*",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.include_router(core.public_app)
|
||||
app.include_router(core.app)
|
||||
app.include_router(core.app_apikey)
|
||||
app.include_router(core_dynamic.public_app)
|
||||
app.include_router(core_dynamic.app)
|
||||
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)
|
||||
|
||||
app.include_router(usability_tests.public_app)
|
||||
app.include_router(usability_tests.app)
|
||||
app.include_router(usability_tests.app_apikey)
|
||||
|
||||
app.include_router(spot.public_app)
|
||||
app.include_router(spot.app)
|
||||
app.include_router(spot.app_apikey)
|
||||
|
||||
app.include_router(product_anaytics.public_app)
|
||||
app.include_router(product_anaytics.app)
|
||||
app.include_router(product_anaytics.app_apikey)
|
||||
# Open source
|
||||
app.register_blueprint(bp_authorizers.app)
|
||||
app.register_blueprint(bp_core.app)
|
||||
app.register_blueprint(bp_core_crons.app)
|
||||
app.register_blueprint(bp_core_dynamic.app)
|
||||
app.register_blueprint(bp_core_dynamic_crons.app)
|
||||
app.register_blueprint(bp_dashboard.app)
|
||||
app.register_blueprint(bp_insights.app)
|
||||
app.register_blueprint(v1_api.app)
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from decouple import config
|
||||
from fastapi import FastAPI
|
||||
|
||||
from chalicelib.core.alerts import alerts_processor
|
||||
from chalicelib.utils import pg_client
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
ap_logger.info(">>>>> starting up <<<<<")
|
||||
await pg_client.init()
|
||||
app.schedule.start()
|
||||
app.schedule.add_job(id="alerts_processor", **{"func": alerts_processor.process, "trigger": "interval",
|
||||
"minutes": config("ALERTS_INTERVAL", cast=int, default=5),
|
||||
"misfire_grace_time": 20})
|
||||
|
||||
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
|
||||
ap_logger.info(">>>>> shutting down <<<<<")
|
||||
app.schedule.shutdown(wait=False)
|
||||
await pg_client.terminate()
|
||||
|
||||
|
||||
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 = FastAPI(root_path=config("root_path", default="/alerts"), docs_url=config("docs_url", default=""),
|
||||
redoc_url=config("redoc_url", default=""), lifespan=lifespan)
|
||||
|
||||
app.schedule = AsyncIOScheduler()
|
||||
ap_logger.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")}
|
||||
}}
|
||||
|
||||
|
||||
if config("LOCAL_DEV", default=False, cast=bool):
|
||||
@app.get('/trigger', tags=["private"])
|
||||
async def trigger_main_cron():
|
||||
ap_logger.info("Triggering main cron")
|
||||
alerts_processor.process()
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.security import APIKeyHeader
|
||||
from starlette import status
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from chalicelib.core import authorizers
|
||||
from schemas import CurrentAPIContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIKeyAuth(APIKeyHeader):
|
||||
def __init__(self, auto_error: bool = True):
|
||||
super(APIKeyAuth, self).__init__(name="Authorization", auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[CurrentAPIContext]:
|
||||
api_key: Optional[str] = await super(APIKeyAuth, self).__call__(request)
|
||||
r = authorizers.api_key_authorizer(api_key)
|
||||
if r is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API Key",
|
||||
)
|
||||
r["authorizer_identity"] = "api_key"
|
||||
logger.debug(r)
|
||||
request.state.authorizer_identity = "api_key"
|
||||
request.state.currentContext = CurrentAPIContext(tenantId=r["tenantId"])
|
||||
return request.state.currentContext
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
import datetime
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from decouple import config
|
||||
from fastapi import Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from starlette import status
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import authorizers, users, spot
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_current_auth_context(request: Request, jwt_payload: dict) -> schemas.CurrentContext:
|
||||
user = users.get(user_id=jwt_payload.get("userId", -1), tenant_id=jwt_payload.get("tenantId", -1))
|
||||
if user is None:
|
||||
logger.warning("User not found.")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User not found.")
|
||||
request.state.authorizer_identity = "jwt"
|
||||
request.state.currentContext = schemas.CurrentContext(tenantId=jwt_payload.get("tenantId", -1),
|
||||
userId=jwt_payload.get("userId", -1),
|
||||
email=user["email"],
|
||||
role=user["role"])
|
||||
return request.state.currentContext
|
||||
|
||||
|
||||
class JWTAuth(HTTPBearer):
|
||||
def __init__(self, auto_error: bool = True):
|
||||
super(JWTAuth, self).__init__(auto_error=auto_error)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[schemas.CurrentContext]:
|
||||
if request.url.path in ["/refresh", "/api/refresh"]:
|
||||
return await self.__process_refresh_call(request)
|
||||
|
||||
elif request.url.path in ["/spot/refresh", "/api/spot/refresh"]:
|
||||
return await self.__process_spot_refresh_call(request)
|
||||
|
||||
else:
|
||||
credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request)
|
||||
if credentials:
|
||||
if not credentials.scheme == "Bearer":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid authentication scheme.")
|
||||
jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials)
|
||||
auth_exists = jwt_payload is not None and users.auth_exists(user_id=jwt_payload.get("userId", -1),
|
||||
jwt_iat=jwt_payload.get("iat", 100))
|
||||
if jwt_payload is None \
|
||||
or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \
|
||||
or not auth_exists:
|
||||
if jwt_payload is not None:
|
||||
logger.debug(jwt_payload)
|
||||
if jwt_payload.get("iat") is None:
|
||||
logger.debug("iat is None")
|
||||
if jwt_payload.get("aud") is None:
|
||||
logger.debug("aud is None")
|
||||
if not auth_exists:
|
||||
logger.warning("not users.auth_exists")
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.")
|
||||
|
||||
if jwt_payload.get("aud", "").startswith("spot") and not request.url.path.startswith("/spot"):
|
||||
# Allow access to spot endpoints only
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Unauthorized access (spot).")
|
||||
elif jwt_payload.get("aud", "").startswith("front") and request.url.path.startswith("/spot"):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Unauthorized access endpoint reserved for Spot only.")
|
||||
|
||||
return _get_current_auth_context(request=request, jwt_payload=jwt_payload)
|
||||
|
||||
logger.warning("Invalid authorization code.")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code.")
|
||||
|
||||
async def __process_refresh_call(self, request: Request) -> schemas.CurrentContext:
|
||||
if "refreshToken" not in request.cookies:
|
||||
logger.warning("Missing refreshToken cookie.")
|
||||
jwt_payload = None
|
||||
else:
|
||||
jwt_payload = authorizers.jwt_refresh_authorizer(scheme="Bearer", token=request.cookies["refreshToken"])
|
||||
|
||||
if jwt_payload is None or jwt_payload.get("jti") is None:
|
||||
logger.warning("Null refreshToken's payload, or null JTI.")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid refresh-token or expired refresh-token.")
|
||||
auth_exists = users.refresh_auth_exists(user_id=jwt_payload.get("userId", -1),
|
||||
jwt_jti=jwt_payload["jti"])
|
||||
if not auth_exists:
|
||||
logger.warning("refreshToken's user not found.")
|
||||
logger.warning(jwt_payload)
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid refresh-token or expired refresh-token.")
|
||||
|
||||
credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request)
|
||||
if credentials:
|
||||
if not credentials.scheme == "Bearer":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid authentication scheme.")
|
||||
old_jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials,
|
||||
leeway=datetime.timedelta(
|
||||
days=config("JWT_LEEWAY_DAYS", cast=int, default=3)
|
||||
))
|
||||
if old_jwt_payload is None \
|
||||
or old_jwt_payload.get("userId") is None \
|
||||
or old_jwt_payload.get("userId") != jwt_payload.get("userId"):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid token or expired token.")
|
||||
|
||||
return _get_current_auth_context(request=request, jwt_payload=jwt_payload)
|
||||
|
||||
logger.warning("Invalid authorization code (refresh logic).")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid authorization code for refresh.")
|
||||
|
||||
async def __process_spot_refresh_call(self, request: Request) -> schemas.CurrentContext:
|
||||
if "spotRefreshToken" not in request.cookies:
|
||||
logger.warning("Missing soptRefreshToken cookie.")
|
||||
jwt_payload = None
|
||||
else:
|
||||
jwt_payload = authorizers.jwt_refresh_authorizer(scheme="Bearer", token=request.cookies["spotRefreshToken"])
|
||||
|
||||
if jwt_payload is None or jwt_payload.get("jti") is None:
|
||||
logger.warning("Null spotRefreshToken's payload, or null JTI.")
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid spotRefreshToken or expired refresh-token.")
|
||||
auth_exists = spot.refresh_auth_exists(user_id=jwt_payload.get("userId", -1),
|
||||
jwt_jti=jwt_payload["jti"])
|
||||
if not auth_exists:
|
||||
logger.warning("spotRefreshToken's user not found.")
|
||||
logger.warning(jwt_payload)
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid spotRefreshToken or expired refresh-token.")
|
||||
|
||||
credentials: HTTPAuthorizationCredentials = await super(JWTAuth, self).__call__(request)
|
||||
if credentials:
|
||||
if not credentials.scheme == "Bearer":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid spot-authentication scheme.")
|
||||
old_jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials,
|
||||
leeway=datetime.timedelta(
|
||||
days=config("JWT_LEEWAY_DAYS", cast=int, default=3)
|
||||
))
|
||||
if old_jwt_payload is None \
|
||||
or old_jwt_payload.get("userId") is None \
|
||||
or old_jwt_payload.get("userId") != jwt_payload.get("userId"):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid spot-token or expired token.")
|
||||
|
||||
return _get_current_auth_context(request=request, jwt_payload=jwt_payload)
|
||||
|
||||
logger.warning("Invalid authorization code (spot-refresh logic).")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid authorization code for spot-refresh.")
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import logging
|
||||
|
||||
from fastapi import Request
|
||||
from starlette import status
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import projects
|
||||
from or_dependencies import OR_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectAuthorizer:
|
||||
def __init__(self, project_identifier):
|
||||
self.project_identifier: str = project_identifier
|
||||
|
||||
async def __call__(self, request: Request) -> None:
|
||||
if len(request.path_params.keys()) == 0 or request.path_params.get(self.project_identifier) is None:
|
||||
return
|
||||
current_user: schemas.CurrentContext = await OR_context(request)
|
||||
value = request.path_params[self.project_identifier]
|
||||
current_project = None
|
||||
if self.project_identifier == "projectId" \
|
||||
and (isinstance(value, int) or isinstance(value, str) and value.isnumeric()):
|
||||
current_project = projects.get_project(project_id=value, tenant_id=current_user.tenant_id)
|
||||
elif self.project_identifier == "projectKey":
|
||||
current_project = projects.get_by_project_key(project_key=value)
|
||||
|
||||
if current_project is None:
|
||||
logger.debug(f"unauthorized project {self.project_identifier}:{value}")
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="project not found.")
|
||||
else:
|
||||
current_project = schemas.ProjectContext(projectId=current_project["projectId"],
|
||||
projectKey=current_project["projectKey"],
|
||||
platform=current_project["platform"],
|
||||
name=current_project["name"])
|
||||
request.state.currentContext.project = current_project
|
||||
73
api/build.sh
73
api/build.sh
|
|
@ -7,85 +7,32 @@
|
|||
|
||||
# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh <ee>
|
||||
|
||||
# Helper function
|
||||
exit_err() {
|
||||
err_code=$1
|
||||
if [[ $err_code != 0 ]]; then
|
||||
exit $err_code
|
||||
fi
|
||||
}
|
||||
|
||||
source ../scripts/lib/_docker.sh
|
||||
ARCH=${ARCH:-'amd64'}
|
||||
|
||||
environment=$1
|
||||
git_sha=$(git rev-parse --short HEAD)
|
||||
image_tag=${IMAGE_TAG:-git_sha}
|
||||
git_sha1=${IMAGE_TAG:-$(git rev-parse HEAD)}
|
||||
envarg="default-foss"
|
||||
chart="chalice"
|
||||
check_prereq() {
|
||||
which docker || {
|
||||
echo "Docker not installed, please install docker."
|
||||
exit 1
|
||||
exit=1
|
||||
}
|
||||
return
|
||||
[[ exit -eq 1 ]] && exit 1
|
||||
}
|
||||
|
||||
[[ $1 == ee ]] && ee=true
|
||||
[[ $PATCH -eq 1 ]] && {
|
||||
image_tag="$(grep -ER ^.ppVersion ../scripts/helmcharts/openreplay/charts/$chart | xargs | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
[[ $ee == "true" ]] && {
|
||||
image_tag="${image_tag}-ee"
|
||||
}
|
||||
}
|
||||
update_helm_release() {
|
||||
[[ $ee == "true" ]] && return
|
||||
HELM_TAG="$(grep -iER ^version ../scripts/helmcharts/openreplay/charts/$chart | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
# Update the chart version
|
||||
sed -i "s#^version.*#version: $HELM_TAG# g" ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml
|
||||
# Update image tags
|
||||
sed -i "s#ppVersion.*#ppVersion: \"$image_tag\"#g" ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml
|
||||
# Commit the changes
|
||||
git add ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml
|
||||
git commit -m "chore(helm): Updating $chart image release"
|
||||
}
|
||||
|
||||
function build_api() {
|
||||
destination="_api"
|
||||
[[ $1 == "ee" ]] && {
|
||||
destination="_api_ee"
|
||||
}
|
||||
[[ -d ../${destination} ]] && {
|
||||
echo "Removing previous build cache"
|
||||
rm -rf ../${destination}
|
||||
}
|
||||
cp -R ../api ../${destination}
|
||||
cd ../${destination} || exit_err 100
|
||||
function build_api(){
|
||||
tag=""
|
||||
# Copy enterprise code
|
||||
[[ $1 == "ee" ]] && {
|
||||
cp -rf ../ee/api/* ./
|
||||
cp -rf ../ee/api/.chalice/* ./.chalice/
|
||||
envarg="default-ee"
|
||||
tag="ee-"
|
||||
}
|
||||
mv Dockerfile.dockerignore .dockerignore
|
||||
docker build -f ./Dockerfile --platform linux/${ARCH} --build-arg envarg=$envarg --build-arg GIT_SHA=$git_sha -t ${DOCKER_REPO:-'local'}/${IMAGE_NAME:-'chalice'}:${image_tag} .
|
||||
cd ../api || exit_err 100
|
||||
rm -rf ../${destination}
|
||||
docker build -f ./Dockerfile --build-arg envarg=$envarg -t ${DOCKER_REPO:-'local'}/chalice:${git_sha1} .
|
||||
[[ $PUSH_IMAGE -eq 1 ]] && {
|
||||
docker push ${DOCKER_REPO:-'local'}/${IMAGE_NAME:-'chalice'}:${image_tag}
|
||||
docker tag ${DOCKER_REPO:-'local'}/${IMAGE_NAME:-'chalice'}:${image_tag} ${DOCKER_REPO:-'local'}/chalice:${tag}latest
|
||||
docker push ${DOCKER_REPO:-'local'}/${IMAGE_NAME:-'chalice'}:${tag}latest
|
||||
docker push ${DOCKER_REPO:-'local'}/chalice:${git_sha1}
|
||||
docker tag ${DOCKER_REPO:-'local'}/chalice:${git_sha1} ${DOCKER_REPO:-'local'}/chalice:${tag}latest
|
||||
docker push ${DOCKER_REPO:-'local'}/chalice:${tag}latest
|
||||
}
|
||||
[[ $SIGN_IMAGE -eq 1 ]] && {
|
||||
cosign sign --key $SIGN_KEY ${DOCKER_REPO:-'local'}/${IMAGE_NAME:-'chalice'}:${image_tag}
|
||||
}
|
||||
echo "api docker build completed"
|
||||
}
|
||||
|
||||
check_prereq
|
||||
build_api $environment
|
||||
echo buil_complete
|
||||
if [[ $PATCH -eq 1 ]]; then
|
||||
update_helm_release
|
||||
fi
|
||||
build_api $1
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script to build alerts module
|
||||
# flags to accept:
|
||||
# envarg: build for enterprise edition.
|
||||
# Default will be OSS build.
|
||||
|
||||
# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh <ee>
|
||||
|
||||
git_sha=$(git rev-parse --short HEAD)
|
||||
image_tag=${IMAGE_TAG:-git_sha}
|
||||
envarg="default-foss"
|
||||
source ../scripts/lib/_docker.sh
|
||||
check_prereq() {
|
||||
which docker || {
|
||||
echo "Docker not installed, please install docker."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
[[ $1 == ee ]] && ee=true
|
||||
[[ $PATCH -eq 1 ]] && {
|
||||
image_tag="$(grep -ER ^.ppVersion ../scripts/helmcharts/openreplay/charts/$chart | xargs | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
[[ $ee == "true" ]] && {
|
||||
image_tag="${image_tag}-ee"
|
||||
}
|
||||
}
|
||||
update_helm_release() {
|
||||
chart=$1
|
||||
HELM_TAG="$(grep -iER ^version ../scripts/helmcharts/openreplay/charts/$chart | awk '{print $2}' | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
# Update the chart version
|
||||
sed -i "s#^version.*#version: $HELM_TAG# g" ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml
|
||||
# Update image tags
|
||||
sed -i "s#ppVersion.*#ppVersion: \"$image_tag\"#g" ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml
|
||||
# Commit the changes
|
||||
git add ../scripts/helmcharts/openreplay/charts/$chart/Chart.yaml
|
||||
git commit -m "chore(helm): Updating $chart image release"
|
||||
}
|
||||
|
||||
function build_alerts() {
|
||||
destination="_alerts"
|
||||
[[ $1 == "ee" ]] && {
|
||||
destination="_alerts_ee"
|
||||
}
|
||||
cp -R ../api ../${destination}
|
||||
cd ../${destination}
|
||||
tag=""
|
||||
# Copy enterprise code
|
||||
[[ $1 == "ee" ]] && {
|
||||
cp -rf ../ee/api/* ./
|
||||
envarg="default-ee"
|
||||
tag="ee-"
|
||||
}
|
||||
mv Dockerfile_alerts.dockerignore .dockerignore
|
||||
docker build -f ./Dockerfile_alerts --platform linux/${ARCH:-"amd64"} --build-arg envarg=$envarg --build-arg GIT_SHA=$git_sha -t ${DOCKER_REPO:-'local'}/alerts:${image_tag} .
|
||||
cd ../api
|
||||
rm -rf ../${destination}
|
||||
[[ $PUSH_IMAGE -eq 1 ]] && {
|
||||
docker push ${DOCKER_REPO:-'local'}/alerts:${image_tag}
|
||||
docker tag ${DOCKER_REPO:-'local'}/alerts:${image_tag} ${DOCKER_REPO:-'local'}/alerts:${tag}latest
|
||||
docker push ${DOCKER_REPO:-'local'}/alerts:${tag}latest
|
||||
}
|
||||
[[ $SIGN_IMAGE -eq 1 ]] && {
|
||||
cosign sign --key $SIGN_KEY ${DOCKER_REPO:-'local'}/alerts:${image_tag}
|
||||
}
|
||||
echo "completed alerts build"
|
||||
}
|
||||
|
||||
check_prereq
|
||||
build_alerts $1
|
||||
if [[ $PATCH -eq 1 ]]; then
|
||||
update_helm_release alerts
|
||||
fi
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script to build crons module
|
||||
# flags to accept:
|
||||
# envarg: build for enterprise edition.
|
||||
# Default will be OSS build.
|
||||
|
||||
# Usage: IMAGE_TAG=latest DOCKER_REPO=myDockerHubID bash build.sh <ee>
|
||||
|
||||
git_sha1=${IMAGE_TAG:-$(git rev-parse HEAD)}
|
||||
envarg="default-foss"
|
||||
source ../scripts/lib/_docker.sh
|
||||
check_prereq() {
|
||||
which docker || {
|
||||
echo "Docker not installed, please install docker."
|
||||
exit=1
|
||||
}
|
||||
[[ exit -eq 1 ]] && exit 1
|
||||
}
|
||||
|
||||
function build_crons() {
|
||||
destination="_crons_ee"
|
||||
cp -R ../api ../${destination}
|
||||
cd ../${destination}
|
||||
tag=""
|
||||
# Copy enterprise code
|
||||
|
||||
cp -rf ../ee/api/* ./
|
||||
envarg="default-ee"
|
||||
tag="ee-"
|
||||
mv Dockerfile_crons.dockerignore .dockerignore
|
||||
docker build -f ./Dockerfile_crons --platform=linux/${ARCH:-'amd64'} --build-arg envarg=$envarg -t ${DOCKER_REPO:-'local'}/crons:${git_sha1} .
|
||||
cd ../api
|
||||
rm -rf ../${destination}
|
||||
[[ $PUSH_IMAGE -eq 1 ]] && {
|
||||
docker push ${DOCKER_REPO:-'local'}/crons:${git_sha1}
|
||||
docker tag ${DOCKER_REPO:-'local'}/crons:${git_sha1} ${DOCKER_REPO:-'local'}/crons:${tag}latest
|
||||
docker push ${DOCKER_REPO:-'local'}/crons:${tag}latest
|
||||
}
|
||||
[[ $SIGN_IMAGE -eq 1 ]] && {
|
||||
cosign sign --key $SIGN_KEY ${DOCKER_REPO:-'local'}/crons:${git_sha1}
|
||||
}
|
||||
echo "completed crons build"
|
||||
}
|
||||
|
||||
check_prereq
|
||||
[[ $1 == "ee" ]] && {
|
||||
build_crons $1
|
||||
} || {
|
||||
echo -e "Crons is only for ee. Rerun the script using \n bash $0 ee"
|
||||
exit 100
|
||||
}
|
||||
104
api/chalicelib/_overrides.py
Normal file
104
api/chalicelib/_overrides.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
from chalice import Chalice, CORSConfig
|
||||
from chalicelib.blueprints import bp_authorizers
|
||||
from chalicelib.core import authorizers
|
||||
|
||||
import sched
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
from croniter import croniter
|
||||
|
||||
base_time = datetime.now(pytz.utc)
|
||||
|
||||
cors_config = CORSConfig(
|
||||
allow_origin='*',
|
||||
allow_headers=['vnd.openreplay.com.sid', 'vnd.asayer.io.sid'],
|
||||
# max_age=600,
|
||||
# expose_headers=['X-Special-Header'],
|
||||
allow_credentials=True
|
||||
)
|
||||
|
||||
|
||||
def chalice_app(app):
|
||||
def app_route(self, path, **kwargs):
|
||||
kwargs.setdefault('cors', cors_config)
|
||||
kwargs.setdefault('authorizer', bp_authorizers.jwt_authorizer)
|
||||
handler_type = 'route'
|
||||
name = kwargs.pop('name', None)
|
||||
registration_kwargs = {'path': path, 'kwargs': kwargs, 'authorizer': kwargs.get("authorizer")}
|
||||
|
||||
def _register_handler(user_handler):
|
||||
handler_name = name
|
||||
if handler_name is None:
|
||||
handler_name = user_handler.__name__
|
||||
if registration_kwargs is not None:
|
||||
kwargs = registration_kwargs
|
||||
else:
|
||||
kwargs = {}
|
||||
|
||||
if kwargs['authorizer'] == bp_authorizers.jwt_authorizer \
|
||||
or kwargs['authorizer'] == bp_authorizers.api_key_authorizer:
|
||||
def _user_handler(context=None, **args):
|
||||
if context is not None:
|
||||
args['context'] = context
|
||||
else:
|
||||
authorizer_context = app.current_request.context['authorizer']
|
||||
if kwargs['authorizer'] == bp_authorizers.jwt_authorizer:
|
||||
args['context'] = authorizers.jwt_context(authorizer_context)
|
||||
else:
|
||||
args['context'] = authorizer_context
|
||||
return user_handler(**args)
|
||||
|
||||
wrapped = self._wrap_handler(handler_type, handler_name, _user_handler)
|
||||
self._register_handler(handler_type, handler_name, _user_handler, wrapped, kwargs)
|
||||
else:
|
||||
wrapped = self._wrap_handler(handler_type, handler_name, user_handler)
|
||||
self._register_handler(handler_type, handler_name, user_handler, wrapped, kwargs)
|
||||
return wrapped
|
||||
|
||||
return _register_handler
|
||||
|
||||
app.route = app_route.__get__(app, Chalice)
|
||||
|
||||
def app_schedule(self, expression, name=None, description=''):
|
||||
handler_type = 'schedule'
|
||||
registration_kwargs = {'expression': expression,
|
||||
'description': description}
|
||||
|
||||
def _register_handler(user_handler):
|
||||
handler_name = name
|
||||
if handler_name is None:
|
||||
handler_name = user_handler.__name__
|
||||
kwargs = registration_kwargs
|
||||
cron_expression = kwargs["expression"].to_string()[len("cron("):-1]
|
||||
if len(cron_expression.split(" ")) > 5:
|
||||
cron_expression = " ".join(cron_expression.split(" ")[:-1])
|
||||
cron_expression = cron_expression.replace("?", "*")
|
||||
cron_shell(user_handler, cron_expression)
|
||||
|
||||
wrapped = self._wrap_handler(handler_type, handler_name, user_handler)
|
||||
self._register_handler(handler_type, handler_name, user_handler, wrapped, kwargs)
|
||||
return wrapped
|
||||
|
||||
return _register_handler
|
||||
|
||||
app.schedule = app_schedule.__get__(app, Chalice)
|
||||
|
||||
def spawn(function, args):
|
||||
th = threading.Thread(target=function, kwargs=args)
|
||||
th.setDaemon(True)
|
||||
th.start()
|
||||
|
||||
def cron_shell(function, cron_expression):
|
||||
def to_start():
|
||||
scheduler = sched.scheduler(time.time, time.sleep)
|
||||
citer = croniter(cron_expression, base_time)
|
||||
while True:
|
||||
next_execution = citer.get_next(datetime)
|
||||
print(f"{function.__name__} next execution: {next_execution}")
|
||||
scheduler.enterabs(next_execution.timestamp(), 1, function, argument=(None,))
|
||||
scheduler.run()
|
||||
print(f"{function.__name__} executed: {next_execution}")
|
||||
|
||||
spawn(to_start, None)
|
||||
127
api/chalicelib/blueprints/app/v1_api.py
Normal file
127
api/chalicelib/blueprints/app/v1_api.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
from chalice import Blueprint, Response
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.blueprints import bp_authorizers
|
||||
from chalicelib.core import sessions, events, jobs, projects
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/users/{userId}/sessions', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_user_sessions(projectKey, userId, context):
|
||||
projectId = projects.get_internal_project_id(projectKey)
|
||||
params = app.current_request.query_params
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return {
|
||||
'data': sessions.get_user_sessions(
|
||||
project_id=projectId,
|
||||
user_id=userId,
|
||||
start_date=params.get('start_date'),
|
||||
end_date=params.get('end_date')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/sessions/{sessionId}/events', methods=['GET'],
|
||||
authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_session_events(projectKey, sessionId, context):
|
||||
projectId = projects.get_internal_project_id(projectKey)
|
||||
return {
|
||||
'data': events.get_by_sessionId2_pg(
|
||||
project_id=projectId,
|
||||
session_id=sessionId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/users/{userId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_user_details(projectKey, userId, context):
|
||||
projectId = projects.get_internal_project_id(projectKey)
|
||||
return {
|
||||
'data': sessions.get_session_user(
|
||||
project_id=projectId,
|
||||
user_id=userId
|
||||
)
|
||||
}
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/users/{userId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def schedule_to_delete_user_data(projectKey, userId, context):
|
||||
projectId = projects.get_internal_project_id(projectKey)
|
||||
data = app.current_request.json_body
|
||||
|
||||
data["action"] = "delete_user_data"
|
||||
data["reference_id"] = userId
|
||||
data["description"] = f"Delete user sessions of userId = {userId}"
|
||||
data["start_at"] = TimeUTC.to_human_readable(TimeUTC.midnight(1))
|
||||
record = jobs.create(project_id=projectId, data=data)
|
||||
return {
|
||||
'data': record
|
||||
}
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/jobs', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_jobs(projectKey, context):
|
||||
projectId = projects.get_internal_project_id(projectKey)
|
||||
return {
|
||||
'data': jobs.get_all(project_id=projectId)
|
||||
}
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/jobs/{jobId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_job(projectKey, jobId, context):
|
||||
return {
|
||||
'data': jobs.get(job_id=jobId)
|
||||
}
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/v1/{projectKey}/jobs/{jobId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def cancel_job(projectKey, jobId, context):
|
||||
job = jobs.get(job_id=jobId)
|
||||
job_not_found = len(job.keys()) == 0
|
||||
|
||||
if job_not_found or job["status"] == jobs.JobStatus.COMPLETED or job["status"] == jobs.JobStatus.CANCELLED:
|
||||
return Response(status_code=501, body="The request job has already been canceled/completed (or was not found).")
|
||||
|
||||
job["status"] = "cancelled"
|
||||
return {
|
||||
'data': jobs.update(job_id=jobId, job=job)
|
||||
}
|
||||
|
||||
@app.route('/v1/projects', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_projects(context):
|
||||
records = projects.get_projects(tenant_id=context['tenantId'])
|
||||
for record in records:
|
||||
del record['projectId']
|
||||
|
||||
return {
|
||||
'data': records
|
||||
}
|
||||
|
||||
|
||||
@app.route('/v1/projects/{projectKey}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_project(projectKey, context):
|
||||
return {
|
||||
'data': projects.get_project_by_key(tenant_id=context['tenantId'], project_key=projectKey)
|
||||
}
|
||||
|
||||
|
||||
@app.route('/v1/projects', methods=['POST'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def create_project(context):
|
||||
data = app.current_request.json_body
|
||||
record = projects.create(
|
||||
tenant_id=context['tenantId'],
|
||||
user_id=None,
|
||||
data=data,
|
||||
skip_authorization=True
|
||||
)
|
||||
del record['data']['projectId']
|
||||
return record
|
||||
37
api/chalicelib/blueprints/bp_authorizers.py
Normal file
37
api/chalicelib/blueprints/bp_authorizers.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from chalice import Blueprint, AuthResponse
|
||||
from chalicelib.core import authorizers
|
||||
|
||||
from chalicelib.core import users
|
||||
|
||||
app = Blueprint(__name__)
|
||||
|
||||
|
||||
@app.authorizer()
|
||||
def api_key_authorizer(auth_request):
|
||||
r = authorizers.api_key_authorizer(auth_request.token)
|
||||
if r is None:
|
||||
return AuthResponse(routes=[], principal_id=None)
|
||||
r["authorizer_identity"] = "api_key"
|
||||
print(r)
|
||||
return AuthResponse(
|
||||
routes=['*'],
|
||||
principal_id=r['tenantId'],
|
||||
context=r
|
||||
)
|
||||
|
||||
|
||||
@app.authorizer(ttl_seconds=60)
|
||||
def jwt_authorizer(auth_request):
|
||||
jwt_payload = authorizers.jwt_authorizer(auth_request.token)
|
||||
if jwt_payload is None \
|
||||
or jwt_payload.get("iat") is None or jwt_payload.get("aud") is None \
|
||||
or not users.auth_exists(user_id=jwt_payload["userId"], tenant_id=jwt_payload["tenantId"],
|
||||
jwt_iat=jwt_payload["iat"], jwt_aud=jwt_payload["aud"]):
|
||||
return AuthResponse(routes=[], principal_id=None)
|
||||
jwt_payload["authorizer_identity"] = "jwt"
|
||||
print(jwt_payload)
|
||||
return AuthResponse(
|
||||
routes=['*'],
|
||||
principal_id=jwt_payload['userId'],
|
||||
context=jwt_payload
|
||||
)
|
||||
899
api/chalicelib/blueprints/bp_core.py
Normal file
899
api/chalicelib/blueprints/bp_core.py
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
from chalicelib.utils.helper import environ
|
||||
|
||||
from chalice import Blueprint
|
||||
from chalice import Response
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.blueprints import bp_authorizers
|
||||
from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assignments, projects, \
|
||||
sessions_metas, alerts, funnels, issues, integrations_manager, errors_favorite_viewed, metadata, \
|
||||
log_tool_elasticsearch, log_tool_datadog, \
|
||||
log_tool_stackdriver, reset_password, sessions_favorite_viewed, \
|
||||
log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, errors, sessions, \
|
||||
log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \
|
||||
assist, heatmaps
|
||||
from chalicelib.core.collaboration_slack import Slack
|
||||
from chalicelib.utils import email_helper
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/favorite', methods=['GET'])
|
||||
def get_favorite_sessions2(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
|
||||
return {
|
||||
'data': sessions.get_favorite_sessions(project_id=projectId, user_id=context["userId"], include_viewed=True)
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}', methods=['GET'])
|
||||
def get_session2(projectId, sessionId, context):
|
||||
data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True, user_id=context["userId"],
|
||||
include_fav_viewed=True, group_metadata=True)
|
||||
if data is None:
|
||||
return {"errors": ["session not found"]}
|
||||
|
||||
sessions_favorite_viewed.view_session(project_id=projectId, user_id=context['userId'], session_id=sessionId)
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}/favorite', methods=['GET'])
|
||||
def add_remove_favorite_session2(projectId, sessionId, context):
|
||||
return {
|
||||
"data": sessions_favorite_viewed.favorite_session(project_id=projectId, user_id=context['userId'],
|
||||
session_id=sessionId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}/assign', methods=['GET'])
|
||||
def assign_session(projectId, sessionId, context):
|
||||
data = sessions_assignments.get_by_session(project_id=projectId, session_id=sessionId,
|
||||
tenant_id=context['tenantId'],
|
||||
user_id=context["userId"])
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}/errors/{errorId}/sourcemaps', methods=['GET'])
|
||||
def get_error_trace(projectId, sessionId, errorId, context):
|
||||
data = errors.get_trace(project_id=projectId, error_id=errorId)
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}/assign/{issueId}', methods=['GET'])
|
||||
def assign_session(projectId, sessionId, issueId, context):
|
||||
data = sessions_assignments.get(project_id=projectId, session_id=sessionId, assignment_id=issueId,
|
||||
tenant_id=context['tenantId'], user_id=context["userId"])
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', methods=['POST', 'PUT'])
|
||||
def comment_assignment(projectId, sessionId, issueId, context):
|
||||
data = app.current_request.json_body
|
||||
data = sessions_assignments.comment(tenant_id=context['tenantId'], project_id=projectId,
|
||||
session_id=sessionId, assignment_id=issueId,
|
||||
user_id=context["userId"], message=data["message"])
|
||||
if "errors" in data.keys():
|
||||
return data
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/events/search', methods=['GET'])
|
||||
def events_search(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
return {"data": []}
|
||||
|
||||
q = params.get('q', '')
|
||||
if len(q) == 0:
|
||||
return {"data": []}
|
||||
result = events.search_pg2(q, params.get('type', ''), project_id=projectId, source=params.get('source'),
|
||||
key=params.get("key"))
|
||||
return result
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions/search2', methods=['POST'])
|
||||
def sessions_search2(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
data = sessions.search2_pg(data, projectId, user_id=context["userId"])
|
||||
return {'data': data}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions/filters', methods=['GET'])
|
||||
def session_filter_values(projectId, context):
|
||||
return {'data': sessions_metas.get_key_values(projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions/filters/top', methods=['GET'])
|
||||
def session_top_filter_values(projectId, context):
|
||||
return {'data': sessions_metas.get_top_key_values(projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions/filters/search', methods=['GET'])
|
||||
def get_session_filters_meta(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
return {"data": []}
|
||||
|
||||
meta_type = params.get('type', '')
|
||||
if len(meta_type) == 0:
|
||||
return {"data": []}
|
||||
q = params.get('q', '')
|
||||
if len(q) == 0:
|
||||
return {"data": []}
|
||||
return sessions_metas.search(project_id=projectId, meta_type=meta_type, text=q)
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/{integration}/notify/{integrationId}/{source}/{sourceId}',
|
||||
methods=['POST', 'PUT'])
|
||||
def integration_notify(projectId, integration, integrationId, source, sourceId, context):
|
||||
data = app.current_request.json_body
|
||||
comment = None
|
||||
if "comment" in data:
|
||||
comment = data["comment"]
|
||||
if integration == "slack":
|
||||
args = {"tenant_id": context["tenantId"],
|
||||
"user": context['email'], "comment": comment, "project_id": projectId,
|
||||
"integration_id": integrationId}
|
||||
if source == "sessions":
|
||||
return Slack.share_session(session_id=sourceId, **args)
|
||||
elif source == "errors":
|
||||
return Slack.share_error(error_id=sourceId, **args)
|
||||
return {"data": None}
|
||||
|
||||
|
||||
@app.route('/integrations/sentry', methods=['GET'])
|
||||
def get_all_sentry(context):
|
||||
return {"data": log_tool_sentry.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sentry', methods=['GET'])
|
||||
def get_sentry(projectId, context):
|
||||
return {"data": log_tool_sentry.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sentry', methods=['POST', 'PUT'])
|
||||
def add_edit_sentry(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_sentry.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sentry', methods=['DELETE'])
|
||||
def delete_sentry(projectId, context):
|
||||
return {"data": log_tool_sentry.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sentry/events/{eventId}', methods=['GET'])
|
||||
def proxy_sentry(projectId, eventId, context):
|
||||
return {"data": log_tool_sentry.proxy_get(tenant_id=context["tenantId"], project_id=projectId, event_id=eventId)}
|
||||
|
||||
|
||||
@app.route('/integrations/datadog', methods=['GET'])
|
||||
def get_all_datadog(context):
|
||||
return {"data": log_tool_datadog.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/datadog', methods=['GET'])
|
||||
def get_datadog(projectId, context):
|
||||
return {"data": log_tool_datadog.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/datadog', methods=['POST', 'PUT'])
|
||||
def add_edit_datadog(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_datadog.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/datadog', methods=['DELETE'])
|
||||
def delete_datadog(projectId, context):
|
||||
return {"data": log_tool_datadog.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/stackdriver', methods=['GET'])
|
||||
def get_all_stackdriver(context):
|
||||
return {"data": log_tool_stackdriver.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/stackdriver', methods=['GET'])
|
||||
def get_stackdriver(projectId, context):
|
||||
return {"data": log_tool_stackdriver.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/stackdriver', methods=['POST', 'PUT'])
|
||||
def add_edit_stackdriver(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_stackdriver.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/stackdriver', methods=['DELETE'])
|
||||
def delete_stackdriver(projectId, context):
|
||||
return {"data": log_tool_stackdriver.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/newrelic', methods=['GET'])
|
||||
def get_all_newrelic(context):
|
||||
return {"data": log_tool_newrelic.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/newrelic', methods=['GET'])
|
||||
def get_newrelic(projectId, context):
|
||||
return {"data": log_tool_newrelic.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/newrelic', methods=['POST', 'PUT'])
|
||||
def add_edit_newrelic(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_newrelic.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/newrelic', methods=['DELETE'])
|
||||
def delete_newrelic(projectId, context):
|
||||
return {"data": log_tool_newrelic.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/rollbar', methods=['GET'])
|
||||
def get_all_rollbar(context):
|
||||
return {"data": log_tool_rollbar.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/rollbar', methods=['GET'])
|
||||
def get_rollbar(projectId, context):
|
||||
return {"data": log_tool_rollbar.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/rollbar', methods=['POST', 'PUT'])
|
||||
def add_edit_rollbar(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_rollbar.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/rollbar', methods=['DELETE'])
|
||||
def delete_datadog(projectId, context):
|
||||
return {"data": log_tool_rollbar.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/bugsnag/list_projects', methods=['POST'])
|
||||
def list_projects_bugsnag(context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": log_tool_bugsnag.list_projects(auth_token=data["authorizationToken"])}
|
||||
|
||||
|
||||
@app.route('/integrations/bugsnag', methods=['GET'])
|
||||
def get_all_bugsnag(context):
|
||||
return {"data": log_tool_bugsnag.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/bugsnag', methods=['GET'])
|
||||
def get_bugsnag(projectId, context):
|
||||
return {"data": log_tool_bugsnag.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/bugsnag', methods=['POST', 'PUT'])
|
||||
def add_edit_bugsnag(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_bugsnag.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/bugsnag', methods=['DELETE'])
|
||||
def delete_bugsnag(projectId, context):
|
||||
return {"data": log_tool_bugsnag.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/cloudwatch/list_groups', methods=['POST'])
|
||||
def list_groups_cloudwatch(context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": log_tool_cloudwatch.list_log_groups(aws_access_key_id=data["awsAccessKeyId"],
|
||||
aws_secret_access_key=data["awsSecretAccessKey"],
|
||||
region=data["region"])}
|
||||
|
||||
|
||||
@app.route('/integrations/cloudwatch', methods=['GET'])
|
||||
def get_all_cloudwatch(context):
|
||||
return {"data": log_tool_cloudwatch.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/cloudwatch', methods=['GET'])
|
||||
def get_cloudwatch(projectId, context):
|
||||
return {"data": log_tool_cloudwatch.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/cloudwatch', methods=['POST', 'PUT'])
|
||||
def add_edit_cloudwatch(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_cloudwatch.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/cloudwatch', methods=['DELETE'])
|
||||
def delete_cloudwatch(projectId, context):
|
||||
return {"data": log_tool_cloudwatch.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/elasticsearch', methods=['GET'])
|
||||
def get_all_elasticsearch(context):
|
||||
return {"data": log_tool_elasticsearch.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/elasticsearch', methods=['GET'])
|
||||
def get_elasticsearch(projectId, context):
|
||||
return {"data": log_tool_elasticsearch.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/elasticsearch/test', methods=['POST'])
|
||||
def test_elasticsearch_connection(context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": log_tool_elasticsearch.ping(tenant_id=context["tenantId"], **data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/elasticsearch', methods=['POST', 'PUT'])
|
||||
def add_edit_elasticsearch(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_elasticsearch.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/elasticsearch', methods=['DELETE'])
|
||||
def delete_elasticsearch(projectId, context):
|
||||
return {"data": log_tool_elasticsearch.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/sumologic', methods=['GET'])
|
||||
def get_all_sumologic(context):
|
||||
return {"data": log_tool_sumologic.get_all(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sumologic', methods=['GET'])
|
||||
def get_sumologic(projectId, context):
|
||||
return {"data": log_tool_sumologic.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sumologic', methods=['POST', 'PUT'])
|
||||
def add_edit_sumologic(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": log_tool_sumologic.add_edit(tenant_id=context["tenantId"], project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/integrations/sumologic', methods=['DELETE'])
|
||||
def delete_sumologic(projectId, context):
|
||||
return {"data": log_tool_sumologic.delete(tenant_id=context["tenantId"], project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/integrations/issues', methods=['GET'])
|
||||
def get_integration_status(context):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return {"data": {}}
|
||||
return {"data": integration.get_obfuscated()}
|
||||
|
||||
|
||||
@app.route('/integrations/jira', methods=['POST', 'PUT'])
|
||||
def add_edit_jira_cloud(context):
|
||||
data = app.current_request.json_body
|
||||
error, integration = integrations_manager.get_integration(tool=integration_jira_cloud.PROVIDER,
|
||||
tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.add_edit(data=data)}
|
||||
|
||||
|
||||
@app.route('/integrations/github', methods=['POST', 'PUT'])
|
||||
def add_edit_github(context):
|
||||
data = app.current_request.json_body
|
||||
error, integration = integrations_manager.get_integration(tool=integration_github.PROVIDER,
|
||||
tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.add_edit(data=data)}
|
||||
|
||||
|
||||
@app.route('/integrations/issues', methods=['DELETE'])
|
||||
def delete_default_issue_tracking_tool(context):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.delete()}
|
||||
|
||||
|
||||
@app.route('/integrations/jira', methods=['DELETE'])
|
||||
def delete_jira_cloud(context):
|
||||
error, integration = integrations_manager.get_integration(tool=integration_jira_cloud.PROVIDER,
|
||||
tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.delete()}
|
||||
|
||||
|
||||
@app.route('/integrations/github', methods=['DELETE'])
|
||||
def delete_github(context):
|
||||
error, integration = integrations_manager.get_integration(tool=integration_github.PROVIDER,
|
||||
tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.delete()}
|
||||
|
||||
|
||||
@app.route('/integrations/issues/list_projects', methods=['GET'])
|
||||
def get_all_issue_tracking_projects(context):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
data = integration.issue_handler.get_projects()
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.route('/integrations/issues/{integrationProjectId}', methods=['GET'])
|
||||
def get_integration_metadata(integrationProjectId, context):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
data = integration.issue_handler.get_metas(integrationProjectId)
|
||||
if "errors" in data.keys():
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.route('/{projectId}/assignments', methods=['GET'])
|
||||
def get_all_assignments(projectId, context):
|
||||
data = sessions_assignments.get_all(project_id=projectId, user_id=context["userId"])
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sessions2/{sessionId}/assign/projects/{integrationProjectId}', methods=['POST', 'PUT'])
|
||||
def create_issue_assignment(projectId, sessionId, integrationProjectId, context):
|
||||
data = app.current_request.json_body
|
||||
data = sessions_assignments.create_new_assignment(tenant_id=context['tenantId'], project_id=projectId,
|
||||
session_id=sessionId,
|
||||
creator_id=context["userId"], assignee=data["assignee"],
|
||||
description=data["description"], title=data["title"],
|
||||
issue_type=data["issueType"],
|
||||
integration_project_id=integrationProjectId)
|
||||
if "errors" in data.keys():
|
||||
return data
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/gdpr', methods=['GET'])
|
||||
def get_gdpr(projectId, context):
|
||||
return {"data": projects.get_gdpr(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/gdpr', methods=['POST', 'PUT'])
|
||||
def edit_gdpr(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": projects.edit_gdpr(project_id=projectId, gdpr=data)}
|
||||
|
||||
|
||||
@app.route('/password/reset-link', methods=['PUT', 'POST'], authorizer=None)
|
||||
def reset_password_handler():
|
||||
data = app.current_request.json_body
|
||||
if "email" not in data or len(data["email"]) < 5:
|
||||
return {"errors": ["please provide a valid email address"]}
|
||||
return reset_password.reset(data)
|
||||
|
||||
|
||||
@app.route('/{projectId}/metadata', methods=['GET'])
|
||||
def get_metadata(projectId, context):
|
||||
return {"data": metadata.get(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/metadata/list', methods=['POST', 'PUT'])
|
||||
def add_edit_delete_metadata(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return metadata.add_edit_delete(tenant_id=context["tenantId"], project_id=projectId, new_metas=data["list"])
|
||||
|
||||
|
||||
@app.route('/{projectId}/metadata', methods=['POST', 'PUT'])
|
||||
def add_metadata(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return metadata.add(tenant_id=context["tenantId"], project_id=projectId, new_name=data["key"])
|
||||
|
||||
|
||||
@app.route('/{projectId}/metadata/{index}', methods=['POST', 'PUT'])
|
||||
def edit_metadata(projectId, index, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return metadata.edit(tenant_id=context["tenantId"], project_id=projectId, index=int(index),
|
||||
new_name=data["key"])
|
||||
|
||||
|
||||
@app.route('/{projectId}/metadata/{index}', methods=['DELETE'])
|
||||
def delete_metadata(projectId, index, context):
|
||||
return metadata.delete(tenant_id=context["tenantId"], project_id=projectId, index=index)
|
||||
|
||||
|
||||
@app.route('/{projectId}/metadata/search', methods=['GET'])
|
||||
def search_metadata(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
q = params.get('q', '')
|
||||
key = params.get('key', '')
|
||||
if len(q) == 0 and len(key) == 0:
|
||||
return {"data": []}
|
||||
if len(q) == 0:
|
||||
return {"errors": ["please provide a value for search"]}
|
||||
if len(key) == 0:
|
||||
return {"errors": ["please provide a key for search"]}
|
||||
return metadata.search(tenant_id=context["tenantId"], project_id=projectId, value=q, key=key)
|
||||
|
||||
|
||||
@app.route('/{projectId}/integration/sources', methods=['GET'])
|
||||
def search_integrations(projectId, context):
|
||||
return log_tools.search(project_id=projectId)
|
||||
|
||||
|
||||
@app.route('/async/email_assignment', methods=['POST', 'PUT'], authorizer=None)
|
||||
def async_send_signup_emails():
|
||||
data = app.current_request.json_body
|
||||
if data.pop("auth") != environ["async_Token"]:
|
||||
return {}
|
||||
email_helper.send_assign_session(recipient=data["email"], link=data["link"], message=data["message"])
|
||||
|
||||
|
||||
@app.route('/async/funnel/weekly_report2', methods=['POST', 'PUT'], authorizer=None)
|
||||
def async_weekly_report():
|
||||
print("=========================> Sending weekly report")
|
||||
data = app.current_request.json_body
|
||||
if data.pop("auth") != environ["async_Token"]:
|
||||
return {}
|
||||
email_helper.weekly_report2(recipients=data["email"], data=data.get("data", None))
|
||||
|
||||
|
||||
@app.route('/async/basic/{step}', methods=['POST', 'PUT'], authorizer=None)
|
||||
def async_basic_emails(step):
|
||||
data = app.current_request.json_body
|
||||
if data.pop("auth") != environ["async_Token"]:
|
||||
return {}
|
||||
if step.lower() == "member_invitation":
|
||||
email_helper.send_team_invitation(recipient=data["email"], invitation_link=data["invitationLink"],
|
||||
client_id=data["clientId"], sender_name=data["senderName"])
|
||||
|
||||
|
||||
@app.route('/{projectId}/sample_rate', methods=['GET'])
|
||||
def get_capture_status(projectId, context):
|
||||
return {"data": projects.get_capture_status(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/sample_rate', methods=['POST', 'PUT'])
|
||||
def update_capture_status(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return {"data": projects.update_capture_status(project_id=projectId, changes=data)}
|
||||
|
||||
|
||||
@app.route('/announcements', methods=['GET'])
|
||||
def get_all_announcements(context):
|
||||
return {"data": announcements.get_all(context["userId"])}
|
||||
|
||||
|
||||
@app.route('/announcements/view', methods=['GET'])
|
||||
def get_all_announcements(context):
|
||||
return {"data": announcements.view(user_id=context["userId"])}
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/{errorId}/{action}', methods=['GET'])
|
||||
def add_remove_favorite_error(projectId, errorId, action, context):
|
||||
if action == "favorite":
|
||||
return errors_favorite_viewed.favorite_error(project_id=projectId, user_id=context['userId'], error_id=errorId)
|
||||
elif action == "sessions":
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
start_date = params.get("startDate")
|
||||
end_date = params.get("endDate")
|
||||
return {
|
||||
"data": errors.get_sessions(project_id=projectId, user_id=context['userId'], error_id=errorId,
|
||||
start_date=start_date, end_date=end_date)}
|
||||
elif action in list(errors.ACTION_STATE.keys()):
|
||||
return errors.change_state(project_id=projectId, user_id=context['userId'], error_id=errorId, action=action)
|
||||
else:
|
||||
return {"errors": ["undefined action"]}
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/merge', methods=['POST'])
|
||||
def errors_merge(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
data = errors.merge(error_ids=data.get("errors", []))
|
||||
return data
|
||||
|
||||
|
||||
@app.route('/show_banner', methods=['GET'])
|
||||
def errors_merge(context):
|
||||
return {"data": False}
|
||||
|
||||
|
||||
@app.route('/{projectId}/alerts', methods=['POST', 'PUT'])
|
||||
def create_alert(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
return alerts.create(projectId, data)
|
||||
|
||||
|
||||
@app.route('/{projectId}/alerts', methods=['GET'])
|
||||
def get_all_alerts(projectId, context):
|
||||
return {"data": alerts.get_all(projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/alerts/{alertId}', methods=['GET'])
|
||||
def get_alert(projectId, alertId, context):
|
||||
return {"data": alerts.get(alertId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/alerts/{alertId}', methods=['POST', 'PUT'])
|
||||
def update_alert(projectId, alertId, context):
|
||||
data = app.current_request.json_body
|
||||
return alerts.update(alertId, data)
|
||||
|
||||
|
||||
@app.route('/{projectId}/alerts/{alertId}', methods=['DELETE'])
|
||||
def delete_alert(projectId, alertId, context):
|
||||
return alerts.delete(projectId, alertId)
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels', methods=['POST', 'PUT'])
|
||||
def add_funnel(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
return funnels.create(project_id=projectId,
|
||||
user_id=context['userId'],
|
||||
name=data["name"],
|
||||
filter=data["filter"],
|
||||
is_public=data.get("isPublic", False))
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels', methods=['GET'])
|
||||
def get_funnels(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return {"data": funnels.get_by_user(project_id=projectId,
|
||||
user_id=context['userId'],
|
||||
range_value=None,
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
details=False)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/details', methods=['GET'])
|
||||
def get_funnels_with_details(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return {"data": funnels.get_by_user(project_id=projectId,
|
||||
user_id=context['userId'],
|
||||
range_value=params.get("rangeValue", None),
|
||||
start_date=params.get('startDate', None),
|
||||
end_date=params.get('endDate', None),
|
||||
details=True)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/issue_types', methods=['GET'])
|
||||
def get_possible_issue_types(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return {"data": funnels.get_possible_issue_types(project_id=projectId)}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/insights', methods=['GET'])
|
||||
def get_funnel_insights(projectId, funnelId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return funnels.get_top_insights(funnel_id=funnelId, project_id=projectId,
|
||||
range_value=params.get("range_value", None),
|
||||
start_date=params.get('startDate', None),
|
||||
end_date=params.get('endDate', None))
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/insights', methods=['POST', 'PUT'])
|
||||
def get_funnel_insights_on_the_fly(projectId, funnelId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
return funnels.get_top_insights_on_the_fly(funnel_id=funnelId, project_id=projectId, data={**params, **data})
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/issues', methods=['GET'])
|
||||
def get_funnel_issues(projectId, funnelId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return funnels.get_issues(funnel_id=funnelId, project_id=projectId,
|
||||
range_value=params.get("range_value", None),
|
||||
start_date=params.get('startDate', None), end_date=params.get('endDate', None))
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/issues', methods=['POST', 'PUT'])
|
||||
def get_funnel_issues_on_the_fly(projectId, funnelId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
return {"data": funnels.get_issues_on_the_fly(funnel_id=funnelId, project_id=projectId, data={**params, **data})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/sessions', methods=['GET'])
|
||||
def get_funnel_sessions(projectId, funnelId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return {"data": funnels.get_sessions(funnel_id=funnelId, user_id=context['userId'], project_id=projectId,
|
||||
range_value=params.get("range_value", None),
|
||||
start_date=params.get('startDate', None),
|
||||
end_date=params.get('endDate', None))}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/sessions', methods=['POST', 'PUT'])
|
||||
def get_funnel_sessions_on_the_fly(projectId, funnelId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
return {"data": funnels.get_sessions_on_the_fly(funnel_id=funnelId, user_id=context['userId'], project_id=projectId,
|
||||
data={**params, **data})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/issues/{issueId}/sessions', methods=['GET'])
|
||||
def get_issue_sessions(projectId, issueId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
issue = issues.get(project_id=projectId, issue_id=issueId)
|
||||
return {
|
||||
"data": {"sessions": sessions.search_by_issue(user_id=context["userId"], project_id=projectId, issue=issue,
|
||||
start_date=params.get('startDate', None),
|
||||
end_date=params.get('endDate', None)),
|
||||
"issue": issue}}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}/issues/{issueId}/sessions', methods=['POST', 'PUT'])
|
||||
def get_funnel_issue_sessions(projectId, funnelId, issueId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
data = funnels.search_by_issue(project_id=projectId, user_id=context["userId"], issue_id=issueId,
|
||||
funnel_id=funnelId, data=data)
|
||||
if "errors" in data:
|
||||
return data
|
||||
if data.get("issue") is None:
|
||||
data["issue"] = issues.get(project_id=projectId, issue_id=issueId)
|
||||
return {
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}', methods=['GET'])
|
||||
def get_funnel(projectId, funnelId, context):
|
||||
data = funnels.get(funnel_id=funnelId,
|
||||
project_id=projectId)
|
||||
if data is None:
|
||||
return {"errors": ["funnel not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}', methods=['POST', 'PUT'])
|
||||
def edit_funnel(projectId, funnelId, context):
|
||||
data = app.current_request.json_body
|
||||
return funnels.update(funnel_id=funnelId,
|
||||
user_id=context['userId'],
|
||||
name=data.get("name"),
|
||||
filter=data.get("filter"),
|
||||
is_public=data.get("isPublic"))
|
||||
|
||||
|
||||
@app.route('/{projectId}/funnels/{funnelId}', methods=['DELETE'])
|
||||
def delete_filter(projectId, funnelId, context):
|
||||
return funnels.delete(user_id=context['userId'], funnel_id=funnelId, project_id=projectId)
|
||||
|
||||
|
||||
@app.route('/{projectId}/sourcemaps', methods=['PUT'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def sign_sourcemap_for_upload(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
project_id = projects.get_internal_project_id(projectId)
|
||||
if project_id is None:
|
||||
return Response(status_code=400, body='invalid projectId')
|
||||
|
||||
return {"data": sourcemaps.presign_upload_urls(project_id=project_id, urls=data["URL"])}
|
||||
|
||||
|
||||
@app.route('/config/weekly_report', methods=['GET'])
|
||||
def get_weekly_report_config(context):
|
||||
return {"data": weekly_report.get_config(user_id=context['userId'])}
|
||||
|
||||
|
||||
@app.route('/config/weekly_report', methods=['POST', 'PUT'])
|
||||
def get_weekly_report_config(context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": weekly_report.edit_config(user_id=context['userId'], weekly_report=data.get("weeklyReport", True))}
|
||||
|
||||
|
||||
@app.route('/{projectId}/issue_types', methods=['GET'])
|
||||
def issue_types(projectId, context):
|
||||
# return {"data": issues.get_types_by_project(project_id=projectId)}
|
||||
return {"data": issues.get_all_types()}
|
||||
|
||||
|
||||
@app.route('/issue_types', methods=['GET'])
|
||||
def all_issue_types(context):
|
||||
return {"data": issues.get_all_types()}
|
||||
|
||||
|
||||
@app.route('/flows', methods=['GET', 'PUT', 'POST', 'DELETE'])
|
||||
@app.route('/{projectId}/flows', methods=['GET', 'PUT', 'POST', 'DELETE'])
|
||||
def removed_endpoints(projectId=None, context=None):
|
||||
return Response(body={"errors": ["Endpoint no longer available"]}, status_code=410)
|
||||
|
||||
|
||||
@app.route('/{projectId}/assist/sessions', methods=['GET'])
|
||||
def sessions_live(projectId, context):
|
||||
data = assist.get_live_sessions(projectId)
|
||||
return {'data': data}
|
||||
|
||||
|
||||
@app.route('/{projectId}/assist/sessions', methods=['POST'])
|
||||
def sessions_live_search(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
data = assist.get_live_sessions(projectId, filters=data.get("filters"))
|
||||
return {'data': data}
|
||||
|
||||
|
||||
@app.route('/{projectId}/heatmaps/url', methods=['POST'])
|
||||
def get_heatmaps_by_url(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": heatmaps.get_by_url(project_id=projectId, data=data)}
|
||||
18
api/chalicelib/blueprints/bp_core_crons.py
Normal file
18
api/chalicelib/blueprints/bp_core_crons.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from chalice import Blueprint
|
||||
from chalice import Cron
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.core import reset_password, weekly_report, jobs
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.schedule(Cron('0', '*', '?', '*', '*', '*'))
|
||||
def run_scheduled_jobs(event):
|
||||
jobs.execute_jobs()
|
||||
|
||||
|
||||
# Run every monday.
|
||||
@app.schedule(Cron('5', '0', '?', '*', 'MON', '*'))
|
||||
def weekly_report2(event):
|
||||
weekly_report.cron()
|
||||
452
api/chalicelib/blueprints/bp_core_dynamic.py
Normal file
452
api/chalicelib/blueprints/bp_core_dynamic.py
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
from chalice import Blueprint, Response
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.core import metadata, errors_favorite_viewed, slack, alerts, sessions, integration_github, \
|
||||
integrations_manager
|
||||
from chalicelib.utils import captcha
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
from chalicelib.core import tenants
|
||||
from chalicelib.core import signup
|
||||
from chalicelib.core import users
|
||||
from chalicelib.core import projects
|
||||
from chalicelib.core import errors
|
||||
from chalicelib.core import notifications
|
||||
from chalicelib.core import boarding
|
||||
from chalicelib.core import webhook
|
||||
from chalicelib.core import license
|
||||
from chalicelib.core.collaboration_slack import Slack
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.route('/login', methods=['POST'], authorizer=None)
|
||||
def login():
|
||||
data = app.current_request.json_body
|
||||
if helper.allow_captcha() and not captcha.is_valid(data["g-recaptcha-response"]):
|
||||
return {"errors": ["Invalid captcha."]}
|
||||
r = users.authenticate(data['email'], data['password'],
|
||||
for_plugin=False
|
||||
)
|
||||
if r is None:
|
||||
return Response(status_code=401, body={
|
||||
'errors': ['You’ve entered invalid Email or Password.']
|
||||
})
|
||||
|
||||
tenant_id = r.pop("tenantId")
|
||||
|
||||
r["limits"] = {
|
||||
"teamMember": -1,
|
||||
"projects": -1,
|
||||
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
|
||||
|
||||
c = tenants.get_by_tenant_id(tenant_id)
|
||||
c.pop("createdAt")
|
||||
c["projects"] = projects.get_projects(tenant_id=tenant_id, recording_state=True, recorded=True,
|
||||
stack_integrations=True, version=True)
|
||||
c["smtp"] = helper.has_smtp()
|
||||
return {
|
||||
'jwt': r.pop('jwt'),
|
||||
'data': {
|
||||
"user": r,
|
||||
"client": c
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.route('/account', methods=['GET'])
|
||||
def get_account(context):
|
||||
r = users.get(tenant_id=context['tenantId'], user_id=context['userId'])
|
||||
return {
|
||||
'data': {
|
||||
**r,
|
||||
"limits": {
|
||||
"teamMember": -1,
|
||||
"projects": -1,
|
||||
"metadata": metadata.get_remaining_metadata_with_count(context['tenantId'])
|
||||
},
|
||||
**license.get_status(context["tenantId"]),
|
||||
"smtp": helper.has_smtp()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.route('/projects', methods=['GET'])
|
||||
def get_projects(context):
|
||||
return {"data": projects.get_projects(tenant_id=context["tenantId"], recording_state=True, gdpr=True, recorded=True,
|
||||
stack_integrations=True, version=True)}
|
||||
|
||||
|
||||
@app.route('/projects', methods=['POST', 'PUT'])
|
||||
def create_project(context):
|
||||
data = app.current_request.json_body
|
||||
return projects.create(tenant_id=context["tenantId"], user_id=context["userId"], data=data)
|
||||
|
||||
|
||||
@app.route('/projects/{projectId}', methods=['POST', 'PUT'])
|
||||
def create_edit_project(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
|
||||
return projects.edit(tenant_id=context["tenantId"], user_id=context["userId"], data=data, project_id=projectId)
|
||||
|
||||
|
||||
@app.route('/projects/{projectId}', methods=['GET'])
|
||||
def get_project(projectId, context):
|
||||
data = projects.get_project(tenant_id=context["tenantId"], project_id=projectId, include_last_session=True,
|
||||
include_gdpr=True)
|
||||
if data is None:
|
||||
return {"errors": ["project not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.route('/projects/{projectId}', methods=['DELETE'])
|
||||
def delete_project(projectId, context):
|
||||
return projects.delete(tenant_id=context["tenantId"], user_id=context["userId"], project_id=projectId)
|
||||
|
||||
|
||||
@app.route('/projects/limit', methods=['GET'])
|
||||
def get_projects_limit(context):
|
||||
return {"data": {
|
||||
"current": projects.count_by_tenant(tenant_id=context["tenantId"]),
|
||||
"remaining": -1
|
||||
}}
|
||||
|
||||
|
||||
@app.route('/client', methods=['GET'])
|
||||
def get_client(context):
|
||||
r = tenants.get_by_tenant_id(context['tenantId'])
|
||||
if r is not None:
|
||||
r.pop("createdAt")
|
||||
r["projects"] = projects.get_projects(tenant_id=context['tenantId'], recording_state=True, recorded=True,
|
||||
stack_integrations=True, version=True)
|
||||
return {
|
||||
'data': r
|
||||
}
|
||||
|
||||
|
||||
@app.route('/client/new_api_key', methods=['GET'])
|
||||
def generate_new_tenant_token(context):
|
||||
return {
|
||||
'data': tenants.generate_new_api_key(context['tenantId'])
|
||||
}
|
||||
|
||||
|
||||
@app.route('/client', methods=['PUT', 'POST'])
|
||||
def put_client(context):
|
||||
data = app.current_request.json_body
|
||||
return tenants.update(tenant_id=context["tenantId"], user_id=context["userId"], data=data)
|
||||
|
||||
|
||||
@app.route('/signup', methods=['GET'], authorizer=None)
|
||||
def get_all_signup():
|
||||
return {"data": tenants.tenants_exists()}
|
||||
|
||||
|
||||
@app.route('/signup', methods=['POST', 'PUT'], authorizer=None)
|
||||
def signup_handler():
|
||||
data = app.current_request.json_body
|
||||
return signup.create_step1(data)
|
||||
|
||||
|
||||
@app.route('/integrations/slack', methods=['POST', 'PUT'])
|
||||
def add_slack_client(context):
|
||||
data = app.current_request.json_body
|
||||
if "url" not in data or "name" not in data:
|
||||
return {"errors": ["please provide a url and a name"]}
|
||||
n = Slack.add_channel(tenant_id=context["tenantId"], url=data["url"], name=data["name"])
|
||||
if n is None:
|
||||
return {
|
||||
"errors": ["We couldn't send you a test message on your Slack channel. Please verify your webhook url."]
|
||||
}
|
||||
return {"data": n}
|
||||
|
||||
|
||||
@app.route('/integrations/slack/{integrationId}', methods=['POST', 'PUT'])
|
||||
def edit_slack_integration(integrationId, context):
|
||||
data = app.current_request.json_body
|
||||
if data.get("url") and len(data["url"]) > 0:
|
||||
old = webhook.get(tenant_id=context["tenantId"], webhook_id=integrationId)
|
||||
if old["endpoint"] != data["url"]:
|
||||
if not Slack.say_hello(data["url"]):
|
||||
return {
|
||||
"errors": [
|
||||
"We couldn't send you a test message on your Slack channel. Please verify your webhook url."]
|
||||
}
|
||||
return {"data": webhook.update(tenant_id=context["tenantId"], webhook_id=integrationId,
|
||||
changes={"name": data.get("name", ""), "endpoint": data["url"]})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/search', methods=['POST'])
|
||||
def errors_search(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return errors.search(data, projectId, user_id=context["userId"], status=params.get("status", "ALL"),
|
||||
favorite_only="favorite" in params)
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/stats', methods=['GET'])
|
||||
def errors_stats(projectId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return errors.stats(projectId, user_id=context["userId"], **params)
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/{errorId}', methods=['GET'])
|
||||
def errors_get_details(projectId, errorId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
data = errors.get_details(project_id=projectId, user_id=context["userId"], error_id=errorId, **params)
|
||||
if data.get("data") is not None:
|
||||
errors_favorite_viewed.viewed_error(project_id=projectId, user_id=context['userId'], error_id=errorId)
|
||||
return data
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/{errorId}/stats', methods=['GET'])
|
||||
def errors_get_details_right_column(projectId, errorId, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
data = errors.get_details_chart(project_id=projectId, user_id=context["userId"], error_id=errorId, **params)
|
||||
return data
|
||||
|
||||
|
||||
@app.route('/{projectId}/errors/{errorId}/sourcemaps', methods=['GET'])
|
||||
def errors_get_details_sourcemaps(projectId, errorId, context):
|
||||
data = errors.get_trace(project_id=projectId, error_id=errorId)
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@app.route('/async/alerts/notifications/{step}', methods=['POST', 'PUT'], authorizer=None)
|
||||
def send_alerts_notification_async(step):
|
||||
data = app.current_request.json_body
|
||||
if data.pop("auth") != environ["async_Token"]:
|
||||
return {"errors": ["missing auth"]}
|
||||
if step == "slack":
|
||||
slack.send_batch(notifications_list=data.get("notifications"))
|
||||
elif step == "email":
|
||||
alerts.send_by_email_batch(notifications_list=data.get("notifications"))
|
||||
elif step == "webhook":
|
||||
webhook.trigger_batch(data_list=data.get("notifications"))
|
||||
|
||||
|
||||
@app.route('/notifications', methods=['GET'])
|
||||
def get_notifications(context):
|
||||
return {"data": notifications.get_all(tenant_id=context['tenantId'], user_id=context['userId'])}
|
||||
|
||||
|
||||
@app.route('/notifications/{notificationId}/view', methods=['GET'])
|
||||
def view_notifications(notificationId, context):
|
||||
return {"data": notifications.view_notification(notification_ids=[notificationId], user_id=context['userId'])}
|
||||
|
||||
|
||||
@app.route('/notifications/view', methods=['POST', 'PUT'])
|
||||
def batch_view_notifications(context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": notifications.view_notification(notification_ids=data.get("ids", []),
|
||||
startTimestamp=data.get("startTimestamp"),
|
||||
endTimestamp=data.get("endTimestamp"),
|
||||
user_id=context['userId'],
|
||||
tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/notifications', methods=['POST', 'PUT'], authorizer=None)
|
||||
def create_notifications():
|
||||
data = app.current_request.json_body
|
||||
if data.get("token", "") != "nF46JdQqAM5v9KI9lPMpcu8o9xiJGvNNWOGL7TJP":
|
||||
return {"errors": ["missing token"]}
|
||||
return notifications.create(data.get("notifications", []))
|
||||
|
||||
|
||||
@app.route('/boarding', methods=['GET'])
|
||||
def get_boarding_state(context):
|
||||
return {"data": boarding.get_state(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/boarding/installing', methods=['GET'])
|
||||
def get_boarding_state_installing(context):
|
||||
return {"data": boarding.get_state_installing(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/boarding/identify-users', methods=['GET'])
|
||||
def get_boarding_state_identify_users(context):
|
||||
return {"data": boarding.get_state_identify_users(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/boarding/manage-users', methods=['GET'])
|
||||
def get_boarding_state_manage_users(context):
|
||||
return {"data": boarding.get_state_manage_users(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
@app.route('/boarding/integrations', methods=['GET'])
|
||||
def get_boarding_state_integrations(context):
|
||||
return {"data": boarding.get_state_integrations(tenant_id=context["tenantId"])}
|
||||
|
||||
|
||||
# this endpoint supports both jira & github based on `provider` attribute
|
||||
@app.route('/integrations/issues', methods=['POST', 'PUT'])
|
||||
def add_edit_jira_cloud_github(context):
|
||||
data = app.current_request.json_body
|
||||
provider = data.get("provider", "").upper()
|
||||
error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
if error is not None:
|
||||
return error
|
||||
return {"data": integration.add_edit(data=data)}
|
||||
|
||||
|
||||
@app.route('/integrations/slack/{integrationId}', methods=['GET'])
|
||||
def get_slack_webhook(integrationId, context):
|
||||
return {"data": webhook.get(tenant_id=context["tenantId"], webhook_id=integrationId)}
|
||||
|
||||
|
||||
@app.route('/integrations/slack/channels', methods=['GET'])
|
||||
def get_slack_integration(context):
|
||||
return {"data": webhook.get_by_type(tenant_id=context["tenantId"], webhook_type='slack')}
|
||||
|
||||
|
||||
@app.route('/integrations/slack/{integrationId}', methods=['DELETE'])
|
||||
def delete_slack_integration(integrationId, context):
|
||||
return webhook.delete(context["tenantId"], integrationId)
|
||||
|
||||
|
||||
@app.route('/webhooks', methods=['POST', 'PUT'])
|
||||
def add_edit_webhook(context):
|
||||
data = app.current_request.json_body
|
||||
return {"data": webhook.add_edit(tenant_id=context["tenantId"], data=data, replace_none=True)}
|
||||
|
||||
|
||||
@app.route('/webhooks', methods=['GET'])
|
||||
def get_webhooks(context):
|
||||
return {"data": webhook.get_by_tenant(tenant_id=context["tenantId"], replace_none=True)}
|
||||
|
||||
|
||||
@app.route('/webhooks/{webhookId}', methods=['DELETE'])
|
||||
def delete_webhook(webhookId, context):
|
||||
return {"data": webhook.delete(tenant_id=context["tenantId"], webhook_id=webhookId)}
|
||||
|
||||
|
||||
@app.route('/client/members', methods=['GET'])
|
||||
def get_members(context):
|
||||
return {"data": users.get_members(tenant_id=context['tenantId'])}
|
||||
|
||||
|
||||
@app.route('/client/members', methods=['PUT', 'POST'])
|
||||
def add_member(context):
|
||||
data = app.current_request.json_body
|
||||
return users.create_member(tenant_id=context['tenantId'], user_id=context['userId'], data=data)
|
||||
|
||||
|
||||
@app.route('/users/invitation', methods=['GET'], authorizer=None)
|
||||
def process_invitation_link():
|
||||
params = app.current_request.query_params
|
||||
if params is None or len(params.get("token", "")) < 64:
|
||||
return {"errors": ["please provide a valid invitation"]}
|
||||
user = users.get_by_invitation_token(params["token"])
|
||||
if user is None:
|
||||
return {"errors": ["invitation not found"]}
|
||||
if user["expiredInvitation"]:
|
||||
return {"errors": ["expired invitation, please ask your admin to send a new one"]}
|
||||
pass_token = users.allow_password_change(user_id=user["userId"])
|
||||
return Response(
|
||||
status_code=307,
|
||||
body='',
|
||||
headers={'Location': environ["SITE_URL"] + environ["change_password_link"] % (params["token"], pass_token),
|
||||
'Content-Type': 'text/plain'})
|
||||
|
||||
|
||||
@app.route('/password/reset', methods=['POST', 'PUT'], authorizer=None)
|
||||
def change_password_by_invitation():
|
||||
data = app.current_request.json_body
|
||||
if data is None or len(data.get("invitation", "")) < 64 or len(data.get("pass", "")) < 8:
|
||||
return {"errors": ["please provide a valid invitation & pass"]}
|
||||
user = users.get_by_invitation_token(token=data["invitation"], pass_token=data["pass"])
|
||||
if user is None:
|
||||
return {"errors": ["invitation not found"]}
|
||||
if user["expiredChange"]:
|
||||
return {"errors": ["expired change, please re-use the invitation link"]}
|
||||
|
||||
return users.set_password_invitation(new_password=data["password"], user_id=user["userId"])
|
||||
|
||||
|
||||
@app.route('/client/members/{memberId}', methods=['PUT', 'POST'])
|
||||
def edit_member(memberId, context):
|
||||
data = app.current_request.json_body
|
||||
return users.edit(tenant_id=context['tenantId'], editor_id=context['userId'], changes=data,
|
||||
user_id_to_update=memberId)
|
||||
|
||||
|
||||
@app.route('/client/members/{memberId}/reset', methods=['GET'])
|
||||
def reset_reinvite_member(memberId, context):
|
||||
return users.reset_member(tenant_id=context['tenantId'], editor_id=context['userId'], user_id_to_update=memberId)
|
||||
|
||||
|
||||
@app.route('/client/members/{memberId}', methods=['DELETE'])
|
||||
def delete_member(memberId, context):
|
||||
return users.delete_member(tenant_id=context["tenantId"], user_id=context['userId'], id_to_delete=memberId)
|
||||
|
||||
|
||||
@app.route('/account/new_api_key', methods=['GET'])
|
||||
def generate_new_user_token(context):
|
||||
return {"data": users.generate_new_api_key(user_id=context['userId'])}
|
||||
|
||||
|
||||
@app.route('/account', methods=['POST', 'PUT'])
|
||||
def edit_account(context):
|
||||
data = app.current_request.json_body
|
||||
return users.edit(tenant_id=context['tenantId'], user_id_to_update=context['userId'], changes=data,
|
||||
editor_id=context['userId'])
|
||||
|
||||
|
||||
@app.route('/account/password', methods=['PUT', 'POST'])
|
||||
def change_client_password(context):
|
||||
data = app.current_request.json_body
|
||||
return users.change_password(email=context['email'], old_password=data["oldPassword"],
|
||||
new_password=data["newPassword"], tenant_id=context["tenantId"],
|
||||
user_id=context["userId"])
|
||||
|
||||
|
||||
@app.route('/metadata/session_search', methods=['GET'])
|
||||
def search_sessions_by_metadata(context):
|
||||
params = app.current_request.query_params
|
||||
if params is None:
|
||||
return {"errors": ["please provide a key&value for search"]}
|
||||
value = params.get('value', '')
|
||||
key = params.get('key', '')
|
||||
project_id = params.get('projectId')
|
||||
if len(value) == 0 and len(key) == 0:
|
||||
return {"errors": ["please provide a key&value for search"]}
|
||||
if len(value) == 0:
|
||||
return {"errors": ["please provide a value for search"]}
|
||||
if len(key) == 0:
|
||||
return {"errors": ["please provide a key for search"]}
|
||||
return {
|
||||
"data": sessions.search_by_metadata(tenant_id=context["tenantId"], user_id=context["userId"], m_value=value,
|
||||
m_key=key,
|
||||
project_id=project_id)}
|
||||
|
||||
|
||||
@app.route('/plans', methods=['GET'])
|
||||
def get_current_plan(context):
|
||||
return {
|
||||
"data": license.get_status(context["tenantId"])
|
||||
}
|
||||
|
||||
|
||||
@app.route('/alerts/notifications', methods=['POST', 'PUT'], authorizer=None)
|
||||
def send_alerts_notifications():
|
||||
data = app.current_request.json_body
|
||||
return {"data": alerts.process_notifications(data.get("notifications", []))}
|
||||
13
api/chalicelib/blueprints/bp_core_dynamic_crons.py
Normal file
13
api/chalicelib/blueprints/bp_core_dynamic_crons.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from chalice import Blueprint, Cron
|
||||
from chalicelib import _overrides
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
from chalicelib.core import telemetry
|
||||
|
||||
|
||||
# Run every day.
|
||||
@app.schedule(Cron('0', '0', '?', '*', '*', '*'))
|
||||
def telemetry_cron(event):
|
||||
telemetry.compute()
|
||||
550
api/chalicelib/blueprints/subs/bp_dashboard.py
Normal file
550
api/chalicelib/blueprints/subs/bp_dashboard.py
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
from chalice import Blueprint
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib import _overrides
|
||||
|
||||
from chalicelib.core import dashboard
|
||||
from chalicelib.core import metadata
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/metadata', methods=['GET'])
|
||||
def get_metadata_map(projectId, context):
|
||||
metamap = []
|
||||
for m in metadata.get(project_id=projectId):
|
||||
metamap.append({"name": m["key"], "key": f"metadata{m['index']}"})
|
||||
return {"data": metamap}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/sessions', methods=['GET', 'POST'])
|
||||
def get_dashboard_processed_sessions(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_processed_sessions(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/errors', methods=['GET', 'POST'])
|
||||
def get_dashboard_errors(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_errors(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/errors_trend', methods=['GET', 'POST'])
|
||||
def get_dashboard_errors_trend(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_errors_trend(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/application_activity', methods=['GET', 'POST'])
|
||||
def get_dashboard_application_activity(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_application_activity(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/page_metrics', methods=['GET', 'POST'])
|
||||
def get_dashboard_page_metrics(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_page_metrics(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/user_activity', methods=['GET', 'POST'])
|
||||
def get_dashboard_user_activity(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_user_activity(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/performance', methods=['GET', 'POST'])
|
||||
def get_dashboard_performance(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_performance(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/slowest_images', methods=['GET', 'POST'])
|
||||
def get_dashboard_slowest_images(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_slowest_images(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/missing_resources', methods=['GET', 'POST'])
|
||||
def get_performance_sessions(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_missing_resources_trend(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/network', methods=['GET', 'POST'])
|
||||
def get_network_widget(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_network(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/{widget}/search', methods=['GET'])
|
||||
def get_dashboard_autocomplete(projectId, widget, context):
|
||||
params = app.current_request.query_params
|
||||
if params is None or params.get('q') is None or len(params.get('q')) == 0:
|
||||
return {"data": []}
|
||||
params['q'] = '^' + params['q']
|
||||
|
||||
if widget in ['performance']:
|
||||
data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
platform=params.get('platform', None), performance=True)
|
||||
elif widget in ['pages', 'pages_dom_buildtime', 'top_metrics', 'time_to_render',
|
||||
'impacted_sessions_by_slow_pages', 'pages_response_time']:
|
||||
data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
platform=params.get('platform', None), pages_only=True)
|
||||
elif widget in ['resources_loading_time']:
|
||||
data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
platform=params.get('platform', None), performance=False)
|
||||
elif widget in ['time_between_events', 'events']:
|
||||
data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
platform=params.get('platform', None), performance=False, events_only=True)
|
||||
elif widget in ['metadata']:
|
||||
data = dashboard.search(params.get('q', ''), None, project_id=projectId,
|
||||
platform=params.get('platform', None), metadata=True, key=params.get("key"))
|
||||
else:
|
||||
return {"errors": [f"unsupported widget: {widget}"]}
|
||||
return {'data': data}
|
||||
|
||||
|
||||
# 1
|
||||
@app.route('/{projectId}/dashboard/slowest_resources', methods=['GET', 'POST'])
|
||||
def get_dashboard_slowest_resources(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_slowest_resources(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 2
|
||||
@app.route('/{projectId}/dashboard/resources_loading_time', methods=['GET', 'POST'])
|
||||
def get_dashboard_resources(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_resources_loading_time(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 3
|
||||
@app.route('/{projectId}/dashboard/pages_dom_buildtime', methods=['GET', 'POST'])
|
||||
def get_dashboard_pages_dom(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_pages_dom_build_time(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 4
|
||||
@app.route('/{projectId}/dashboard/busiest_time_of_day', methods=['GET', 'POST'])
|
||||
def get_dashboard_busiest_time_of_day(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_busiest_time_of_day(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 5
|
||||
@app.route('/{projectId}/dashboard/sessions_location', methods=['GET', 'POST'])
|
||||
def get_dashboard_sessions_location(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_sessions_location(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 6
|
||||
@app.route('/{projectId}/dashboard/speed_location', methods=['GET', 'POST'])
|
||||
def get_dashboard_speed_location(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_speed_index_location(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 7
|
||||
@app.route('/{projectId}/dashboard/pages_response_time', methods=['GET', 'POST'])
|
||||
def get_dashboard_pages_response_time(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_pages_response_time(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 8
|
||||
@app.route('/{projectId}/dashboard/pages_response_time_distribution', methods=['GET', 'POST'])
|
||||
def get_dashboard_pages_response_time_distribution(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_pages_response_time_distribution(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 9
|
||||
@app.route('/{projectId}/dashboard/top_metrics', methods=['GET', 'POST'])
|
||||
def get_dashboard_top_metrics(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_top_metrics(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 10
|
||||
@app.route('/{projectId}/dashboard/time_to_render', methods=['GET', 'POST'])
|
||||
def get_dashboard_time_to_render(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_time_to_render(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 11
|
||||
@app.route('/{projectId}/dashboard/impacted_sessions_by_slow_pages', methods=['GET', 'POST'])
|
||||
def get_dashboard_impacted_sessions_by_slow_pages(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_impacted_sessions_by_slow_pages(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 12
|
||||
@app.route('/{projectId}/dashboard/memory_consumption', methods=['GET', 'POST'])
|
||||
def get_dashboard_memory_consumption(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_memory_consumption(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 12.1
|
||||
@app.route('/{projectId}/dashboard/fps', methods=['GET', 'POST'])
|
||||
def get_dashboard_avg_fps(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_avg_fps(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 12.2
|
||||
@app.route('/{projectId}/dashboard/cpu', methods=['GET', 'POST'])
|
||||
def get_dashboard_avg_cpu(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_avg_cpu(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 13
|
||||
@app.route('/{projectId}/dashboard/crashes', methods=['GET', 'POST'])
|
||||
def get_dashboard_impacted_sessions_by_slow_pages(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_crashes(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 14
|
||||
@app.route('/{projectId}/dashboard/domains_errors', methods=['GET', 'POST'])
|
||||
def get_dashboard_domains_errors(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_domains_errors(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 14.1
|
||||
@app.route('/{projectId}/dashboard/domains_errors_4xx', methods=['GET', 'POST'])
|
||||
def get_dashboard_domains_errors_4xx(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_domains_errors_4xx(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 14.2
|
||||
@app.route('/{projectId}/dashboard/domains_errors_5xx', methods=['GET', 'POST'])
|
||||
def get_dashboard_domains_errors_5xx(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_domains_errors_5xx(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 15
|
||||
@app.route('/{projectId}/dashboard/slowest_domains', methods=['GET', 'POST'])
|
||||
def get_dashboard_slowest_domains(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_slowest_domains(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 16
|
||||
@app.route('/{projectId}/dashboard/errors_per_domains', methods=['GET', 'POST'])
|
||||
def get_dashboard_errors_per_domains(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_errors_per_domains(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 17
|
||||
@app.route('/{projectId}/dashboard/sessions_per_browser', methods=['GET', 'POST'])
|
||||
def get_dashboard_sessions_per_browser(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_sessions_per_browser(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 18
|
||||
@app.route('/{projectId}/dashboard/calls_errors', methods=['GET', 'POST'])
|
||||
def get_dashboard_calls_errors(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_calls_errors(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 18.1
|
||||
@app.route('/{projectId}/dashboard/calls_errors_4xx', methods=['GET', 'POST'])
|
||||
def get_dashboard_calls_errors_4xx(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_calls_errors_4xx(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 18.2
|
||||
@app.route('/{projectId}/dashboard/calls_errors_5xx', methods=['GET', 'POST'])
|
||||
def get_dashboard_calls_errors_5xx(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_calls_errors_5xx(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 19
|
||||
@app.route('/{projectId}/dashboard/errors_per_type', methods=['GET', 'POST'])
|
||||
def get_dashboard_errors_per_type(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_errors_per_type(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 20
|
||||
@app.route('/{projectId}/dashboard/resources_by_party', methods=['GET', 'POST'])
|
||||
def get_dashboard_resources_by_party(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_resources_by_party(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 21
|
||||
@app.route('/{projectId}/dashboard/resource_type_vs_response_end', methods=['GET', 'POST'])
|
||||
def get_dashboard_errors_per_resource_type(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.resource_type_vs_response_end(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 22
|
||||
@app.route('/{projectId}/dashboard/resources_vs_visually_complete', methods=['GET', 'POST'])
|
||||
def get_dashboard_resources_vs_visually_complete(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_resources_vs_visually_complete(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 23
|
||||
@app.route('/{projectId}/dashboard/impacted_sessions_by_js_errors', methods=['GET', 'POST'])
|
||||
def get_dashboard_impacted_sessions_by_js_errors(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_impacted_sessions_by_js_errors(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 24
|
||||
@app.route('/{projectId}/dashboard/resources_count_by_type', methods=['GET', 'POST'])
|
||||
def get_dashboard_resources_count_by_type(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": dashboard.get_resources_count_by_type(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
# 25
|
||||
@app.route('/{projectId}/dashboard/time_between_events', methods=['GET'])
|
||||
def get_dashboard_resources_count_by_type(projectId, context):
|
||||
return {"errors": ["please choose 2 events"]}
|
||||
|
||||
|
||||
@app.route('/{projectId}/dashboard/overview', methods=['GET', 'POST'])
|
||||
def get_dashboard_group(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": [
|
||||
*helper.explode_widget(key="count_sessions",
|
||||
data=dashboard.get_processed_sessions(project_id=projectId, **{**data, **args})),
|
||||
*helper.explode_widget(data={**dashboard.get_application_activity(project_id=projectId, **{**data, **args}),
|
||||
"chart": dashboard.get_performance(project_id=projectId, **{**data, **args})
|
||||
.get("chart", [])}),
|
||||
*helper.explode_widget(data=dashboard.get_page_metrics(project_id=projectId, **{**data, **args})),
|
||||
*helper.explode_widget(data=dashboard.get_user_activity(project_id=projectId, **{**data, **args})),
|
||||
*helper.explode_widget(data=dashboard.get_pages_dom_build_time(project_id=projectId, **{**data, **args}),
|
||||
key="avg_pages_dom_buildtime"),
|
||||
*helper.explode_widget(data=dashboard.get_pages_response_time(project_id=projectId, **{**data, **args}),
|
||||
key="avg_pages_response_time"),
|
||||
*helper.explode_widget(dashboard.get_top_metrics(project_id=projectId, **{**data, **args})),
|
||||
*helper.explode_widget(data=dashboard.get_time_to_render(project_id=projectId, **{**data, **args}),
|
||||
key="avg_time_to_render"),
|
||||
*helper.explode_widget(dashboard.get_memory_consumption(project_id=projectId, **{**data, **args})),
|
||||
*helper.explode_widget(dashboard.get_avg_cpu(project_id=projectId, **{**data, **args})),
|
||||
*helper.explode_widget(dashboard.get_avg_fps(project_id=projectId, **{**data, **args})),
|
||||
]}
|
||||
178
api/chalicelib/blueprints/subs/bp_insights.py
Normal file
178
api/chalicelib/blueprints/subs/bp_insights.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
from chalice import Blueprint
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib import _overrides
|
||||
|
||||
from chalicelib.core import dashboard, insights
|
||||
from chalicelib.core import metadata
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
#
|
||||
# @app.route('/{projectId}/dashboard/metadata', methods=['GET'])
|
||||
# def get_metadata_map(projectId, context):
|
||||
# metamap = []
|
||||
# for m in metadata.get(project_id=projectId):
|
||||
# metamap.append({"name": m["key"], "key": f"metadata{m['index']}"})
|
||||
# return {"data": metamap}
|
||||
#
|
||||
#
|
||||
@app.route('/{projectId}/insights/journey', methods=['GET', 'POST'])
|
||||
def get_insights_journey(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.journey(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/users_acquisition', methods=['GET', 'POST'])
|
||||
def get_users_acquisition(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.users_acquisition(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/users_retention', methods=['GET', 'POST'])
|
||||
def get_users_retention(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.users_retention(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/feature_retention', methods=['GET', 'POST'])
|
||||
def get_feature_rentention(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.feature_retention(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/feature_acquisition', methods=['GET', 'POST'])
|
||||
def get_feature_acquisition(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.feature_acquisition(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/feature_popularity_frequency', methods=['GET', 'POST'])
|
||||
def get_feature_popularity_frequency(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.feature_popularity_frequency(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/feature_intensity', methods=['GET', 'POST'])
|
||||
def get_feature_intensity(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.feature_intensity(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/feature_adoption', methods=['GET', 'POST'])
|
||||
def get_feature_adoption(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.feature_adoption(project_id=projectId, **{**data, **args})}
|
||||
|
||||
@app.route('/{projectId}/insights/feature_adoption_top_users', methods=['GET', 'POST'])
|
||||
def get_feature_adoption(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.feature_adoption_top_users(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/users_active', methods=['GET', 'POST'])
|
||||
def get_users_active(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.users_active(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/users_power', methods=['GET', 'POST'])
|
||||
def get_users_power(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.users_power(project_id=projectId, **{**data, **args})}
|
||||
|
||||
|
||||
@app.route('/{projectId}/insights/users_slipping', methods=['GET', 'POST'])
|
||||
def get_users_slipping(projectId, context):
|
||||
data = app.current_request.json_body
|
||||
if data is None:
|
||||
data = {}
|
||||
params = app.current_request.query_params
|
||||
args = dashboard.dashboard_args(params)
|
||||
|
||||
return {"data": insights.users_slipping(project_id=projectId, **{**data, **args})}
|
||||
|
||||
#
|
||||
#
|
||||
# @app.route('/{projectId}/dashboard/{widget}/search', methods=['GET'])
|
||||
# def get_dashboard_autocomplete(projectId, widget, context):
|
||||
# params = app.current_request.query_params
|
||||
# if params is None or params.get('q') is None or len(params.get('q')) == 0:
|
||||
# return {"data": []}
|
||||
# params['q'] = '^' + params['q']
|
||||
#
|
||||
# if widget in ['performance']:
|
||||
# data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
# platform=params.get('platform', None), performance=True)
|
||||
# elif widget in ['pages', 'pages_dom_buildtime', 'top_metrics', 'time_to_render',
|
||||
# 'impacted_sessions_by_slow_pages', 'pages_response_time']:
|
||||
# data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
# platform=params.get('platform', None), pages_only=True)
|
||||
# elif widget in ['resources_loading_time']:
|
||||
# data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
# platform=params.get('platform', None), performance=False)
|
||||
# elif widget in ['time_between_events', 'events']:
|
||||
# data = dashboard.search(params.get('q', ''), params.get('type', ''), project_id=projectId,
|
||||
# platform=params.get('platform', None), performance=False, events_only=True)
|
||||
# elif widget in ['metadata']:
|
||||
# data = dashboard.search(params.get('q', ''), None, project_id=projectId,
|
||||
# platform=params.get('platform', None), metadata=True, key=params.get("key"))
|
||||
# else:
|
||||
# return {"errors": [f"unsupported widget: {widget}"]}
|
||||
# return {'data': data}
|
||||
168
api/chalicelib/core/alerts.py
Normal file
168
api/chalicelib/core/alerts.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import time
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
from chalicelib.core import notifications
|
||||
from chalicelib.utils import pg_client, helper, email_helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
import json
|
||||
|
||||
ALLOW_UPDATE = ["name", "description", "active", "detectionMethod", "query", "options"]
|
||||
|
||||
|
||||
def get(id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify("""\
|
||||
SELECT *
|
||||
FROM public.alerts
|
||||
WHERE alert_id =%(id)s;""",
|
||||
{"id": id})
|
||||
)
|
||||
a = helper.dict_to_camel_case(cur.fetchone())
|
||||
return __process_circular(a)
|
||||
|
||||
|
||||
def get_all(project_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify("""\
|
||||
SELECT *
|
||||
FROM public.alerts
|
||||
WHERE project_id =%(project_id)s AND deleted_at ISNULL
|
||||
ORDER BY created_at;""",
|
||||
{"project_id": project_id})
|
||||
cur.execute(query=query)
|
||||
all = helper.list_to_camel_case(cur.fetchall())
|
||||
for a in all:
|
||||
a = __process_circular(a)
|
||||
return all
|
||||
|
||||
|
||||
SUPPORTED_THRESHOLD = [15, 30, 60, 120, 240, 1440]
|
||||
|
||||
|
||||
def __transform_structure(data):
|
||||
if data.get("options") is None:
|
||||
return f"Missing 'options'", None
|
||||
if data["options"].get("currentPeriod") not in SUPPORTED_THRESHOLD:
|
||||
return f"Unsupported currentPeriod, please provide one of these values {SUPPORTED_THRESHOLD}", None
|
||||
if data["options"].get("previousPeriod", 15) not in SUPPORTED_THRESHOLD:
|
||||
return f"Unsupported previousPeriod, please provide one of these values {SUPPORTED_THRESHOLD}", None
|
||||
if data["options"].get("renotifyInterval") is None:
|
||||
data["options"]["renotifyInterval"] = 720
|
||||
data["query"]["right"] = float(data["query"]["right"])
|
||||
data["query"] = json.dumps(data["query"])
|
||||
data["description"] = data["description"] if data.get("description") is not None and len(
|
||||
data["description"]) > 0 else None
|
||||
if data.get("options"):
|
||||
messages = []
|
||||
for m in data["options"].get("message", []):
|
||||
if m.get("value") is None:
|
||||
continue
|
||||
m["value"] = str(m["value"])
|
||||
messages.append(m)
|
||||
data["options"]["message"] = messages
|
||||
data["options"] = json.dumps(data["options"])
|
||||
return None, data
|
||||
|
||||
|
||||
def __process_circular(alert):
|
||||
if alert is None:
|
||||
return None
|
||||
alert.pop("deletedAt")
|
||||
alert["createdAt"] = TimeUTC.datetime_to_timestamp(alert["createdAt"])
|
||||
return alert
|
||||
|
||||
|
||||
def create(project_id, data):
|
||||
err, data = __transform_structure(data)
|
||||
if err is not None:
|
||||
return {"errors": [err]}
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify("""\
|
||||
INSERT INTO public.alerts(project_id, name, description, detection_method, query, options)
|
||||
VALUES (%(project_id)s, %(name)s, %(description)s, %(detectionMethod)s, %(query)s, %(options)s::jsonb)
|
||||
RETURNING *;""",
|
||||
{"project_id": project_id, **data})
|
||||
)
|
||||
a = helper.dict_to_camel_case(cur.fetchone())
|
||||
return {"data": helper.dict_to_camel_case(__process_circular(a))}
|
||||
|
||||
|
||||
def update(id, changes):
|
||||
changes = {k: changes[k] for k in changes.keys() if k in ALLOW_UPDATE}
|
||||
err, changes = __transform_structure(changes)
|
||||
if err is not None:
|
||||
return {"errors": [err]}
|
||||
updateq = []
|
||||
for k in changes.keys():
|
||||
updateq.append(f"{helper.key_to_snake_case(k)} = %({k})s")
|
||||
if len(updateq) == 0:
|
||||
return {"errors": ["nothing to update"]}
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(f"""\
|
||||
UPDATE public.alerts
|
||||
SET {", ".join(updateq)}
|
||||
WHERE alert_id =%(id)s AND deleted_at ISNULL
|
||||
RETURNING *;""",
|
||||
{"id": id, **changes})
|
||||
cur.execute(query=query)
|
||||
a = helper.dict_to_camel_case(cur.fetchone())
|
||||
return {"data": __process_circular(a)}
|
||||
|
||||
|
||||
def process_notifications(data):
|
||||
full = {}
|
||||
for n in data:
|
||||
if "message" in n["options"]:
|
||||
webhook_data = {}
|
||||
if "data" in n["options"]:
|
||||
webhook_data = n["options"].pop("data")
|
||||
for c in n["options"].pop("message"):
|
||||
if c["type"] not in full:
|
||||
full[c["type"]] = []
|
||||
if c["type"] in ["slack", "email"]:
|
||||
full[c["type"]].append({
|
||||
"notification": n,
|
||||
"destination": c["value"]
|
||||
})
|
||||
elif c["type"] in ["webhook"]:
|
||||
full[c["type"]].append({"data": webhook_data, "destination": c["value"]})
|
||||
notifications.create(data)
|
||||
BATCH_SIZE = 200
|
||||
for t in full.keys():
|
||||
for i in range(0, len(full[t]), BATCH_SIZE):
|
||||
helper.async_post(environ['alert_ntf'] % t, {"notifications": full[t][i:i + BATCH_SIZE]})
|
||||
|
||||
|
||||
def send_by_email(notification, destination):
|
||||
if notification is None:
|
||||
return
|
||||
email_helper.alert_email(recipients=destination,
|
||||
subject=f'"{notification["title"]}" has been triggered',
|
||||
data={
|
||||
"message": f'"{notification["title"]}" {notification["description"]}',
|
||||
"project_id": notification["options"]["projectId"]})
|
||||
|
||||
|
||||
def send_by_email_batch(notifications_list):
|
||||
if notifications_list is None or len(notifications_list) == 0:
|
||||
return
|
||||
for n in notifications_list:
|
||||
send_by_email(notification=n.get("notification"), destination=n.get("destination"))
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def delete(project_id, alert_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify("""\
|
||||
UPDATE public.alerts
|
||||
SET
|
||||
deleted_at = timezone('utc'::text, now()),
|
||||
active = FALSE
|
||||
WHERE
|
||||
alert_id = %(alert_id)s AND project_id=%(project_id)s;""",
|
||||
{"alert_id": alert_id, "project_id": project_id})
|
||||
)
|
||||
return {"data": {"state": "success"}}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import logging
|
||||
|
||||
from decouple import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if config("EXP_ALERTS", cast=bool, default=False):
|
||||
logging.info(">>> Using experimental alerts")
|
||||
from . import alerts_processor_ch as alerts_processor
|
||||
else:
|
||||
from . import alerts_processor as alerts_processor
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from decouple import config
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import notifications, webhook
|
||||
from chalicelib.core.collaborations.collaboration_msteams import MSTeams
|
||||
from chalicelib.core.collaborations.collaboration_slack import Slack
|
||||
from chalicelib.utils import pg_client, helper, email_helper, smtp
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get(id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify("""\
|
||||
SELECT *
|
||||
FROM public.alerts
|
||||
WHERE alert_id =%(id)s;""",
|
||||
{"id": id})
|
||||
)
|
||||
a = helper.dict_to_camel_case(cur.fetchone())
|
||||
return helper.custom_alert_to_front(__process_circular(a))
|
||||
|
||||
|
||||
def get_all(project_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify("""\
|
||||
SELECT alerts.*,
|
||||
COALESCE(metrics.name || '.' || (COALESCE(metric_series.name, 'series ' || index)) || '.count',
|
||||
query ->> 'left') AS series_name
|
||||
FROM public.alerts
|
||||
LEFT JOIN metric_series USING (series_id)
|
||||
LEFT JOIN metrics USING (metric_id)
|
||||
WHERE alerts.project_id =%(project_id)s
|
||||
AND alerts.deleted_at ISNULL
|
||||
ORDER BY alerts.created_at;""",
|
||||
{"project_id": project_id})
|
||||
cur.execute(query=query)
|
||||
all = helper.list_to_camel_case(cur.fetchall())
|
||||
for i in range(len(all)):
|
||||
all[i] = helper.custom_alert_to_front(__process_circular(all[i]))
|
||||
return all
|
||||
|
||||
|
||||
def __process_circular(alert):
|
||||
if alert is None:
|
||||
return None
|
||||
alert.pop("deletedAt")
|
||||
alert["createdAt"] = TimeUTC.datetime_to_timestamp(alert["createdAt"])
|
||||
return alert
|
||||
|
||||
|
||||
def create(project_id, data: schemas.AlertSchema):
|
||||
data = data.model_dump()
|
||||
data["query"] = json.dumps(data["query"])
|
||||
data["options"] = json.dumps(data["options"])
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify("""\
|
||||
INSERT INTO public.alerts(project_id, name, description, detection_method, query, options, series_id, change)
|
||||
VALUES (%(project_id)s, %(name)s, %(description)s, %(detection_method)s, %(query)s, %(options)s::jsonb, %(series_id)s, %(change)s)
|
||||
RETURNING *;""",
|
||||
{"project_id": project_id, **data})
|
||||
)
|
||||
a = helper.dict_to_camel_case(cur.fetchone())
|
||||
return {"data": helper.custom_alert_to_front(helper.dict_to_camel_case(__process_circular(a)))}
|
||||
|
||||
|
||||
def update(id, data: schemas.AlertSchema):
|
||||
data = data.model_dump()
|
||||
data["query"] = json.dumps(data["query"])
|
||||
data["options"] = json.dumps(data["options"])
|
||||
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify("""\
|
||||
UPDATE public.alerts
|
||||
SET name = %(name)s,
|
||||
description = %(description)s,
|
||||
active = TRUE,
|
||||
detection_method = %(detection_method)s,
|
||||
query = %(query)s,
|
||||
options = %(options)s,
|
||||
series_id = %(series_id)s,
|
||||
change = %(change)s
|
||||
WHERE alert_id =%(id)s AND deleted_at ISNULL
|
||||
RETURNING *;""",
|
||||
{"id": id, **data})
|
||||
cur.execute(query=query)
|
||||
a = helper.dict_to_camel_case(cur.fetchone())
|
||||
return {"data": helper.custom_alert_to_front(__process_circular(a))}
|
||||
|
||||
|
||||
def process_notifications(data):
|
||||
full = {}
|
||||
for n in data:
|
||||
if "message" in n["options"]:
|
||||
webhook_data = {}
|
||||
if "data" in n["options"]:
|
||||
webhook_data = n["options"].pop("data")
|
||||
for c in n["options"].pop("message"):
|
||||
if c["type"] not in full:
|
||||
full[c["type"]] = []
|
||||
if c["type"] in ["slack", "msteams", "email"]:
|
||||
full[c["type"]].append({
|
||||
"notification": n,
|
||||
"destination": c["value"]
|
||||
})
|
||||
elif c["type"] in ["webhook"]:
|
||||
full[c["type"]].append({"data": webhook_data, "destination": c["value"]})
|
||||
notifications.create(data)
|
||||
BATCH_SIZE = 200
|
||||
for t in full.keys():
|
||||
for i in range(0, len(full[t]), 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
|
||||
|
||||
if t == "slack":
|
||||
try:
|
||||
send_to_slack_batch(notifications_list=notifications_list)
|
||||
except Exception as e:
|
||||
logger.error("!!!Error while sending slack notifications batch")
|
||||
logger.error(str(e))
|
||||
elif t == "msteams":
|
||||
try:
|
||||
send_to_msteams_batch(notifications_list=notifications_list)
|
||||
except Exception as e:
|
||||
logger.error("!!!Error while sending msteams notifications batch")
|
||||
logger.error(str(e))
|
||||
elif t == "email":
|
||||
try:
|
||||
send_by_email_batch(notifications_list=notifications_list)
|
||||
except Exception as e:
|
||||
logger.error("!!!Error while sending email notifications batch")
|
||||
logger.error(str(e))
|
||||
elif t == "webhook":
|
||||
try:
|
||||
webhook.trigger_batch(data_list=notifications_list)
|
||||
except Exception as e:
|
||||
logger.error("!!!Error while sending webhook notifications batch")
|
||||
logger.error(str(e))
|
||||
|
||||
|
||||
def send_by_email(notification, destination):
|
||||
if notification is None:
|
||||
return
|
||||
email_helper.alert_email(recipients=destination,
|
||||
subject=f'"{notification["title"]}" has been triggered',
|
||||
data={
|
||||
"message": f'"{notification["title"]}" {notification["description"]}',
|
||||
"project_id": notification["options"]["projectId"]})
|
||||
|
||||
|
||||
def send_by_email_batch(notifications_list):
|
||||
if not smtp.has_smtp():
|
||||
logger.info("no SMTP configuration for email notifications")
|
||||
if notifications_list is None or len(notifications_list) == 0:
|
||||
logger.info("no email notifications")
|
||||
return
|
||||
for n in notifications_list:
|
||||
send_by_email(notification=n.get("notification"), destination=n.get("destination"))
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def send_to_slack_batch(notifications_list):
|
||||
webhookId_map = {}
|
||||
for n in notifications_list:
|
||||
if n.get("destination") not in webhookId_map:
|
||||
webhookId_map[n.get("destination")] = {"tenantId": n["notification"]["tenantId"], "batch": []}
|
||||
webhookId_map[n.get("destination")]["batch"].append({"text": n["notification"]["description"] \
|
||||
+ f"\n<{config('SITE_URL')}{n['notification']['buttonUrl']}|{n['notification']['buttonText']}>",
|
||||
"title": n["notification"]["title"],
|
||||
"title_link": n["notification"]["buttonUrl"],
|
||||
"ts": datetime.now().timestamp()})
|
||||
for batch in webhookId_map.keys():
|
||||
Slack.send_batch(tenant_id=webhookId_map[batch]["tenantId"], webhook_id=batch,
|
||||
attachments=webhookId_map[batch]["batch"])
|
||||
|
||||
|
||||
def send_to_msteams_batch(notifications_list):
|
||||
webhookId_map = {}
|
||||
for n in notifications_list:
|
||||
if n.get("destination") not in webhookId_map:
|
||||
webhookId_map[n.get("destination")] = {"tenantId": n["notification"]["tenantId"], "batch": []}
|
||||
|
||||
link = f"{config('SITE_URL')}{n['notification']['buttonUrl']}"
|
||||
# for MSTeams, the batch is the list of `sections`
|
||||
webhookId_map[n.get("destination")]["batch"].append(
|
||||
{
|
||||
"activityTitle": n["notification"]["title"],
|
||||
"activitySubtitle": f"On Project *{n['notification']['projectName']}*",
|
||||
"facts": [
|
||||
{
|
||||
"name": "Target:",
|
||||
"value": link
|
||||
},
|
||||
{
|
||||
"name": "Description:",
|
||||
"value": n["notification"]["description"]
|
||||
}],
|
||||
"markdown": True
|
||||
}
|
||||
)
|
||||
for batch in webhookId_map.keys():
|
||||
MSTeams.send_batch(tenant_id=webhookId_map[batch]["tenantId"], webhook_id=batch,
|
||||
attachments=webhookId_map[batch]["batch"])
|
||||
|
||||
|
||||
def delete(project_id, alert_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify(""" UPDATE public.alerts
|
||||
SET deleted_at = timezone('utc'::text, now()),
|
||||
active = FALSE
|
||||
WHERE alert_id = %(alert_id)s AND project_id=%(project_id)s;""",
|
||||
{"alert_id": alert_id, "project_id": project_id})
|
||||
)
|
||||
return {"data": {"state": "success"}}
|
||||
|
||||
|
||||
def get_predefined_values():
|
||||
values = [e.value for e in schemas.AlertColumn]
|
||||
values = [{"name": v, "value": v,
|
||||
"unit": "count" if v.endswith(".count") else "ms",
|
||||
"predefined": True,
|
||||
"metricId": None,
|
||||
"seriesId": None} for v in values if v != schemas.AlertColumn.CUSTOM]
|
||||
return values
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
from chalicelib.core.alerts.modules import TENANT_ID
|
||||
from chalicelib.utils import pg_client, helper
|
||||
|
||||
|
||||
def get_all_alerts():
|
||||
with pg_client.PostgresClient(long_query=True) as cur:
|
||||
query = f"""SELECT {TENANT_ID} AS tenant_id,
|
||||
alert_id,
|
||||
projects.project_id,
|
||||
projects.name AS project_name,
|
||||
detection_method,
|
||||
query,
|
||||
options,
|
||||
(EXTRACT(EPOCH FROM alerts.created_at) * 1000)::BIGINT AS created_at,
|
||||
alerts.name,
|
||||
alerts.series_id,
|
||||
filter,
|
||||
change,
|
||||
COALESCE(metrics.name || '.' || (COALESCE(metric_series.name, 'series ' || index)) || '.count',
|
||||
query ->> 'left') AS series_name
|
||||
FROM public.alerts
|
||||
INNER JOIN projects USING (project_id)
|
||||
LEFT JOIN metric_series USING (series_id)
|
||||
LEFT JOIN metrics USING (metric_id)
|
||||
WHERE alerts.deleted_at ISNULL
|
||||
AND alerts.active
|
||||
AND projects.active
|
||||
AND projects.deleted_at ISNULL
|
||||
AND (alerts.series_id ISNULL OR metric_series.deleted_at ISNULL)
|
||||
ORDER BY alerts.created_at;"""
|
||||
cur.execute(query=query)
|
||||
all_alerts = helper.list_to_camel_case(cur.fetchall())
|
||||
return all_alerts
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
import logging
|
||||
|
||||
from pydantic_core._pydantic_core import ValidationError
|
||||
|
||||
import schemas
|
||||
from chalicelib.core.alerts import alerts, alerts_listener
|
||||
from chalicelib.core.alerts.modules import alert_helpers
|
||||
from chalicelib.core.sessions import sessions_pg as sessions
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LeftToDb = {
|
||||
schemas.AlertColumn.PERFORMANCE__DOM_CONTENT_LOADED__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)",
|
||||
"formula": "COALESCE(AVG(NULLIF(dom_content_loaded_time ,0)),0)"},
|
||||
schemas.AlertColumn.PERFORMANCE__FIRST_MEANINGFUL_PAINT__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)",
|
||||
"formula": "COALESCE(AVG(NULLIF(first_contentful_paint_time,0)),0)"},
|
||||
schemas.AlertColumn.PERFORMANCE__PAGE_LOAD_TIME__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)", "formula": "AVG(NULLIF(load_time ,0))"},
|
||||
schemas.AlertColumn.PERFORMANCE__DOM_BUILD_TIME__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)",
|
||||
"formula": "AVG(NULLIF(dom_building_time,0))"},
|
||||
schemas.AlertColumn.PERFORMANCE__SPEED_INDEX__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)", "formula": "AVG(NULLIF(speed_index,0))"},
|
||||
schemas.AlertColumn.PERFORMANCE__PAGE_RESPONSE_TIME__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)",
|
||||
"formula": "AVG(NULLIF(response_time,0))"},
|
||||
schemas.AlertColumn.PERFORMANCE__TTFB__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)",
|
||||
"formula": "AVG(NULLIF(first_paint_time,0))"},
|
||||
schemas.AlertColumn.PERFORMANCE__TIME_TO_RENDER__AVERAGE: {
|
||||
"table": "events.pages INNER JOIN public.sessions USING(session_id)",
|
||||
"formula": "AVG(NULLIF(visually_complete,0))"},
|
||||
schemas.AlertColumn.PERFORMANCE__CRASHES__COUNT: {
|
||||
"table": "public.sessions",
|
||||
"formula": "COUNT(DISTINCT session_id)",
|
||||
"condition": "errors_count > 0 AND duration>0"},
|
||||
schemas.AlertColumn.ERRORS__JAVASCRIPT__COUNT: {
|
||||
"table": "events.errors INNER JOIN public.errors AS m_errors USING (error_id)",
|
||||
"formula": "COUNT(DISTINCT session_id)", "condition": "source='js_exception'", "joinSessions": False},
|
||||
schemas.AlertColumn.ERRORS__BACKEND__COUNT: {
|
||||
"table": "events.errors INNER JOIN public.errors AS m_errors USING (error_id)",
|
||||
"formula": "COUNT(DISTINCT session_id)", "condition": "source!='js_exception'", "joinSessions": False},
|
||||
}
|
||||
|
||||
|
||||
def Build(a):
|
||||
now = TimeUTC.now()
|
||||
params = {"project_id": a["projectId"], "now": now}
|
||||
full_args = {}
|
||||
j_s = True
|
||||
main_table = ""
|
||||
if a["seriesId"] is not None:
|
||||
a["filter"]["sort"] = "session_id"
|
||||
a["filter"]["order"] = schemas.SortOrderType.DESC
|
||||
a["filter"]["startDate"] = 0
|
||||
a["filter"]["endDate"] = TimeUTC.now()
|
||||
try:
|
||||
data = schemas.SessionsSearchPayloadSchema.model_validate(a["filter"])
|
||||
except ValidationError:
|
||||
logger.warning("Validation error for:")
|
||||
logger.warning(a["filter"])
|
||||
raise
|
||||
|
||||
full_args, query_part = sessions.search_query_parts(data=data, error_status=None, errors_only=False,
|
||||
issue=None, project_id=a["projectId"], user_id=None,
|
||||
favorite_only=False)
|
||||
subQ = f"""SELECT COUNT(session_id) AS value
|
||||
{query_part}"""
|
||||
else:
|
||||
colDef = LeftToDb[a["query"]["left"]]
|
||||
subQ = f"""SELECT {colDef["formula"]} AS value
|
||||
FROM {colDef["table"]}
|
||||
WHERE project_id = %(project_id)s
|
||||
{"AND " + colDef["condition"] if colDef.get("condition") else ""}"""
|
||||
j_s = colDef.get("joinSessions", True)
|
||||
main_table = colDef["table"]
|
||||
is_ss = main_table == "public.sessions"
|
||||
q = f"""SELECT coalesce(value,0) AS value, coalesce(value,0) {a["query"]["operator"]} {a["query"]["right"]} AS valid"""
|
||||
|
||||
if a["detectionMethod"] == schemas.AlertDetectionMethod.THRESHOLD:
|
||||
if a["seriesId"] is not None:
|
||||
q += f""" FROM ({subQ}) AS stat"""
|
||||
else:
|
||||
q += f""" FROM ({subQ} {"AND timestamp >= %(startDate)s AND timestamp <= %(now)s" if not is_ss else ""}
|
||||
{"AND start_ts >= %(startDate)s AND start_ts <= %(now)s" if j_s else ""}) AS stat"""
|
||||
params = {**params, **full_args, "startDate": TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000}
|
||||
else:
|
||||
if a["change"] == schemas.AlertDetectionType.CHANGE:
|
||||
if a["seriesId"] is not None:
|
||||
sub2 = subQ.replace("%(startDate)s", "%(timestamp_sub2)s").replace("%(endDate)s", "%(startDate)s")
|
||||
sub1 = f"SELECT (({subQ})-({sub2})) AS value"
|
||||
q += f" FROM ( {sub1} ) AS stat"
|
||||
params = {**params, **full_args,
|
||||
"startDate": TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000,
|
||||
"timestamp_sub2": TimeUTC.now() - 2 * a["options"]["currentPeriod"] * 60 * 1000}
|
||||
else:
|
||||
sub1 = f"""{subQ} {"AND timestamp >= %(startDate)s AND timestamp <= %(now)s" if not is_ss else ""}
|
||||
{"AND start_ts >= %(startDate)s AND start_ts <= %(now)s" if j_s else ""}"""
|
||||
params["startDate"] = TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000
|
||||
sub2 = f"""{subQ} {"AND timestamp < %(startDate)s AND timestamp >= %(timestamp_sub2)s" if not is_ss else ""}
|
||||
{"AND start_ts < %(startDate)s AND start_ts >= %(timestamp_sub2)s" if j_s else ""}"""
|
||||
params["timestamp_sub2"] = TimeUTC.now() - 2 * a["options"]["currentPeriod"] * 60 * 1000
|
||||
sub1 = f"SELECT (( {sub1} )-( {sub2} )) AS value"
|
||||
q += f" FROM ( {sub1} ) AS stat"
|
||||
|
||||
else:
|
||||
if a["seriesId"] is not None:
|
||||
sub2 = subQ.replace("%(startDate)s", "%(timestamp_sub2)s").replace("%(endDate)s", "%(startDate)s")
|
||||
sub1 = f"SELECT (({subQ})/NULLIF(({sub2}),0)-1)*100 AS value"
|
||||
q += f" FROM ({sub1}) AS stat"
|
||||
params = {**params, **full_args,
|
||||
"startDate": TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000,
|
||||
"timestamp_sub2": TimeUTC.now() \
|
||||
- (a["options"]["currentPeriod"] + a["options"]["currentPeriod"]) \
|
||||
* 60 * 1000}
|
||||
else:
|
||||
sub1 = f"""{subQ} {"AND timestamp >= %(startDate)s AND timestamp <= %(now)s" if not is_ss else ""}
|
||||
{"AND start_ts >= %(startDate)s AND start_ts <= %(now)s" if j_s else ""}"""
|
||||
params["startDate"] = TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000
|
||||
sub2 = f"""{subQ} {"AND timestamp < %(startDate)s AND timestamp >= %(timestamp_sub2)s" if not is_ss else ""}
|
||||
{"AND start_ts < %(startDate)s AND start_ts >= %(timestamp_sub2)s" if j_s else ""}"""
|
||||
params["timestamp_sub2"] = TimeUTC.now() \
|
||||
- (a["options"]["currentPeriod"] + a["options"]["currentPeriod"]) * 60 * 1000
|
||||
sub1 = f"SELECT (({sub1})/NULLIF(({sub2}),0)-1)*100 AS value"
|
||||
q += f" FROM ({sub1}) AS stat"
|
||||
|
||||
return q, params
|
||||
|
||||
|
||||
def process():
|
||||
logger.info("> processing alerts on PG")
|
||||
notifications = []
|
||||
all_alerts = alerts_listener.get_all_alerts()
|
||||
with pg_client.PostgresClient() as cur:
|
||||
for alert in all_alerts:
|
||||
if alert_helpers.can_check(alert):
|
||||
query, params = Build(alert)
|
||||
try:
|
||||
query = cur.mogrify(query, params)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"!!!Error while building alert query for alertId:{alert['alertId']} name: {alert['name']}")
|
||||
logger.error(e)
|
||||
continue
|
||||
logger.debug(alert)
|
||||
logger.debug(query)
|
||||
try:
|
||||
cur.execute(query)
|
||||
result = cur.fetchone()
|
||||
if result["valid"]:
|
||||
logger.info(f"Valid alert, notifying users, alertId:{alert['alertId']} name: {alert['name']}")
|
||||
notifications.append(alert_helpers.generate_notification(alert, result))
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"!!!Error while running alert query for alertId:{alert['alertId']} name: {alert['name']}")
|
||||
logger.error(query)
|
||||
logger.error(e)
|
||||
cur = cur.recreate(rollback=True)
|
||||
if len(notifications) > 0:
|
||||
cur.execute(
|
||||
cur.mogrify(f"""UPDATE public.alerts
|
||||
SET options = options||'{{"lastNotification":{TimeUTC.now()}}}'::jsonb
|
||||
WHERE alert_id IN %(ids)s;""", {"ids": tuple([n["alertId"] for n in notifications])}))
|
||||
if len(notifications) > 0:
|
||||
alerts.process_notifications(notifications)
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import logging
|
||||
|
||||
from pydantic_core._pydantic_core import ValidationError
|
||||
|
||||
import schemas
|
||||
from chalicelib.utils import pg_client, ch_client, exp_ch_helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.core.alerts import alerts, alerts_listener
|
||||
from chalicelib.core.alerts.modules import alert_helpers
|
||||
from chalicelib.core.sessions import sessions_ch as sessions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LeftToDb = {
|
||||
schemas.AlertColumn.PERFORMANCE__DOM_CONTENT_LOADED__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "COALESCE(AVG(NULLIF(dom_content_loaded_event_time ,0)),0)",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__FIRST_MEANINGFUL_PAINT__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "COALESCE(AVG(NULLIF(first_contentful_paint_time,0)),0)",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__PAGE_LOAD_TIME__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "AVG(NULLIF(load_event_time ,0))",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__DOM_BUILD_TIME__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "AVG(NULLIF(dom_building_time,0))",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__SPEED_INDEX__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "AVG(NULLIF(speed_index,0))",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__PAGE_RESPONSE_TIME__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "AVG(NULLIF(response_time,0))",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__TTFB__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "AVG(NULLIF(first_contentful_paint_time,0))",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__TIME_TO_RENDER__AVERAGE: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS pages",
|
||||
"formula": "AVG(NULLIF(visually_complete,0))",
|
||||
"eventType": "LOCATION"
|
||||
},
|
||||
schemas.AlertColumn.PERFORMANCE__CRASHES__COUNT: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_sessions_table(timestamp)} AS sessions",
|
||||
"formula": "COUNT(DISTINCT session_id)",
|
||||
"condition": "duration>0 AND errors_count>0"
|
||||
},
|
||||
schemas.AlertColumn.ERRORS__JAVASCRIPT__COUNT: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS errors",
|
||||
"eventType": "ERROR",
|
||||
"formula": "COUNT(DISTINCT session_id)",
|
||||
"condition": "source='js_exception'"
|
||||
},
|
||||
schemas.AlertColumn.ERRORS__BACKEND__COUNT: {
|
||||
"table": lambda timestamp: f"{exp_ch_helper.get_main_events_table(timestamp)} AS errors",
|
||||
"eventType": "ERROR",
|
||||
"formula": "COUNT(DISTINCT session_id)",
|
||||
"condition": "source!='js_exception'"
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def Build(a):
|
||||
now = TimeUTC.now()
|
||||
params = {"project_id": a["projectId"], "now": now}
|
||||
full_args = {}
|
||||
if a["seriesId"] is not None:
|
||||
a["filter"]["sort"] = "session_id"
|
||||
a["filter"]["order"] = schemas.SortOrderType.DESC
|
||||
a["filter"]["startDate"] = 0
|
||||
a["filter"]["endDate"] = TimeUTC.now()
|
||||
try:
|
||||
data = schemas.SessionsSearchPayloadSchema.model_validate(a["filter"])
|
||||
except ValidationError:
|
||||
logger.warning("Validation error for:")
|
||||
logger.warning(a["filter"])
|
||||
raise
|
||||
|
||||
full_args, query_part = sessions.search_query_parts_ch(data=data, error_status=None, errors_only=False,
|
||||
issue=None, project_id=a["projectId"], user_id=None,
|
||||
favorite_only=False)
|
||||
subQ = f"""SELECT COUNT(session_id) AS value
|
||||
{query_part}"""
|
||||
else:
|
||||
colDef = LeftToDb[a["query"]["left"]]
|
||||
params["event_type"] = LeftToDb[a["query"]["left"]].get("eventType")
|
||||
subQ = f"""SELECT {colDef["formula"]} AS value
|
||||
FROM {colDef["table"](now)}
|
||||
WHERE project_id = %(project_id)s
|
||||
{"AND event_type=%(event_type)s" if params["event_type"] else ""}
|
||||
{"AND " + colDef["condition"] if colDef.get("condition") else ""}"""
|
||||
|
||||
q = f"""SELECT coalesce(value,0) AS value, coalesce(value,0) {a["query"]["operator"]} {a["query"]["right"]} AS valid"""
|
||||
|
||||
if a["detectionMethod"] == schemas.AlertDetectionMethod.THRESHOLD:
|
||||
if a["seriesId"] is not None:
|
||||
q += f""" FROM ({subQ}) AS stat"""
|
||||
else:
|
||||
q += f""" FROM ({subQ}
|
||||
AND datetime>=toDateTime(%(startDate)s/1000)
|
||||
AND datetime<=toDateTime(%(now)s/1000) ) AS stat"""
|
||||
params = {**params, **full_args, "startDate": TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000}
|
||||
else:
|
||||
if a["change"] == schemas.AlertDetectionType.CHANGE:
|
||||
if a["seriesId"] is not None:
|
||||
sub2 = subQ.replace("%(startDate)s", "%(timestamp_sub2)s").replace("%(endDate)s", "%(startDate)s")
|
||||
sub1 = f"SELECT (({subQ})-({sub2})) AS value"
|
||||
q += f" FROM ( {sub1} ) AS stat"
|
||||
params = {**params, **full_args,
|
||||
"startDate": TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000,
|
||||
"timestamp_sub2": TimeUTC.now() - 2 * a["options"]["currentPeriod"] * 60 * 1000}
|
||||
else:
|
||||
sub1 = f"""{subQ} AND datetime>=toDateTime(%(startDate)s/1000)
|
||||
AND datetime<=toDateTime(%(now)s/1000)"""
|
||||
params["startDate"] = TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000
|
||||
sub2 = f"""{subQ} AND datetime<toDateTime(%(startDate)s/1000)
|
||||
AND datetime>=toDateTime(%(timestamp_sub2)s/1000)"""
|
||||
params["timestamp_sub2"] = TimeUTC.now() - 2 * a["options"]["currentPeriod"] * 60 * 1000
|
||||
sub1 = f"SELECT (( {sub1} )-( {sub2} )) AS value"
|
||||
q += f" FROM ( {sub1} ) AS stat"
|
||||
|
||||
else:
|
||||
if a["seriesId"] is not None:
|
||||
sub2 = subQ.replace("%(startDate)s", "%(timestamp_sub2)s").replace("%(endDate)s", "%(startDate)s")
|
||||
sub1 = f"SELECT (({subQ})/NULLIF(({sub2}),0)-1)*100 AS value"
|
||||
q += f" FROM ({sub1}) AS stat"
|
||||
params = {**params, **full_args,
|
||||
"startDate": TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000,
|
||||
"timestamp_sub2": TimeUTC.now() \
|
||||
- (a["options"]["currentPeriod"] + a["options"]["currentPeriod"]) \
|
||||
* 60 * 1000}
|
||||
else:
|
||||
sub1 = f"""{subQ} AND datetime>=toDateTime(%(startDate)s/1000)
|
||||
AND datetime<=toDateTime(%(now)s/1000)"""
|
||||
params["startDate"] = TimeUTC.now() - a["options"]["currentPeriod"] * 60 * 1000
|
||||
sub2 = f"""{subQ} AND datetime<toDateTime(%(startDate)s/1000)
|
||||
AND datetime>=toDateTime(%(timestamp_sub2)s/1000)"""
|
||||
params["timestamp_sub2"] = TimeUTC.now() \
|
||||
- (a["options"]["currentPeriod"] + a["options"]["currentPeriod"]) * 60 * 1000
|
||||
sub1 = f"SELECT (({sub1})/NULLIF(({sub2}),0)-1)*100 AS value"
|
||||
q += f" FROM ({sub1}) AS stat"
|
||||
|
||||
return q, params
|
||||
|
||||
|
||||
def process():
|
||||
logger.info("> processing alerts on CH")
|
||||
notifications = []
|
||||
all_alerts = alerts_listener.get_all_alerts()
|
||||
with pg_client.PostgresClient() as cur, ch_client.ClickHouseClient() as ch_cur:
|
||||
for alert in all_alerts:
|
||||
if alert["query"]["left"] != "CUSTOM":
|
||||
continue
|
||||
if alert_helpers.can_check(alert):
|
||||
query, params = Build(alert)
|
||||
try:
|
||||
query = ch_cur.format(query=query, parameters=params)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"!!!Error while building alert query for alertId:{alert['alertId']} name: {alert['name']}")
|
||||
logger.error(e)
|
||||
continue
|
||||
logger.debug(alert)
|
||||
logger.debug(query)
|
||||
try:
|
||||
result = ch_cur.execute(query=query)
|
||||
if len(result) > 0:
|
||||
result = result[0]
|
||||
|
||||
if result["valid"]:
|
||||
logger.info("Valid alert, notifying users")
|
||||
notifications.append(alert_helpers.generate_notification(alert, result))
|
||||
except Exception as e:
|
||||
logger.error(f"!!!Error while running alert query for alertId:{alert['alertId']}")
|
||||
logger.error(str(e))
|
||||
logger.error(query)
|
||||
if len(notifications) > 0:
|
||||
cur.execute(
|
||||
cur.mogrify(f"""UPDATE public.alerts
|
||||
SET options = options||'{{"lastNotification":{TimeUTC.now()}}}'::jsonb
|
||||
WHERE alert_id IN %(ids)s;""", {"ids": tuple([n["alertId"] for n in notifications])}))
|
||||
if len(notifications) > 0:
|
||||
alerts.process_notifications(notifications)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
TENANT_ID = "-1"
|
||||
|
||||
from . import helpers as alert_helpers
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import decimal
|
||||
import logging
|
||||
|
||||
import schemas
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# This is the frequency of execution for each threshold
|
||||
TimeInterval = {
|
||||
15: 3,
|
||||
30: 5,
|
||||
60: 10,
|
||||
120: 20,
|
||||
240: 30,
|
||||
1440: 60,
|
||||
}
|
||||
|
||||
|
||||
def __format_value(x):
|
||||
if x % 1 == 0:
|
||||
x = int(x)
|
||||
else:
|
||||
x = round(x, 2)
|
||||
return f"{x:,}"
|
||||
|
||||
|
||||
def can_check(a) -> bool:
|
||||
now = TimeUTC.now()
|
||||
|
||||
repetitionBase = a["options"]["currentPeriod"] \
|
||||
if a["detectionMethod"] == schemas.AlertDetectionMethod.CHANGE \
|
||||
and a["options"]["currentPeriod"] > a["options"]["previousPeriod"] \
|
||||
else a["options"]["previousPeriod"]
|
||||
|
||||
if TimeInterval.get(repetitionBase) is None:
|
||||
logger.error(f"repetitionBase: {repetitionBase} NOT FOUND")
|
||||
return False
|
||||
|
||||
return (a["options"]["renotifyInterval"] <= 0 or
|
||||
a["options"].get("lastNotification") is None or
|
||||
a["options"]["lastNotification"] <= 0 or
|
||||
((now - a["options"]["lastNotification"]) > a["options"]["renotifyInterval"] * 60 * 1000)) \
|
||||
and ((now - a["createdAt"]) % (TimeInterval[repetitionBase] * 60 * 1000)) < 60 * 1000
|
||||
|
||||
|
||||
def generate_notification(alert, result):
|
||||
left = __format_value(result['value'])
|
||||
right = __format_value(alert['query']['right'])
|
||||
return {
|
||||
"alertId": alert["alertId"],
|
||||
"tenantId": alert["tenantId"],
|
||||
"title": alert["name"],
|
||||
"description": f"{alert['seriesName']} = {left} ({alert['query']['operator']} {right}).",
|
||||
"buttonText": "Check metrics for more details",
|
||||
"buttonUrl": f"/{alert['projectId']}/metrics",
|
||||
"imageUrl": None,
|
||||
"projectId": alert["projectId"],
|
||||
"projectName": alert["projectName"],
|
||||
"options": {"source": "ALERT", "sourceId": alert["alertId"],
|
||||
"sourceMeta": alert["detectionMethod"],
|
||||
"message": alert["options"]["message"], "projectId": alert["projectId"],
|
||||
"data": {"title": alert["name"],
|
||||
"limitValue": alert["query"]["right"],
|
||||
"actualValue": float(result["value"]) \
|
||||
if isinstance(result["value"], decimal.Decimal) \
|
||||
else result["value"],
|
||||
"operator": alert["query"]["operator"],
|
||||
"trigger": alert["query"]["left"],
|
||||
"alertId": alert["alertId"],
|
||||
"detectionMethod": alert["detectionMethod"],
|
||||
"currentPeriod": alert["options"]["currentPeriod"],
|
||||
"previousPeriod": alert["options"]["previousPeriod"],
|
||||
"createdAt": TimeUTC.now()}},
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils import helper
|
||||
from decouple import config
|
||||
from chalicelib.utils.helper import environ
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ def get_all(user_id):
|
|||
for a in announcements:
|
||||
a["createdAt"] = TimeUTC.datetime_to_timestamp(a["createdAt"])
|
||||
if a["imageUrl"] is not None and len(a["imageUrl"]) > 0:
|
||||
a["imageUrl"] = config("announcement_url") + a["imageUrl"]
|
||||
a["imageUrl"] = environ["announcement_url"] + a["imageUrl"]
|
||||
return announcements
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,287 +1,74 @@
|
|||
import logging
|
||||
from os import access, R_OK
|
||||
from os.path import exists as path_exists, getsize
|
||||
|
||||
import jwt
|
||||
from chalicelib.utils import pg_client, helper
|
||||
from chalicelib.core import projects, sessions, sessions_metas
|
||||
import requests
|
||||
from decouple import config
|
||||
from fastapi import HTTPException, status
|
||||
from chalicelib.utils.helper import environ
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import projects
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ASSIST_KEY = config("ASSIST_KEY")
|
||||
ASSIST_URL = config("ASSIST_URL") % ASSIST_KEY
|
||||
SESSION_PROJECTION_COLS = """s.project_id,
|
||||
s.session_id::text AS session_id,
|
||||
s.user_uuid,
|
||||
s.user_id,
|
||||
s.user_agent,
|
||||
s.user_os,
|
||||
s.user_browser,
|
||||
s.user_device,
|
||||
s.user_device_type,
|
||||
s.user_country,
|
||||
s.start_ts,
|
||||
s.user_anonymous_id,
|
||||
s.platform
|
||||
"""
|
||||
|
||||
|
||||
def get_live_sessions_ws_user_id(project_id, user_id):
|
||||
data = {
|
||||
"filter": {"userId": user_id} if user_id else {}
|
||||
}
|
||||
return __get_live_sessions_ws(project_id=project_id, data=data)
|
||||
|
||||
|
||||
def get_live_sessions_ws_test_id(project_id, test_id):
|
||||
data = {
|
||||
"filter": {
|
||||
'uxtId': test_id,
|
||||
'operator': 'is'
|
||||
}
|
||||
}
|
||||
return __get_live_sessions_ws(project_id=project_id, data=data)
|
||||
|
||||
|
||||
def get_live_sessions_ws(project_id, body: schemas.LiveSessionsSearchPayloadSchema):
|
||||
data = {
|
||||
"filter": {},
|
||||
"pagination": {"limit": body.limit, "page": body.page},
|
||||
"sort": {"key": body.sort, "order": body.order}
|
||||
}
|
||||
for f in body.filters:
|
||||
if f.type == schemas.LiveFilterType.METADATA:
|
||||
data["filter"][f.source] = {"values": f.value, "operator": f.operator}
|
||||
|
||||
else:
|
||||
data["filter"][f.type] = {"values": f.value, "operator": f.operator}
|
||||
return __get_live_sessions_ws(project_id=project_id, data=data)
|
||||
|
||||
|
||||
def __get_live_sessions_ws(project_id, data):
|
||||
def get_live_sessions(project_id, filters=None):
|
||||
project_key = projects.get_project_key(project_id)
|
||||
try:
|
||||
results = requests.post(ASSIST_URL + config("assist") + f"/{project_key}",
|
||||
json=data, timeout=config("assistTimeout", cast=int, default=5))
|
||||
if results.status_code != 200:
|
||||
logger.error(f"!! issue with the peer-server code:{results.status_code} for __get_live_sessions_ws")
|
||||
logger.error(results.text)
|
||||
return {"total": 0, "sessions": []}
|
||||
live_peers = results.json().get("data", [])
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("!! Timeout getting Assist response")
|
||||
live_peers = {"total": 0, "sessions": []}
|
||||
except Exception as e:
|
||||
logger.error("!! Issue getting Live-Assist response")
|
||||
logger.exception(e)
|
||||
logger.error("expected JSON, received:")
|
||||
try:
|
||||
logger.error(results.text)
|
||||
except:
|
||||
logger.error("couldn't get response")
|
||||
live_peers = {"total": 0, "sessions": []}
|
||||
_live_peers = live_peers
|
||||
if "sessions" in live_peers:
|
||||
_live_peers = live_peers["sessions"]
|
||||
for s in _live_peers:
|
||||
s["live"] = True
|
||||
s["projectId"] = project_id
|
||||
if "projectID" in s:
|
||||
s.pop("projectID")
|
||||
return live_peers
|
||||
connected_peers = requests.get(environ["peers"] % environ["S3_KEY"] + f"/{project_key}")
|
||||
if connected_peers.status_code != 200:
|
||||
print("!! issue with the peer-server")
|
||||
print(connected_peers.text)
|
||||
return []
|
||||
connected_peers = connected_peers.json().get("data", [])
|
||||
|
||||
if len(connected_peers) == 0:
|
||||
return []
|
||||
connected_peers = tuple(connected_peers)
|
||||
extra_constraints = ["project_id = %(project_id)s", "session_id IN %(connected_peers)s"]
|
||||
extra_params = {}
|
||||
if filters is not None:
|
||||
for i, f in enumerate(filters):
|
||||
if not isinstance(f.get("value"), list):
|
||||
f["value"] = [f.get("value")]
|
||||
if len(f["value"]) == 0 or f["value"][0] is None:
|
||||
continue
|
||||
filter_type = f["type"].upper()
|
||||
f["value"] = sessions.__get_sql_value_multiple(f["value"])
|
||||
if filter_type == sessions_metas.meta_type.USERID:
|
||||
op = sessions.__get_sql_operator(f["operator"])
|
||||
extra_constraints.append(f"user_id {op} %(value_{i})s")
|
||||
extra_params[f"value_{i}"] = helper.string_to_sql_like_with_op(f["value"][0], op)
|
||||
|
||||
def __get_agent_token(project_id, project_key, session_id):
|
||||
iat = TimeUTC.now()
|
||||
return jwt.encode(
|
||||
payload={
|
||||
"projectKey": project_key,
|
||||
"projectId": project_id,
|
||||
"sessionId": session_id,
|
||||
"iat": iat // 1000,
|
||||
"exp": iat // 1000 + config("ASSIST_JWT_EXPIRATION", cast=int) + TimeUTC.get_utc_offset() // 1000,
|
||||
"iss": config("JWT_ISSUER"),
|
||||
"aud": f"openreplay:agent"
|
||||
},
|
||||
key=config("ASSIST_JWT_SECRET"),
|
||||
algorithm=config("JWT_ALGORITHM")
|
||||
)
|
||||
|
||||
|
||||
def get_live_session_by_id(project_id, session_id):
|
||||
project_key = projects.get_project_key(project_id)
|
||||
try:
|
||||
results = requests.get(ASSIST_URL + config("assist") + f"/{project_key}/{session_id}",
|
||||
timeout=config("assistTimeout", cast=int, default=5))
|
||||
if results.status_code != 200:
|
||||
logger.error(f"!! issue with the peer-server code:{results.status_code} for get_live_session_by_id")
|
||||
logger.error(results.text)
|
||||
return None
|
||||
results = results.json().get("data")
|
||||
if results is None:
|
||||
return None
|
||||
results["live"] = True
|
||||
results["agentToken"] = __get_agent_token(project_id=project_id, project_key=project_key, session_id=session_id)
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("!! Timeout getting Assist response")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("!! Issue getting Assist response")
|
||||
logger.exception(e)
|
||||
logger.error("expected JSON, received:")
|
||||
try:
|
||||
logger.error(results.text)
|
||||
except:
|
||||
logger.error("couldn't get response")
|
||||
return None
|
||||
return results
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(f"""\
|
||||
SELECT {SESSION_PROJECTION_COLS}, %(project_key)s||'-'|| session_id AS peer_id
|
||||
FROM public.sessions AS s
|
||||
WHERE {" AND ".join(extra_constraints)}
|
||||
ORDER BY start_ts DESC
|
||||
LIMIT 500;""",
|
||||
{"project_id": project_id,
|
||||
"connected_peers": connected_peers,
|
||||
"project_key": project_key,
|
||||
**extra_params})
|
||||
cur.execute(query)
|
||||
results = cur.fetchall()
|
||||
return helper.list_to_camel_case(results)
|
||||
|
||||
|
||||
def is_live(project_id, session_id, project_key=None):
|
||||
if project_key is None:
|
||||
project_key = projects.get_project_key(project_id)
|
||||
try:
|
||||
results = requests.get(ASSIST_URL + config("assistList") + f"/{project_key}/{session_id}",
|
||||
timeout=config("assistTimeout", cast=int, default=5))
|
||||
if results.status_code != 200:
|
||||
logger.error(f"!! issue with the peer-server code:{results.status_code} for is_live")
|
||||
logger.error(results.text)
|
||||
return False
|
||||
results = results.json().get("data")
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("!! Timeout getting Assist response")
|
||||
connected_peers = requests.get(environ["peers"] % environ["S3_KEY"] + f"/{project_key}")
|
||||
if connected_peers.status_code != 200:
|
||||
print("!! issue with the peer-server")
|
||||
print(connected_peers.text)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("!! Issue getting Assist response")
|
||||
logger.exception(e)
|
||||
logger.error("expected JSON, received:")
|
||||
try:
|
||||
logger.error(results.text)
|
||||
except:
|
||||
logger.error("couldn't get response")
|
||||
return False
|
||||
return str(session_id) == results
|
||||
|
||||
|
||||
def autocomplete(project_id, q: str, key: str = None):
|
||||
project_key = projects.get_project_key(project_id)
|
||||
params = {"q": q}
|
||||
if key:
|
||||
params["key"] = key
|
||||
try:
|
||||
results = requests.get(
|
||||
ASSIST_URL + config("assistList") + f"/{project_key}/autocomplete",
|
||||
params=params, timeout=config("assistTimeout", cast=int, default=5))
|
||||
if results.status_code != 200:
|
||||
logger.error(f"!! issue with the peer-server code:{results.status_code} for autocomplete")
|
||||
logger.error(results.text)
|
||||
return {"errors": [f"Something went wrong wile calling assist:{results.text}"]}
|
||||
results = results.json().get("data", [])
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("!! Timeout getting Assist response")
|
||||
return {"errors": ["Assist request timeout"]}
|
||||
except Exception as e:
|
||||
logger.error("!! Issue getting Assist response")
|
||||
logger.exception(e)
|
||||
logger.error("expected JSON, received:")
|
||||
try:
|
||||
logger.error(results.text)
|
||||
except:
|
||||
logger.error("couldn't get response")
|
||||
return {"errors": ["Something went wrong wile calling assist"]}
|
||||
for r in results:
|
||||
r["type"] = __change_keys(r["type"])
|
||||
return {"data": results}
|
||||
|
||||
|
||||
def __get_efs_path():
|
||||
efs_path = config("FS_DIR")
|
||||
if not path_exists(efs_path):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"EFS not found in path: {efs_path}")
|
||||
|
||||
if not access(efs_path, R_OK):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"EFS found under: {efs_path}; but it is not readable, please check permissions")
|
||||
return efs_path
|
||||
|
||||
|
||||
def __get_mob_path(project_id, session_id):
|
||||
params = {"projectId": project_id, "sessionId": session_id}
|
||||
return config("EFS_SESSION_MOB_PATTERN", default="%(sessionId)s") % params
|
||||
|
||||
|
||||
def get_raw_mob_by_id(project_id, session_id):
|
||||
efs_path = __get_efs_path()
|
||||
path_to_file = efs_path + "/" + __get_mob_path(project_id=project_id, session_id=session_id)
|
||||
if path_exists(path_to_file):
|
||||
if not access(path_to_file, R_OK):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Replay file found under: {efs_path};" +
|
||||
" but it is not readable, please check permissions")
|
||||
# getsize return size in bytes, UNPROCESSED_MAX_SIZE is in Kb
|
||||
if (getsize(path_to_file) / 1000) >= config("UNPROCESSED_MAX_SIZE", cast=int, default=200 * 1000):
|
||||
raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="Replay file too large")
|
||||
return path_to_file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def __get_devtools_path(project_id, session_id):
|
||||
params = {"projectId": project_id, "sessionId": session_id}
|
||||
return config("EFS_DEVTOOLS_MOB_PATTERN", default="%(sessionId)s") % params
|
||||
|
||||
|
||||
def get_raw_devtools_by_id(project_id, session_id):
|
||||
efs_path = __get_efs_path()
|
||||
path_to_file = efs_path + "/" + __get_devtools_path(project_id=project_id, session_id=session_id)
|
||||
if path_exists(path_to_file):
|
||||
if not access(path_to_file, R_OK):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Devtools file found under: {efs_path};"
|
||||
" but it is not readable, please check permissions")
|
||||
|
||||
return path_to_file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def session_exists(project_id, session_id):
|
||||
project_key = projects.get_project_key(project_id)
|
||||
try:
|
||||
results = requests.get(ASSIST_URL + config("assist") + f"/{project_key}/{session_id}",
|
||||
timeout=config("assistTimeout", cast=int, default=5))
|
||||
if results.status_code != 200:
|
||||
logger.error(f"!! issue with the peer-server code:{results.status_code} for session_exists")
|
||||
logger.error(results.text)
|
||||
return None
|
||||
results = results.json().get("data")
|
||||
if results is None:
|
||||
return False
|
||||
return True
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("!! Timeout getting Assist response")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("!! Issue getting Assist response")
|
||||
logger.exception(e)
|
||||
logger.error("expected JSON, received:")
|
||||
try:
|
||||
logger.error(results.text)
|
||||
except:
|
||||
logger.error("couldn't get response")
|
||||
return False
|
||||
|
||||
|
||||
def __change_keys(key):
|
||||
return {
|
||||
"PAGETITLE": schemas.LiveFilterType.PAGE_TITLE.value,
|
||||
"ACTIVE": "active",
|
||||
"LIVE": "live",
|
||||
"SESSIONID": schemas.LiveFilterType.SESSION_ID.value,
|
||||
"METADATA": schemas.LiveFilterType.METADATA.value,
|
||||
"USERID": schemas.LiveFilterType.USER_ID.value,
|
||||
"USERUUID": schemas.LiveFilterType.USER_UUID.value,
|
||||
"PROJECTKEY": "projectKey",
|
||||
"REVID": schemas.LiveFilterType.REV_ID.value,
|
||||
"TIMESTAMP": "timestamp",
|
||||
"TRACKERVERSION": schemas.LiveFilterType.TRACKER_VERSION.value,
|
||||
"ISSNIPPET": "isSnippet",
|
||||
"USEROS": schemas.LiveFilterType.USER_OS.value,
|
||||
"USERBROWSER": schemas.LiveFilterType.USER_BROWSER.value,
|
||||
"USERBROWSERVERSION": schemas.LiveFilterType.USER_BROWSER_VERSION.value,
|
||||
"USERDEVICE": schemas.LiveFilterType.USER_DEVICE.value,
|
||||
"USERDEVICETYPE": schemas.LiveFilterType.USER_DEVICE_TYPE.value,
|
||||
"USERCOUNTRY": schemas.LiveFilterType.USER_COUNTRY.value,
|
||||
"PROJECTID": "projectId"
|
||||
}.get(key.upper(), key)
|
||||
connected_peers = connected_peers.json().get("data", [])
|
||||
return str(session_id) in connected_peers
|
||||
|
|
|
|||
|
|
@ -1,98 +1,57 @@
|
|||
import logging
|
||||
|
||||
from chalicelib.utils.helper import environ
|
||||
import jwt
|
||||
from decouple import config
|
||||
|
||||
from chalicelib.core import tenants
|
||||
from chalicelib.core import users, spot
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from chalicelib.core import tenants
|
||||
from chalicelib.core import users
|
||||
|
||||
|
||||
def get_supported_audience():
|
||||
return [users.AUDIENCE, spot.AUDIENCE]
|
||||
|
||||
|
||||
def is_spot_token(token: str) -> bool:
|
||||
try:
|
||||
decoded_token = jwt.decode(token, options={"verify_signature": False, "verify_exp": False})
|
||||
audience = decoded_token.get("aud")
|
||||
return audience == spot.AUDIENCE
|
||||
except jwt.InvalidTokenError:
|
||||
logger.error(f"Invalid token for is_spot_token: {token}")
|
||||
raise
|
||||
|
||||
|
||||
def jwt_authorizer(scheme: str, token: str, leeway=0) -> dict | None:
|
||||
if scheme.lower() != "bearer":
|
||||
def jwt_authorizer(token):
|
||||
token = token.split(" ")
|
||||
if len(token) != 2 or token[0].lower() != "bearer":
|
||||
return None
|
||||
try:
|
||||
payload = jwt.decode(jwt=token,
|
||||
key=config("JWT_SECRET") if not is_spot_token(token) else config("JWT_SPOT_SECRET"),
|
||||
algorithms=config("JWT_ALGORITHM"),
|
||||
audience=get_supported_audience(),
|
||||
leeway=leeway)
|
||||
payload = jwt.decode(
|
||||
token[1],
|
||||
environ["jwt_secret"],
|
||||
algorithms=environ["jwt_algorithm"],
|
||||
audience=[f"plugin:{helper.get_stage_name()}", f"front:{helper.get_stage_name()}"]
|
||||
)
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.debug("! JWT Expired signature")
|
||||
print("! JWT Expired signature")
|
||||
return None
|
||||
except BaseException as e:
|
||||
logger.warning("! JWT Base Exception", exc_info=e)
|
||||
print("! JWT Base Exception")
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def jwt_refresh_authorizer(scheme: str, token: str):
|
||||
if scheme.lower() != "bearer":
|
||||
def jwt_context(context):
|
||||
user = users.get(user_id=context["userId"], tenant_id=context["tenantId"])
|
||||
if user is None:
|
||||
return None
|
||||
try:
|
||||
payload = jwt.decode(jwt=token,
|
||||
key=config("JWT_REFRESH_SECRET") if not is_spot_token(token) \
|
||||
else config("JWT_SPOT_REFRESH_SECRET"),
|
||||
algorithms=config("JWT_ALGORITHM"),
|
||||
audience=get_supported_audience())
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.debug("! JWT-refresh Expired signature")
|
||||
return None
|
||||
except BaseException as e:
|
||||
logger.error("! JWT-refresh Base Exception", exc_info=e)
|
||||
return None
|
||||
return payload
|
||||
return {
|
||||
"tenantId": context["tenantId"],
|
||||
"userId": context["userId"],
|
||||
**user
|
||||
}
|
||||
|
||||
|
||||
def generate_jwt(user_id, tenant_id, iat, aud, for_spot=False):
|
||||
def generate_jwt(id, tenant_id, iat, aud):
|
||||
token = jwt.encode(
|
||||
payload={
|
||||
"userId": user_id,
|
||||
"userId": id,
|
||||
"tenantId": tenant_id,
|
||||
"exp": iat + (config("JWT_EXPIRATION", cast=int) if not for_spot
|
||||
else config("JWT_SPOT_EXPIRATION", cast=int)),
|
||||
"iss": config("JWT_ISSUER"),
|
||||
"iat": iat,
|
||||
"exp": iat // 1000 + int(environ["jwt_exp_delta_seconds"]) + TimeUTC.get_utc_offset() // 1000,
|
||||
"iss": environ["jwt_issuer"],
|
||||
"iat": iat // 1000,
|
||||
"aud": aud
|
||||
},
|
||||
key=config("JWT_SECRET") if not for_spot else config("JWT_SPOT_SECRET"),
|
||||
algorithm=config("JWT_ALGORITHM")
|
||||
key=environ["jwt_secret"],
|
||||
algorithm=environ["jwt_algorithm"]
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def generate_jwt_refresh(user_id, tenant_id, iat, aud, jwt_jti, for_spot=False):
|
||||
token = jwt.encode(
|
||||
payload={
|
||||
"userId": user_id,
|
||||
"tenantId": tenant_id,
|
||||
"exp": iat + (config("JWT_REFRESH_EXPIRATION", cast=int) if not for_spot
|
||||
else config("JWT_SPOT_REFRESH_EXPIRATION", cast=int)),
|
||||
"iss": config("JWT_ISSUER"),
|
||||
"iat": iat,
|
||||
"aud": aud,
|
||||
"jti": jwt_jti
|
||||
},
|
||||
key=config("JWT_REFRESH_SECRET") if not for_spot else config("JWT_SPOT_REFRESH_SECRET"),
|
||||
algorithm=config("JWT_ALGORITHM")
|
||||
)
|
||||
return token
|
||||
return token.decode("utf-8")
|
||||
|
||||
|
||||
def api_key_authorizer(token):
|
||||
|
|
|
|||
|
|
@ -1,439 +0,0 @@
|
|||
import logging
|
||||
import schemas
|
||||
from chalicelib.core import countries, events, metadata
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.event_filter_definition import Event
|
||||
from chalicelib.utils.or_cache import CachedResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
TABLE = "public.autocomplete"
|
||||
|
||||
|
||||
def __get_autocomplete_table(value, project_id):
|
||||
autocomplete_events = [schemas.FilterType.REV_ID,
|
||||
schemas.EventType.CLICK,
|
||||
schemas.FilterType.USER_DEVICE,
|
||||
schemas.FilterType.USER_ID,
|
||||
schemas.FilterType.USER_BROWSER,
|
||||
schemas.FilterType.USER_OS,
|
||||
schemas.EventType.CUSTOM,
|
||||
schemas.FilterType.USER_COUNTRY,
|
||||
schemas.FilterType.USER_CITY,
|
||||
schemas.FilterType.USER_STATE,
|
||||
schemas.EventType.LOCATION,
|
||||
schemas.EventType.INPUT]
|
||||
autocomplete_events.sort()
|
||||
sub_queries = []
|
||||
c_list = []
|
||||
for e in autocomplete_events:
|
||||
if e == schemas.FilterType.USER_COUNTRY:
|
||||
c_list = countries.get_country_code_autocomplete(value)
|
||||
if len(c_list) > 0:
|
||||
sub_queries.append(f"""(SELECT DISTINCT ON(value) '{e.value}' AS _type, value
|
||||
FROM {TABLE}
|
||||
WHERE project_id = %(project_id)s
|
||||
AND type= '{e.value.upper()}'
|
||||
AND value IN %(c_list)s)""")
|
||||
continue
|
||||
sub_queries.append(f"""(SELECT '{e.value}' AS _type, value
|
||||
FROM {TABLE}
|
||||
WHERE project_id = %(project_id)s
|
||||
AND type= '{e.value.upper()}'
|
||||
AND value ILIKE %(svalue)s
|
||||
ORDER BY value
|
||||
LIMIT 5)""")
|
||||
if len(value) > 2:
|
||||
sub_queries.append(f"""(SELECT '{e.value}' AS _type, value
|
||||
FROM {TABLE}
|
||||
WHERE project_id = %(project_id)s
|
||||
AND type= '{e.value.upper()}'
|
||||
AND value ILIKE %(value)s
|
||||
ORDER BY value
|
||||
LIMIT 5)""")
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(" UNION DISTINCT ".join(sub_queries) + ";",
|
||||
{"project_id": project_id,
|
||||
"value": helper.string_to_sql_like(value),
|
||||
"svalue": helper.string_to_sql_like("^" + value),
|
||||
"c_list": tuple(c_list)
|
||||
})
|
||||
try:
|
||||
cur.execute(query)
|
||||
except Exception as err:
|
||||
logger.exception("--------- AUTOCOMPLETE SEARCH QUERY EXCEPTION -----------")
|
||||
logger.exception(query.decode('UTF-8'))
|
||||
logger.exception("--------- VALUE -----------")
|
||||
logger.exception(value)
|
||||
logger.exception("--------------------")
|
||||
raise err
|
||||
results = cur.fetchall()
|
||||
for r in results:
|
||||
r["type"] = r.pop("_type")
|
||||
results = helper.list_to_camel_case(results)
|
||||
return results
|
||||
|
||||
|
||||
def __generic_query(typename, value_length=None):
|
||||
if typename == schemas.FilterType.USER_COUNTRY:
|
||||
return f"""SELECT DISTINCT value, type
|
||||
FROM {TABLE}
|
||||
WHERE
|
||||
project_id = %(project_id)s
|
||||
AND type='{typename.upper()}'
|
||||
AND value IN %(value)s
|
||||
ORDER BY value"""
|
||||
|
||||
if value_length is None or value_length > 2:
|
||||
return f"""SELECT DISTINCT ON(value,type) value, type
|
||||
((SELECT DISTINCT value, type
|
||||
FROM {TABLE}
|
||||
WHERE
|
||||
project_id = %(project_id)s
|
||||
AND type='{typename.upper()}'
|
||||
AND value ILIKE %(svalue)s
|
||||
ORDER BY value
|
||||
LIMIT 5)
|
||||
UNION DISTINCT
|
||||
(SELECT DISTINCT value, type
|
||||
FROM {TABLE}
|
||||
WHERE
|
||||
project_id = %(project_id)s
|
||||
AND type='{typename.upper()}'
|
||||
AND value ILIKE %(value)s
|
||||
ORDER BY value
|
||||
LIMIT 5)) AS raw;"""
|
||||
return f"""SELECT DISTINCT value, type
|
||||
FROM {TABLE}
|
||||
WHERE
|
||||
project_id = %(project_id)s
|
||||
AND type='{typename.upper()}'
|
||||
AND value ILIKE %(svalue)s
|
||||
ORDER BY value
|
||||
LIMIT 10;"""
|
||||
|
||||
|
||||
def __generic_autocomplete(event: Event):
|
||||
def f(project_id, value, key=None, source=None):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = __generic_query(event.ui_type, value_length=len(value))
|
||||
params = {"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||
"svalue": helper.string_to_sql_like("^" + value)}
|
||||
cur.execute(cur.mogrify(query, params))
|
||||
return helper.list_to_camel_case(cur.fetchall())
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def generic_autocomplete_metas(typename):
|
||||
def f(project_id, text):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
params = {"project_id": project_id, "value": helper.string_to_sql_like(text),
|
||||
"svalue": helper.string_to_sql_like("^" + text)}
|
||||
|
||||
if typename == schemas.FilterType.USER_COUNTRY:
|
||||
params["value"] = tuple(countries.get_country_code_autocomplete(text))
|
||||
if len(params["value"]) == 0:
|
||||
return []
|
||||
|
||||
query = cur.mogrify(__generic_query(typename, value_length=len(text)), params)
|
||||
cur.execute(query)
|
||||
rows = cur.fetchall()
|
||||
return rows
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def __errors_query(source=None, value_length=None):
|
||||
if value_length is None or value_length > 2:
|
||||
return f"""((SELECT DISTINCT ON(lg.message)
|
||||
lg.message AS value,
|
||||
source,
|
||||
'{events.EventType.ERROR.ui_type}' AS type
|
||||
FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.message ILIKE %(svalue)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
{"AND source = %(source)s" if source is not None else ""}
|
||||
LIMIT 5)
|
||||
UNION DISTINCT
|
||||
(SELECT DISTINCT ON(lg.name)
|
||||
lg.name AS value,
|
||||
source,
|
||||
'{events.EventType.ERROR.ui_type}' AS type
|
||||
FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.name ILIKE %(svalue)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
{"AND source = %(source)s" if source is not None else ""}
|
||||
LIMIT 5)
|
||||
UNION DISTINCT
|
||||
(SELECT DISTINCT ON(lg.message)
|
||||
lg.message AS value,
|
||||
source,
|
||||
'{events.EventType.ERROR.ui_type}' AS type
|
||||
FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.message ILIKE %(value)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
{"AND source = %(source)s" if source is not None else ""}
|
||||
LIMIT 5)
|
||||
UNION DISTINCT
|
||||
(SELECT DISTINCT ON(lg.name)
|
||||
lg.name AS value,
|
||||
source,
|
||||
'{events.EventType.ERROR.ui_type}' AS type
|
||||
FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.name ILIKE %(value)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
{"AND source = %(source)s" if source is not None else ""}
|
||||
LIMIT 5));"""
|
||||
return f"""((SELECT DISTINCT ON(lg.message)
|
||||
lg.message AS value,
|
||||
source,
|
||||
'{events.EventType.ERROR.ui_type}' AS type
|
||||
FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.message ILIKE %(svalue)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
{"AND source = %(source)s" if source is not None else ""}
|
||||
LIMIT 5)
|
||||
UNION DISTINCT
|
||||
(SELECT DISTINCT ON(lg.name)
|
||||
lg.name AS value,
|
||||
source,
|
||||
'{events.EventType.ERROR.ui_type}' AS type
|
||||
FROM {events.EventType.ERROR.table} INNER JOIN public.errors AS lg USING (error_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.name ILIKE %(svalue)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
{"AND source = %(source)s" if source is not None else ""}
|
||||
LIMIT 5));"""
|
||||
|
||||
|
||||
def __search_errors(project_id, value, key=None, source=None):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify(__errors_query(source,
|
||||
value_length=len(value)),
|
||||
{"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||
"svalue": helper.string_to_sql_like("^" + value),
|
||||
"source": source}))
|
||||
results = helper.list_to_camel_case(cur.fetchall())
|
||||
return results
|
||||
|
||||
|
||||
def __search_errors_mobile(project_id, value, key=None, source=None):
|
||||
if len(value) > 2:
|
||||
query = f"""(SELECT DISTINCT ON(lg.reason)
|
||||
lg.reason AS value,
|
||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
||||
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
AND lg.reason ILIKE %(svalue)s
|
||||
LIMIT 5)
|
||||
UNION ALL
|
||||
(SELECT DISTINCT ON(lg.name)
|
||||
lg.name AS value,
|
||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
||||
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
AND lg.name ILIKE %(svalue)s
|
||||
LIMIT 5)
|
||||
UNION ALL
|
||||
(SELECT DISTINCT ON(lg.reason)
|
||||
lg.reason AS value,
|
||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
||||
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
AND lg.reason ILIKE %(value)s
|
||||
LIMIT 5)
|
||||
UNION ALL
|
||||
(SELECT DISTINCT ON(lg.name)
|
||||
lg.name AS value,
|
||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
||||
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
AND lg.name ILIKE %(value)s
|
||||
LIMIT 5);"""
|
||||
else:
|
||||
query = f"""(SELECT DISTINCT ON(lg.reason)
|
||||
lg.reason AS value,
|
||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
||||
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
AND lg.reason ILIKE %(svalue)s
|
||||
LIMIT 5)
|
||||
UNION ALL
|
||||
(SELECT DISTINCT ON(lg.name)
|
||||
lg.name AS value,
|
||||
'{events.EventType.CRASH_MOBILE.ui_type}' AS type
|
||||
FROM {events.EventType.CRASH_MOBILE.table} INNER JOIN public.crashes_ios AS lg USING (crash_ios_id) LEFT JOIN public.sessions AS s USING(session_id)
|
||||
WHERE
|
||||
s.project_id = %(project_id)s
|
||||
AND lg.project_id = %(project_id)s
|
||||
AND lg.name ILIKE %(svalue)s
|
||||
LIMIT 5);"""
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(cur.mogrify(query, {"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||
"svalue": helper.string_to_sql_like("^" + value)}))
|
||||
results = helper.list_to_camel_case(cur.fetchall())
|
||||
return results
|
||||
|
||||
|
||||
def __search_metadata(project_id, value, key=None, source=None):
|
||||
meta_keys = metadata.get(project_id=project_id)
|
||||
meta_keys = {m["key"]: m["index"] for m in meta_keys}
|
||||
if len(meta_keys) == 0 or key is not None and key not in meta_keys.keys():
|
||||
return []
|
||||
sub_from = []
|
||||
if key is not None:
|
||||
meta_keys = {key: meta_keys[key]}
|
||||
|
||||
for k in meta_keys.keys():
|
||||
colname = metadata.index_to_colname(meta_keys[k])
|
||||
if len(value) > 2:
|
||||
sub_from.append(f"""((SELECT DISTINCT ON ({colname}) {colname} AS value, '{k}' AS key
|
||||
FROM public.sessions
|
||||
WHERE project_id = %(project_id)s
|
||||
AND {colname} ILIKE %(svalue)s LIMIT 5)
|
||||
UNION
|
||||
(SELECT DISTINCT ON ({colname}) {colname} AS value, '{k}' AS key
|
||||
FROM public.sessions
|
||||
WHERE project_id = %(project_id)s
|
||||
AND {colname} ILIKE %(value)s LIMIT 5))
|
||||
""")
|
||||
else:
|
||||
sub_from.append(f"""(SELECT DISTINCT ON ({colname}) {colname} AS value, '{k}' AS key
|
||||
FROM public.sessions
|
||||
WHERE project_id = %(project_id)s
|
||||
AND {colname} ILIKE %(svalue)s LIMIT 5)""")
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(cur.mogrify(f"""\
|
||||
SELECT DISTINCT ON(key, value) key, value, 'METADATA' AS TYPE
|
||||
FROM({" UNION ALL ".join(sub_from)}) AS all_metas
|
||||
LIMIT 5;""", {"project_id": project_id, "value": helper.string_to_sql_like(value),
|
||||
"svalue": helper.string_to_sql_like("^" + value)}))
|
||||
results = helper.list_to_camel_case(cur.fetchall())
|
||||
return results
|
||||
|
||||
|
||||
TYPE_TO_COLUMN = {
|
||||
schemas.EventType.CLICK: "label",
|
||||
schemas.EventType.INPUT: "label",
|
||||
schemas.EventType.LOCATION: "path",
|
||||
schemas.EventType.CUSTOM: "name",
|
||||
schemas.FetchFilterType.FETCH_URL: "path",
|
||||
schemas.GraphqlFilterType.GRAPHQL_NAME: "name",
|
||||
schemas.EventType.STATE_ACTION: "name",
|
||||
# For ERROR, sessions search is happening over name OR message,
|
||||
# for simplicity top 10 is using name only
|
||||
schemas.EventType.ERROR: "name",
|
||||
schemas.FilterType.USER_COUNTRY: "user_country",
|
||||
schemas.FilterType.USER_CITY: "user_city",
|
||||
schemas.FilterType.USER_STATE: "user_state",
|
||||
schemas.FilterType.USER_ID: "user_id",
|
||||
schemas.FilterType.USER_ANONYMOUS_ID: "user_anonymous_id",
|
||||
schemas.FilterType.USER_OS: "user_os",
|
||||
schemas.FilterType.USER_BROWSER: "user_browser",
|
||||
schemas.FilterType.USER_DEVICE: "user_device",
|
||||
schemas.FilterType.PLATFORM: "platform",
|
||||
schemas.FilterType.REV_ID: "rev_id",
|
||||
schemas.FilterType.REFERRER: "referrer",
|
||||
schemas.FilterType.UTM_SOURCE: "utm_source",
|
||||
schemas.FilterType.UTM_MEDIUM: "utm_medium",
|
||||
schemas.FilterType.UTM_CAMPAIGN: "utm_campaign",
|
||||
}
|
||||
|
||||
TYPE_TO_TABLE = {
|
||||
schemas.EventType.CLICK: "events.clicks",
|
||||
schemas.EventType.INPUT: "events.inputs",
|
||||
schemas.EventType.LOCATION: "events.pages",
|
||||
schemas.EventType.CUSTOM: "events_common.customs",
|
||||
schemas.FetchFilterType.FETCH_URL: "events_common.requests",
|
||||
schemas.GraphqlFilterType.GRAPHQL_NAME: "events.graphql",
|
||||
schemas.EventType.STATE_ACTION: "events.state_actions",
|
||||
}
|
||||
|
||||
|
||||
def is_top_supported(event_type):
|
||||
return TYPE_TO_COLUMN.get(event_type, False)
|
||||
|
||||
|
||||
@CachedResponse(table="or_cache.autocomplete_top_values", ttl=5 * 60)
|
||||
def get_top_values(project_id, event_type, event_key=None):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
if schemas.FilterType.has_value(event_type):
|
||||
if event_type == schemas.FilterType.METADATA \
|
||||
and (event_key is None \
|
||||
or (colname := metadata.get_colname_by_key(project_id=project_id, key=event_key)) is None) \
|
||||
or event_type != schemas.FilterType.METADATA \
|
||||
and (colname := TYPE_TO_COLUMN.get(event_type)) is None:
|
||||
return []
|
||||
|
||||
query = f"""WITH raw AS (SELECT DISTINCT {colname} AS c_value,
|
||||
COUNT(1) OVER (PARTITION BY {colname}) AS row_count,
|
||||
COUNT(1) OVER () AS total_count
|
||||
FROM public.sessions
|
||||
WHERE project_id = %(project_id)s
|
||||
AND {colname} IS NOT NULL
|
||||
AND sessions.duration IS NOT NULL
|
||||
AND sessions.duration > 0
|
||||
ORDER BY row_count DESC
|
||||
LIMIT 10)
|
||||
SELECT c_value AS value, row_count, trunc(row_count * 100 / total_count, 2) AS row_percentage
|
||||
FROM raw;"""
|
||||
elif event_type == schemas.EventType.ERROR:
|
||||
colname = TYPE_TO_COLUMN.get(event_type)
|
||||
query = f"""WITH raw AS (SELECT DISTINCT {colname} AS c_value,
|
||||
COUNT(1) OVER (PARTITION BY {colname}) AS row_count,
|
||||
COUNT(1) OVER () AS total_count
|
||||
FROM public.errors
|
||||
WHERE project_id = %(project_id)s
|
||||
AND {colname} IS NOT NULL
|
||||
AND {colname} != ''
|
||||
ORDER BY row_count DESC
|
||||
LIMIT 10)
|
||||
SELECT c_value AS value, row_count, trunc(row_count * 100 / total_count,2) AS row_percentage
|
||||
FROM raw;"""
|
||||
else:
|
||||
colname = TYPE_TO_COLUMN.get(event_type)
|
||||
table = TYPE_TO_TABLE.get(event_type)
|
||||
query = f"""WITH raw AS (SELECT DISTINCT {colname} AS c_value,
|
||||
COUNT(1) OVER (PARTITION BY {colname}) AS row_count,
|
||||
COUNT(1) OVER () AS total_count
|
||||
FROM {table} INNER JOIN public.sessions USING(session_id)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND {colname} IS NOT NULL
|
||||
AND {colname} != ''
|
||||
AND sessions.duration IS NOT NULL
|
||||
AND sessions.duration > 0
|
||||
ORDER BY row_count DESC
|
||||
LIMIT 10)
|
||||
SELECT c_value AS value, row_count, trunc(row_count * 100 / total_count,2) AS row_percentage
|
||||
FROM raw;"""
|
||||
params = {"project_id": project_id}
|
||||
query = cur.mogrify(query, params)
|
||||
logger.debug("--------------------")
|
||||
logger.debug(query)
|
||||
logger.debug("--------------------")
|
||||
cur.execute(query=query)
|
||||
results = cur.fetchall()
|
||||
return helper.list_to_camel_case(results)
|
||||
|
|
@ -1,143 +1,116 @@
|
|||
from chalicelib.core import projects
|
||||
from chalicelib.core import users
|
||||
from chalicelib.core.log_tools import datadog, stackdriver, sentry
|
||||
from chalicelib.core.modules import TENANT_CONDITION
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.core import projects, log_tool_datadog, log_tool_stackdriver, log_tool_sentry
|
||||
|
||||
from chalicelib.core import users
|
||||
|
||||
|
||||
def get_state(tenant_id):
|
||||
pids = projects.get_projects_ids(tenant_id=tenant_id)
|
||||
my_projects = projects.get_projects(tenant_id=tenant_id, recording_state=False)
|
||||
pids = [s["projectId"] for s in my_projects]
|
||||
with pg_client.PostgresClient() as cur:
|
||||
recorded = False
|
||||
meta = False
|
||||
|
||||
if len(pids) > 0:
|
||||
cur.execute(
|
||||
cur.mogrify(
|
||||
"""SELECT EXISTS(( SELECT 1
|
||||
FROM public.sessions AS s
|
||||
WHERE s.project_id IN %(ids)s)) AS exists;""",
|
||||
{"ids": tuple(pids)},
|
||||
)
|
||||
cur.mogrify("""\
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM public.sessions AS s
|
||||
where s.project_id IN %(ids)s
|
||||
LIMIT 1;""",
|
||||
{"ids": tuple(pids)})
|
||||
)
|
||||
recorded = cur.fetchone()["exists"]
|
||||
recorded = cur.fetchone()["count"] > 0
|
||||
meta = False
|
||||
if recorded:
|
||||
query = cur.mogrify(
|
||||
f"""SELECT EXISTS((SELECT 1
|
||||
FROM public.projects AS p
|
||||
LEFT JOIN LATERAL ( SELECT 1
|
||||
FROM public.sessions
|
||||
WHERE sessions.project_id = p.project_id
|
||||
AND sessions.user_id IS NOT NULL
|
||||
LIMIT 1) AS sessions(user_id) ON (TRUE)
|
||||
WHERE {TENANT_CONDITION} AND p.deleted_at ISNULL
|
||||
AND ( sessions.user_id IS NOT NULL OR p.metadata_1 IS NOT NULL
|
||||
OR p.metadata_2 IS NOT NULL OR p.metadata_3 IS NOT NULL
|
||||
OR p.metadata_4 IS NOT NULL OR p.metadata_5 IS NOT NULL
|
||||
OR p.metadata_6 IS NOT NULL OR p.metadata_7 IS NOT NULL
|
||||
OR p.metadata_8 IS NOT NULL OR p.metadata_9 IS NOT NULL
|
||||
OR p.metadata_10 IS NOT NULL )
|
||||
)) AS exists;""",
|
||||
{"tenant_id": tenant_id},
|
||||
)
|
||||
cur.execute(query)
|
||||
cur.execute("""SELECT SUM((SELECT COUNT(t.meta)
|
||||
FROM (VALUES (p.metadata_1), (p.metadata_2), (p.metadata_3), (p.metadata_4), (p.metadata_5),
|
||||
(p.metadata_6), (p.metadata_7), (p.metadata_8), (p.metadata_9), (p.metadata_10),
|
||||
(sessions.user_id)) AS t(meta)
|
||||
WHERE t.meta NOTNULL))
|
||||
FROM public.projects AS p
|
||||
LEFT JOIN LATERAL ( SELECT 'defined'
|
||||
FROM public.sessions
|
||||
WHERE sessions.project_id=p.project_id AND sessions.user_id IS NOT NULL
|
||||
LIMIT 1) AS sessions(user_id) ON(TRUE)
|
||||
WHERE p.deleted_at ISNULL;"""
|
||||
)
|
||||
|
||||
meta = cur.fetchone()["exists"]
|
||||
meta = cur.fetchone()["sum"] > 0
|
||||
|
||||
return [
|
||||
{
|
||||
"task": "Install OpenReplay",
|
||||
"done": recorded,
|
||||
"URL": "https://docs.openreplay.com/getting-started/quick-start",
|
||||
},
|
||||
{
|
||||
"task": "Identify Users",
|
||||
"done": meta,
|
||||
"URL": "https://docs.openreplay.com/data-privacy-security/metadata",
|
||||
},
|
||||
{
|
||||
"task": "Invite Team Members",
|
||||
"done": len(users.get_members(tenant_id=tenant_id)) > 1,
|
||||
"URL": "https://app.openreplay.com/client/manage-users",
|
||||
},
|
||||
{
|
||||
"task": "Integrations",
|
||||
"done": len(datadog.get_all(tenant_id=tenant_id)) > 0
|
||||
or len(sentry.get_all(tenant_id=tenant_id)) > 0
|
||||
or len(stackdriver.get_all(tenant_id=tenant_id)) > 0,
|
||||
"URL": "https://docs.openreplay.com/integrations",
|
||||
},
|
||||
{"task": "Install OpenReplay",
|
||||
"done": recorded,
|
||||
"URL": "https://docs.openreplay.com/getting-started/quick-start"},
|
||||
{"task": "Identify Users",
|
||||
"done": meta,
|
||||
"URL": "https://docs.openreplay.com/data-privacy-security/metadata"},
|
||||
{"task": "Invite Team Members",
|
||||
"done": len(users.get_members(tenant_id=tenant_id)) > 1,
|
||||
"URL": "https://app.openreplay.com/client/manage-users"},
|
||||
{"task": "Integrations",
|
||||
"done": len(log_tool_datadog.get_all(tenant_id=tenant_id)) > 0 \
|
||||
or len(log_tool_sentry.get_all(tenant_id=tenant_id)) > 0 \
|
||||
or len(log_tool_stackdriver.get_all(tenant_id=tenant_id)) > 0,
|
||||
"URL": "https://docs.openreplay.com/integrations"}
|
||||
]
|
||||
|
||||
|
||||
def get_state_installing(tenant_id):
|
||||
pids = projects.get_projects_ids(tenant_id=tenant_id)
|
||||
my_projects = projects.get_projects(tenant_id=tenant_id, recording_state=False)
|
||||
pids = [s["projectId"] for s in my_projects]
|
||||
with pg_client.PostgresClient() as cur:
|
||||
recorded = False
|
||||
|
||||
if len(pids) > 0:
|
||||
cur.execute(
|
||||
cur.mogrify(
|
||||
"""SELECT EXISTS(( SELECT 1
|
||||
FROM public.sessions AS s
|
||||
WHERE s.project_id IN %(ids)s)) AS exists;""",
|
||||
{"ids": tuple(pids)},
|
||||
)
|
||||
cur.mogrify("""\
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM public.sessions AS s
|
||||
where s.project_id IN %(ids)s
|
||||
LIMIT 1;""",
|
||||
{"ids": tuple(pids)})
|
||||
)
|
||||
recorded = cur.fetchone()["exists"]
|
||||
recorded = cur.fetchone()["count"] > 0
|
||||
|
||||
return {
|
||||
"task": "Install OpenReplay",
|
||||
"done": recorded,
|
||||
"URL": "https://docs.openreplay.com/getting-started/quick-start",
|
||||
}
|
||||
return {"task": "Install OpenReplay",
|
||||
"done": recorded,
|
||||
"URL": "https://docs.openreplay.com/getting-started/quick-start"}
|
||||
|
||||
|
||||
def get_state_identify_users(tenant_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
f"""SELECT EXISTS((SELECT 1
|
||||
FROM public.projects AS p
|
||||
LEFT JOIN LATERAL ( SELECT 1
|
||||
FROM public.sessions
|
||||
WHERE sessions.project_id = p.project_id
|
||||
AND sessions.user_id IS NOT NULL
|
||||
LIMIT 1) AS sessions(user_id) ON (TRUE)
|
||||
WHERE {TENANT_CONDITION} AND p.deleted_at ISNULL
|
||||
AND ( sessions.user_id IS NOT NULL OR p.metadata_1 IS NOT NULL
|
||||
OR p.metadata_2 IS NOT NULL OR p.metadata_3 IS NOT NULL
|
||||
OR p.metadata_4 IS NOT NULL OR p.metadata_5 IS NOT NULL
|
||||
OR p.metadata_6 IS NOT NULL OR p.metadata_7 IS NOT NULL
|
||||
OR p.metadata_8 IS NOT NULL OR p.metadata_9 IS NOT NULL
|
||||
OR p.metadata_10 IS NOT NULL )
|
||||
)) AS exists;""",
|
||||
{"tenant_id": tenant_id},
|
||||
)
|
||||
cur.execute(query)
|
||||
cur.execute(
|
||||
"""SELECT SUM((SELECT COUNT(t.meta)
|
||||
FROM (VALUES (p.metadata_1), (p.metadata_2), (p.metadata_3), (p.metadata_4), (p.metadata_5),
|
||||
(p.metadata_6), (p.metadata_7), (p.metadata_8), (p.metadata_9), (p.metadata_10),
|
||||
(sessions.user_id)) AS t(meta)
|
||||
WHERE t.meta NOTNULL))
|
||||
FROM public.projects AS p
|
||||
LEFT JOIN LATERAL ( SELECT 'defined'
|
||||
FROM public.sessions
|
||||
WHERE sessions.project_id=p.project_id AND sessions.user_id IS NOT NULL
|
||||
LIMIT 1) AS sessions(user_id) ON(TRUE)
|
||||
WHERE p.deleted_at ISNULL;""")
|
||||
|
||||
meta = cur.fetchone()["exists"]
|
||||
meta = cur.fetchone()["sum"] > 0
|
||||
|
||||
return {
|
||||
"task": "Identify Users",
|
||||
"done": meta,
|
||||
"URL": "https://docs.openreplay.com/data-privacy-security/metadata",
|
||||
}
|
||||
return {"task": "Identify Users",
|
||||
"done": meta,
|
||||
"URL": "https://docs.openreplay.com/data-privacy-security/metadata"}
|
||||
|
||||
|
||||
def get_state_manage_users(tenant_id):
|
||||
return {
|
||||
"task": "Invite Team Members",
|
||||
"done": len(users.get_members(tenant_id=tenant_id)) > 1,
|
||||
"URL": "https://app.openreplay.com/client/manage-users",
|
||||
}
|
||||
return {"task": "Invite Team Members",
|
||||
"done": len(users.get_members(tenant_id=tenant_id)) > 1,
|
||||
"URL": "https://app.openreplay.com/client/manage-users"}
|
||||
|
||||
|
||||
def get_state_integrations(tenant_id):
|
||||
return {
|
||||
"task": "Integrations",
|
||||
"done": len(datadog.get_all(tenant_id=tenant_id)) > 0
|
||||
or len(sentry.get_all(tenant_id=tenant_id)) > 0
|
||||
or len(stackdriver.get_all(tenant_id=tenant_id)) > 0,
|
||||
"URL": "https://docs.openreplay.com/integrations",
|
||||
}
|
||||
return {"task": "Integrations",
|
||||
"done": len(log_tool_datadog.get_all(tenant_id=tenant_id)) > 0 \
|
||||
or len(log_tool_sentry.get_all(tenant_id=tenant_id)) > 0 \
|
||||
or len(log_tool_stackdriver.get_all(tenant_id=tenant_id)) > 0,
|
||||
"URL": "https://docs.openreplay.com/integrations"}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.storage import StorageClient
|
||||
from decouple import config
|
||||
|
||||
|
||||
def get_canvas_presigned_urls(session_id, project_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(cur.mogrify("""\
|
||||
SELECT *
|
||||
FROM events.canvas_recordings
|
||||
WHERE session_id = %(session_id)s
|
||||
ORDER BY timestamp;""",
|
||||
{"project_id": project_id, "session_id": session_id})
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
urls = []
|
||||
for i in range(len(rows)):
|
||||
params = {
|
||||
"sessionId": session_id,
|
||||
"projectId": project_id,
|
||||
"recordingId": rows[i]["recording_id"]
|
||||
}
|
||||
oldKey = "%(sessionId)s/%(recordingId)s.mp4" % params
|
||||
key = config("CANVAS_PATTERN", default="%(sessionId)s/%(recordingId)s.tar.zst") % params
|
||||
urls.append(StorageClient.get_presigned_url_for_sharing(
|
||||
bucket=config("CANVAS_BUCKET", default=config("sessions_bucket")),
|
||||
expires_in=config("PRESIGNED_URL_EXPIRATION", cast=int, default=900),
|
||||
key=key
|
||||
))
|
||||
urls.append(StorageClient.get_presigned_url_for_sharing(
|
||||
bucket=config("CANVAS_BUCKET", default=config("sessions_bucket")),
|
||||
expires_in=config("PRESIGNED_URL_EXPIRATION", cast=int, default=900),
|
||||
key=oldKey
|
||||
))
|
||||
return urls
|
||||
125
api/chalicelib/core/collaboration_slack.py
Normal file
125
api/chalicelib/core/collaboration_slack.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import requests
|
||||
from chalicelib.utils.helper import environ
|
||||
from datetime import datetime
|
||||
from chalicelib.core import webhook
|
||||
|
||||
|
||||
class Slack:
|
||||
@classmethod
|
||||
def add_channel(cls, tenant_id, **args):
|
||||
url = args["url"]
|
||||
name = args["name"]
|
||||
if cls.say_hello(url):
|
||||
return webhook.add(tenant_id=tenant_id,
|
||||
endpoint=url,
|
||||
webhook_type="slack",
|
||||
name=name)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def say_hello(cls, url):
|
||||
r = requests.post(
|
||||
url=url,
|
||||
json={
|
||||
"attachments": [
|
||||
{
|
||||
"text": "Welcome to OpenReplay",
|
||||
"ts": datetime.now().timestamp(),
|
||||
}
|
||||
]
|
||||
})
|
||||
if r.status_code != 200:
|
||||
print("slack integration failed")
|
||||
print(r.text)
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def send_text(cls, tenant_id, webhook_id, text, **args):
|
||||
integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id)
|
||||
if integration is None:
|
||||
return {"errors": ["slack integration not found"]}
|
||||
print("====> sending slack notification")
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json={
|
||||
"attachments": [
|
||||
{
|
||||
"text": text,
|
||||
"ts": datetime.now().timestamp(),
|
||||
**args
|
||||
}
|
||||
]
|
||||
})
|
||||
print(r)
|
||||
print(r.text)
|
||||
return {"data": r.text}
|
||||
|
||||
@classmethod
|
||||
def send_batch(cls, tenant_id, webhook_id, attachments):
|
||||
integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id)
|
||||
if integration is None:
|
||||
return {"errors": ["slack integration not found"]}
|
||||
print(f"====> sending slack batch notification: {len(attachments)}")
|
||||
for i in range(0, len(attachments), 100):
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json={"attachments": attachments[i:i + 100]})
|
||||
if r.status_code != 200:
|
||||
print("!!!! something went wrong")
|
||||
print(r)
|
||||
print(r.text)
|
||||
|
||||
@classmethod
|
||||
def __share_to_slack(cls, tenant_id, integration_id, fallback, pretext, title, title_link, text):
|
||||
integration = cls.__get(tenant_id=tenant_id, integration_id=integration_id)
|
||||
if integration is None:
|
||||
return {"errors": ["slack integration not found"]}
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json={
|
||||
"attachments": [
|
||||
{
|
||||
"fallback": fallback,
|
||||
"pretext": pretext,
|
||||
"title": title,
|
||||
"title_link": title_link,
|
||||
"text": text,
|
||||
"ts": datetime.now().timestamp()
|
||||
}
|
||||
]
|
||||
})
|
||||
return r.text
|
||||
|
||||
@classmethod
|
||||
def share_session(cls, tenant_id, project_id, session_id, user, comment, integration_id=None):
|
||||
args = {"fallback": f"{user} has shared the below session!",
|
||||
"pretext": f"{user} has shared the below session!",
|
||||
"title": f"{environ['SITE_URL']}/{project_id}/session/{session_id}",
|
||||
"title_link": f"{environ['SITE_URL']}/{project_id}/session/{session_id}",
|
||||
"text": comment}
|
||||
return {"data": cls.__share_to_slack(tenant_id, integration_id, **args)}
|
||||
|
||||
@classmethod
|
||||
def share_error(cls, tenant_id, project_id, error_id, user, comment, integration_id=None):
|
||||
args = {"fallback": f"{user} has shared the below error!",
|
||||
"pretext": f"{user} has shared the below error!",
|
||||
"title": f"{environ['SITE_URL']}/{project_id}/errors/{error_id}",
|
||||
"title_link": f"{environ['SITE_URL']}/{project_id}/errors/{error_id}",
|
||||
"text": comment}
|
||||
return {"data": cls.__share_to_slack(tenant_id, integration_id, **args)}
|
||||
|
||||
@classmethod
|
||||
def has_slack(cls, tenant_id):
|
||||
integration = cls.__get(tenant_id=tenant_id)
|
||||
return not (integration is None or len(integration) == 0)
|
||||
|
||||
@classmethod
|
||||
def __get(cls, tenant_id, integration_id=None):
|
||||
if integration_id is not None:
|
||||
return webhook.get(tenant_id=tenant_id, webhook_id=integration_id)
|
||||
|
||||
integrations = webhook.get_by_type(tenant_id=tenant_id, webhook_type="slack")
|
||||
if integrations is None or len(integrations) == 0:
|
||||
return None
|
||||
return integrations[0]
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import collaboration_base as _
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
from abc import ABC, abstractmethod
|
||||
|
||||
import schemas
|
||||
|
||||
|
||||
class BaseCollaboration(ABC):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def add(cls, tenant_id, data: schemas.AddCollaborationSchema):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def say_hello(cls, url):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def send_raw(cls, tenant_id, webhook_id, body):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def send_batch(cls, tenant_id, webhook_id, attachments):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def __share(cls, tenant_id, integration_id, attachments, extra=None):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def share_session(cls, tenant_id, project_id, session_id, user, comment, project_name=None, integration_id=None):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def share_error(cls, tenant_id, project_id, error_id, user, comment, project_name=None, integration_id=None):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def get_integration(cls, tenant_id, integration_id=None):
|
||||
pass
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
from decouple import config
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import webhook
|
||||
from chalicelib.core.collaborations.collaboration_base import BaseCollaboration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MSTeams(BaseCollaboration):
|
||||
@classmethod
|
||||
def add(cls, tenant_id, data: schemas.AddCollaborationSchema):
|
||||
if webhook.exists_by_name(tenant_id=tenant_id, name=data.name, exclude_id=None,
|
||||
webhook_type=schemas.WebhookType.MSTEAMS):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
|
||||
if cls.say_hello(data.url):
|
||||
return webhook.add(tenant_id=tenant_id,
|
||||
endpoint=data.url.unicode_string(),
|
||||
webhook_type=schemas.WebhookType.MSTEAMS,
|
||||
name=data.name)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def say_hello(cls, url):
|
||||
try:
|
||||
r = requests.post(
|
||||
url=url,
|
||||
json={
|
||||
"@type": "MessageCard",
|
||||
"@context": "https://schema.org/extensions",
|
||||
"summary": "Welcome to OpenReplay",
|
||||
"title": "Welcome to OpenReplay"
|
||||
},
|
||||
timeout=3)
|
||||
if r.status_code != 200:
|
||||
logger.warning("MSTeams integration failed")
|
||||
logger.warning(r.text)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("!!! MSTeams integration failed")
|
||||
logger.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def send_raw(cls, tenant_id, webhook_id, body):
|
||||
integration = cls.get_integration(tenant_id=tenant_id, integration_id=webhook_id)
|
||||
if integration is None:
|
||||
return {"errors": ["msteams integration not found"]}
|
||||
try:
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json=body,
|
||||
timeout=5)
|
||||
if r.status_code != 200:
|
||||
logger.warning(f"!! issue sending msteams raw; webhookId:{webhook_id} code:{r.status_code}")
|
||||
logger.warning(r.text)
|
||||
return None
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning(f"!! Timeout sending msteams raw webhookId:{webhook_id}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"!! Issue sending msteams raw webhookId:{webhook_id}")
|
||||
logger.warning(e)
|
||||
return None
|
||||
return {"data": r.text}
|
||||
|
||||
@classmethod
|
||||
def send_batch(cls, tenant_id, webhook_id, attachments):
|
||||
integration = cls.get_integration(tenant_id=tenant_id, integration_id=webhook_id)
|
||||
if integration is None:
|
||||
return {"errors": ["msteams integration not found"]}
|
||||
logger.debug(f"====> sending msteams batch notification: {len(attachments)}")
|
||||
for i in range(0, len(attachments), 50):
|
||||
part = attachments[i:i + 50]
|
||||
for j in range(1, len(part), 2):
|
||||
part.insert(j, {"text": "***"})
|
||||
|
||||
r = requests.post(url=integration["endpoint"],
|
||||
json={
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"summary": part[0]["activityTitle"],
|
||||
"sections": part
|
||||
})
|
||||
if r.status_code != 200:
|
||||
logger.warning("!!!! something went wrong")
|
||||
logger.warning(r.text)
|
||||
|
||||
@classmethod
|
||||
def __share(cls, tenant_id, integration_id, attachement, extra=None):
|
||||
if extra is None:
|
||||
extra = {}
|
||||
integration = cls.get_integration(tenant_id=tenant_id, integration_id=integration_id)
|
||||
if integration is None:
|
||||
return {"errors": ["Microsoft Teams integration not found"]}
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json={
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"sections": [attachement],
|
||||
**extra
|
||||
})
|
||||
|
||||
return r.text
|
||||
|
||||
@classmethod
|
||||
def share_session(cls, tenant_id, project_id, session_id, user, comment, project_name=None, integration_id=None):
|
||||
title = f"*{user}* has shared the below session!"
|
||||
link = f"{config('SITE_URL')}/{project_id}/session/{session_id}"
|
||||
args = {
|
||||
"activityTitle": title,
|
||||
"facts": [
|
||||
{
|
||||
"name": "Session:",
|
||||
"value": link
|
||||
}],
|
||||
"markdown": True
|
||||
}
|
||||
if project_name and len(project_name) > 0:
|
||||
args["activitySubtitle"] = f"On Project *{project_name}*"
|
||||
if comment and len(comment) > 0:
|
||||
args["facts"].append({
|
||||
"name": "Comment:",
|
||||
"value": comment
|
||||
})
|
||||
data = cls.__share(tenant_id, integration_id, attachement=args, extra={"summary": title})
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
@classmethod
|
||||
def share_error(cls, tenant_id, project_id, error_id, user, comment, project_name=None, integration_id=None):
|
||||
title = f"*{user}* has shared the below error!"
|
||||
link = f"{config('SITE_URL')}/{project_id}/errors/{error_id}"
|
||||
args = {
|
||||
"activityTitle": title,
|
||||
"facts": [
|
||||
{
|
||||
"name": "Session:",
|
||||
"value": link
|
||||
}],
|
||||
"markdown": True
|
||||
}
|
||||
if project_name and len(project_name) > 0:
|
||||
args["activitySubtitle"] = f"On Project *{project_name}*"
|
||||
if comment and len(comment) > 0:
|
||||
args["facts"].append({
|
||||
"name": "Comment:",
|
||||
"value": comment
|
||||
})
|
||||
data = cls.__share(tenant_id, integration_id, attachement=args, extra={"summary": title})
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
@classmethod
|
||||
def get_integration(cls, tenant_id, integration_id=None):
|
||||
if integration_id is not None:
|
||||
return webhook.get_webhook(tenant_id=tenant_id, webhook_id=integration_id,
|
||||
webhook_type=schemas.WebhookType.MSTEAMS)
|
||||
|
||||
integrations = webhook.get_by_type(tenant_id=tenant_id, webhook_type=schemas.WebhookType.MSTEAMS)
|
||||
if integrations is None or len(integrations) == 0:
|
||||
return None
|
||||
return integrations[0]
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from decouple import config
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import webhook
|
||||
from chalicelib.core.collaborations.collaboration_base import BaseCollaboration
|
||||
|
||||
|
||||
class Slack(BaseCollaboration):
|
||||
@classmethod
|
||||
def add(cls, tenant_id, data: schemas.AddCollaborationSchema):
|
||||
if webhook.exists_by_name(tenant_id=tenant_id, name=data.name, exclude_id=None,
|
||||
webhook_type=schemas.WebhookType.SLACK):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
|
||||
if cls.say_hello(data.url):
|
||||
return webhook.add(tenant_id=tenant_id,
|
||||
endpoint=data.url.unicode_string(),
|
||||
webhook_type=schemas.WebhookType.SLACK,
|
||||
name=data.name)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def say_hello(cls, url):
|
||||
r = requests.post(
|
||||
url=url,
|
||||
json={
|
||||
"attachments": [
|
||||
{
|
||||
"text": "Welcome to OpenReplay",
|
||||
"ts": datetime.now().timestamp(),
|
||||
}
|
||||
]
|
||||
})
|
||||
if r.status_code != 200:
|
||||
print("slack integration failed")
|
||||
print(r.text)
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def send_raw(cls, tenant_id, webhook_id, body):
|
||||
integration = cls.get_integration(tenant_id=tenant_id, integration_id=webhook_id)
|
||||
if integration is None:
|
||||
return {"errors": ["slack integration not found"]}
|
||||
try:
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json=body,
|
||||
timeout=5)
|
||||
if r.status_code != 200:
|
||||
print(f"!! issue sending slack raw; webhookId:{webhook_id} code:{r.status_code}")
|
||||
print(r.text)
|
||||
return None
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"!! Timeout sending slack raw webhookId:{webhook_id}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"!! Issue sending slack raw webhookId:{webhook_id}")
|
||||
print(str(e))
|
||||
return None
|
||||
return {"data": r.text}
|
||||
|
||||
@classmethod
|
||||
def send_batch(cls, tenant_id, webhook_id, attachments):
|
||||
integration = cls.get_integration(tenant_id=tenant_id, integration_id=webhook_id)
|
||||
if integration is None:
|
||||
return {"errors": ["slack integration not found"]}
|
||||
print(f"====> sending slack batch notification: {len(attachments)}")
|
||||
for i in range(0, len(attachments), 100):
|
||||
r = requests.post(
|
||||
url=integration["endpoint"],
|
||||
json={"attachments": attachments[i:i + 100]})
|
||||
if r.status_code != 200:
|
||||
print("!!!! something went wrong while sending to:")
|
||||
print(integration)
|
||||
print(r)
|
||||
print(r.text)
|
||||
|
||||
@classmethod
|
||||
def __share(cls, tenant_id, integration_id, attachement, extra=None):
|
||||
if extra is None:
|
||||
extra = {}
|
||||
integration = cls.get_integration(tenant_id=tenant_id, integration_id=integration_id)
|
||||
if integration is None:
|
||||
return {"errors": ["slack integration not found"]}
|
||||
attachement["ts"] = datetime.now().timestamp()
|
||||
r = requests.post(url=integration["endpoint"], json={"attachments": [attachement], **extra})
|
||||
return r.text
|
||||
|
||||
@classmethod
|
||||
def share_session(cls, tenant_id, project_id, session_id, user, comment, project_name=None, integration_id=None):
|
||||
args = {"fallback": f"{user} has shared the below session!",
|
||||
"pretext": f"{user} has shared the below session!",
|
||||
"title": f"{config('SITE_URL')}/{project_id}/session/{session_id}",
|
||||
"title_link": f"{config('SITE_URL')}/{project_id}/session/{session_id}",
|
||||
"text": comment}
|
||||
data = cls.__share(tenant_id, integration_id, attachement=args)
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
@classmethod
|
||||
def share_error(cls, tenant_id, project_id, error_id, user, comment, project_name=None, integration_id=None):
|
||||
args = {"fallback": f"{user} has shared the below error!",
|
||||
"pretext": f"{user} has shared the below error!",
|
||||
"title": f"{config('SITE_URL')}/{project_id}/errors/{error_id}",
|
||||
"title_link": f"{config('SITE_URL')}/{project_id}/errors/{error_id}",
|
||||
"text": comment}
|
||||
data = cls.__share(tenant_id, integration_id, attachement=args)
|
||||
if "errors" in data:
|
||||
return data
|
||||
return {"data": data}
|
||||
|
||||
@classmethod
|
||||
def get_integration(cls, tenant_id, integration_id=None):
|
||||
if integration_id is not None:
|
||||
return webhook.get_webhook(tenant_id=tenant_id, webhook_id=integration_id,
|
||||
webhook_type=schemas.WebhookType.SLACK)
|
||||
|
||||
integrations = webhook.get_by_type(tenant_id=tenant_id, webhook_type=schemas.WebhookType.SLACK)
|
||||
if integrations is None or len(integrations) == 0:
|
||||
return None
|
||||
return integrations[0]
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
COUNTRIES = {
|
||||
"AC": "Ascension Island",
|
||||
"AD": "Andorra",
|
||||
"AE": "United Arab Emirates",
|
||||
"AF": "Afghanistan",
|
||||
"AG": "Antigua And Barbuda",
|
||||
"AI": "Anguilla",
|
||||
"AL": "Albania",
|
||||
"AM": "Armenia",
|
||||
"AN": "Netherlands Antilles",
|
||||
"AO": "Angola",
|
||||
"AQ": "Antarctica",
|
||||
"AR": "Argentina",
|
||||
"AS": "American Samoa",
|
||||
"AT": "Austria",
|
||||
"AU": "Australia",
|
||||
"AW": "Aruba",
|
||||
"AX": "Åland Islands",
|
||||
"AZ": "Azerbaijan",
|
||||
"BA": "Bosnia & Herzegovina",
|
||||
"BB": "Barbados",
|
||||
"BD": "Bangladesh",
|
||||
"BE": "Belgium",
|
||||
"BF": "Burkina Faso",
|
||||
"BG": "Bulgaria",
|
||||
"BH": "Bahrain",
|
||||
"BI": "Burundi",
|
||||
"BJ": "Benin",
|
||||
"BL": "Saint Barthélemy",
|
||||
"BM": "Bermuda",
|
||||
"BN": "Brunei Darussalam",
|
||||
"BO": "Bolivia",
|
||||
"BQ": "Bonaire, Saint Eustatius And Saba",
|
||||
"BR": "Brazil",
|
||||
"BS": "Bahamas",
|
||||
"BT": "Bhutan",
|
||||
"BU": "Burma",
|
||||
"BV": "Bouvet Island",
|
||||
"BW": "Botswana",
|
||||
"BY": "Belarus",
|
||||
"BZ": "Belize",
|
||||
"CA": "Canada",
|
||||
"CC": "Cocos Islands",
|
||||
"CD": "Congo",
|
||||
"CF": "Central African Republic",
|
||||
"CG": "Congo",
|
||||
"CH": "Switzerland",
|
||||
"CI": "Côte d'Ivoire",
|
||||
"CK": "Cook Islands",
|
||||
"CL": "Chile",
|
||||
"CM": "Cameroon",
|
||||
"CN": "China",
|
||||
"CO": "Colombia",
|
||||
"CP": "Clipperton Island",
|
||||
"CR": "Costa Rica",
|
||||
"CS": "Serbia and Montenegro",
|
||||
"CT": "Canton and Enderbury Islands",
|
||||
"CU": "Cuba",
|
||||
"CV": "Cabo Verde",
|
||||
"CW": "Curacao",
|
||||
"CX": "Christmas Island",
|
||||
"CY": "Cyprus",
|
||||
"CZ": "Czech Republic",
|
||||
"DD": "Germany",
|
||||
"DE": "Germany",
|
||||
"DG": "Diego Garcia",
|
||||
"DJ": "Djibouti",
|
||||
"DK": "Denmark",
|
||||
"DM": "Dominica",
|
||||
"DO": "Dominican Republic",
|
||||
"DY": "Dahomey",
|
||||
"DZ": "Algeria",
|
||||
"EA": "Ceuta, Mulilla",
|
||||
"EC": "Ecuador",
|
||||
"EE": "Estonia",
|
||||
"EG": "Egypt",
|
||||
"EH": "Western Sahara",
|
||||
"ER": "Eritrea",
|
||||
"ES": "Spain",
|
||||
"ET": "Ethiopia",
|
||||
"FI": "Finland",
|
||||
"FJ": "Fiji",
|
||||
"FK": "Falkland Islands",
|
||||
"FM": "Micronesia",
|
||||
"FO": "Faroe Islands",
|
||||
"FQ": "French Southern and Antarctic Territories",
|
||||
"FR": "France",
|
||||
"FX": "France, Metropolitan",
|
||||
"GA": "Gabon",
|
||||
"GB": "United Kingdom",
|
||||
"GD": "Grenada",
|
||||
"GE": "Georgia",
|
||||
"GF": "French Guiana",
|
||||
"GG": "Guernsey",
|
||||
"GH": "Ghana",
|
||||
"GI": "Gibraltar",
|
||||
"GL": "Greenland",
|
||||
"GM": "Gambia",
|
||||
"GN": "Guinea",
|
||||
"GP": "Guadeloupe",
|
||||
"GQ": "Equatorial Guinea",
|
||||
"GR": "Greece",
|
||||
"GS": "South Georgia And The South Sandwich Islands",
|
||||
"GT": "Guatemala",
|
||||
"GU": "Guam",
|
||||
"GW": "Guinea-bissau",
|
||||
"GY": "Guyana",
|
||||
"HK": "Hong Kong",
|
||||
"HM": "Heard Island And McDonald Islands",
|
||||
"HN": "Honduras",
|
||||
"HR": "Croatia",
|
||||
"HT": "Haiti",
|
||||
"HU": "Hungary",
|
||||
"HV": "Upper Volta",
|
||||
"IC": "Canary Islands",
|
||||
"ID": "Indonesia",
|
||||
"IE": "Ireland",
|
||||
"IL": "Israel",
|
||||
"IM": "Isle Of Man",
|
||||
"IN": "India",
|
||||
"IO": "British Indian Ocean Territory",
|
||||
"IQ": "Iraq",
|
||||
"IR": "Iran",
|
||||
"IS": "Iceland",
|
||||
"IT": "Italy",
|
||||
"JE": "Jersey",
|
||||
"JM": "Jamaica",
|
||||
"JO": "Jordan",
|
||||
"JP": "Japan",
|
||||
"JT": "Johnston Island",
|
||||
"KE": "Kenya",
|
||||
"KG": "Kyrgyzstan",
|
||||
"KH": "Cambodia",
|
||||
"KI": "Kiribati",
|
||||
"KM": "Comoros",
|
||||
"KN": "Saint Kitts And Nevis",
|
||||
"KP": "Korea",
|
||||
"KR": "Korea",
|
||||
"KW": "Kuwait",
|
||||
"KY": "Cayman Islands",
|
||||
"KZ": "Kazakhstan",
|
||||
"LA": "Laos",
|
||||
"LB": "Lebanon",
|
||||
"LC": "Saint Lucia",
|
||||
"LI": "Liechtenstein",
|
||||
"LK": "Sri Lanka",
|
||||
"LR": "Liberia",
|
||||
"LS": "Lesotho",
|
||||
"LT": "Lithuania",
|
||||
"LU": "Luxembourg",
|
||||
"LV": "Latvia",
|
||||
"LY": "Libya",
|
||||
"MA": "Morocco",
|
||||
"MC": "Monaco",
|
||||
"MD": "Moldova",
|
||||
"ME": "Montenegro",
|
||||
"MF": "Saint Martin",
|
||||
"MG": "Madagascar",
|
||||
"MH": "Marshall Islands",
|
||||
"MI": "Midway Islands",
|
||||
"MK": "Macedonia",
|
||||
"ML": "Mali",
|
||||
"MM": "Myanmar",
|
||||
"MN": "Mongolia",
|
||||
"MO": "Macao",
|
||||
"MP": "Northern Mariana Islands",
|
||||
"MQ": "Martinique",
|
||||
"MR": "Mauritania",
|
||||
"MS": "Montserrat",
|
||||
"MT": "Malta",
|
||||
"MU": "Mauritius",
|
||||
"MV": "Maldives",
|
||||
"MW": "Malawi",
|
||||
"MX": "Mexico",
|
||||
"MY": "Malaysia",
|
||||
"MZ": "Mozambique",
|
||||
"NA": "Namibia",
|
||||
"NC": "New Caledonia",
|
||||
"NE": "Niger",
|
||||
"NF": "Norfolk Island",
|
||||
"NG": "Nigeria",
|
||||
"NH": "New Hebrides",
|
||||
"NI": "Nicaragua",
|
||||
"NL": "Netherlands",
|
||||
"NO": "Norway",
|
||||
"NP": "Nepal",
|
||||
"NQ": "Dronning Maud Land",
|
||||
"NR": "Nauru",
|
||||
"NT": "Neutral Zone",
|
||||
"NU": "Niue",
|
||||
"NZ": "New Zealand",
|
||||
"OM": "Oman",
|
||||
"PA": "Panama",
|
||||
"PC": "Pacific Islands",
|
||||
"PE": "Peru",
|
||||
"PF": "French Polynesia",
|
||||
"PG": "Papua New Guinea",
|
||||
"PH": "Philippines",
|
||||
"PK": "Pakistan",
|
||||
"PL": "Poland",
|
||||
"PM": "Saint Pierre And Miquelon",
|
||||
"PN": "Pitcairn",
|
||||
"PR": "Puerto Rico",
|
||||
"PS": "Palestine",
|
||||
"PT": "Portugal",
|
||||
"PU": "U.S. Miscellaneous Pacific Islands",
|
||||
"PW": "Palau",
|
||||
"PY": "Paraguay",
|
||||
"PZ": "Panama Canal Zone",
|
||||
"QA": "Qatar",
|
||||
"RE": "Reunion",
|
||||
"RH": "Southern Rhodesia",
|
||||
"RO": "Romania",
|
||||
"RS": "Serbia",
|
||||
"RU": "Russian Federation",
|
||||
"RW": "Rwanda",
|
||||
"SA": "Saudi Arabia",
|
||||
"SB": "Solomon Islands",
|
||||
"SC": "Seychelles",
|
||||
"SD": "Sudan",
|
||||
"SE": "Sweden",
|
||||
"SG": "Singapore",
|
||||
"SH": "Saint Helena, Ascension And Tristan Da Cunha",
|
||||
"SI": "Slovenia",
|
||||
"SJ": "Svalbard And Jan Mayen",
|
||||
"SK": "Slovakia",
|
||||
"SL": "Sierra Leone",
|
||||
"SM": "San Marino",
|
||||
"SN": "Senegal",
|
||||
"SO": "Somalia",
|
||||
"SR": "Suriname",
|
||||
"SS": "South Sudan",
|
||||
"ST": "Sao Tome and Principe",
|
||||
"SU": "USSR",
|
||||
"SV": "El Salvador",
|
||||
"SX": "Sint Maarten",
|
||||
"SY": "Syrian Arab Republic",
|
||||
"SZ": "Swaziland",
|
||||
"TA": "Tristan de Cunha",
|
||||
"TC": "Turks And Caicos Islands",
|
||||
"TD": "Chad",
|
||||
"TF": "French Southern Territories",
|
||||
"TG": "Togo",
|
||||
"TH": "Thailand",
|
||||
"TJ": "Tajikistan",
|
||||
"TK": "Tokelau",
|
||||
"TL": "Timor-Leste",
|
||||
"TM": "Turkmenistan",
|
||||
"TN": "Tunisia",
|
||||
"TO": "Tonga",
|
||||
"TP": "East Timor",
|
||||
"TR": "Turkey",
|
||||
"TT": "Trinidad And Tobago",
|
||||
"TV": "Tuvalu",
|
||||
"TW": "Taiwan",
|
||||
"TZ": "Tanzania",
|
||||
"UA": "Ukraine",
|
||||
"UG": "Uganda",
|
||||
"UM": "United States Minor Outlying Islands",
|
||||
"UN": "United Nations",
|
||||
"US": "United States",
|
||||
"UY": "Uruguay",
|
||||
"UZ": "Uzbekistan",
|
||||
"VA": "Vatican City State",
|
||||
"VC": "Saint Vincent And The Grenadines",
|
||||
"VD": "VietNam",
|
||||
"VE": "Venezuela",
|
||||
"VG": "Virgin Islands (British)",
|
||||
"VI": "Virgin Islands (US)",
|
||||
"VN": "VietNam",
|
||||
"VU": "Vanuatu",
|
||||
"WF": "Wallis And Futuna",
|
||||
"WK": "Wake Island",
|
||||
"WS": "Samoa",
|
||||
"XK": "Kosovo",
|
||||
"YD": "Yemen",
|
||||
"YE": "Yemen",
|
||||
"YT": "Mayotte",
|
||||
"YU": "Yugoslavia",
|
||||
"ZA": "South Africa",
|
||||
"ZM": "Zambia",
|
||||
"ZR": "Zaire",
|
||||
"ZW": "Zimbabwe",
|
||||
}
|
||||
|
||||
|
||||
def get_country_code_autocomplete(text):
|
||||
if text is None or len(text) == 0:
|
||||
return []
|
||||
results = []
|
||||
for code in COUNTRIES:
|
||||
if text.lower() in code.lower() \
|
||||
or text.lower() in COUNTRIES[code].lower():
|
||||
results.append(code)
|
||||
|
||||
return results
|
||||
2285
api/chalicelib/core/dashboard.py
Normal file
2285
api/chalicelib/core/dashboard.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,205 +0,0 @@
|
|||
import logging
|
||||
|
||||
from chalicelib.utils import pg_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseRequestHandler:
|
||||
def __init__(self, table_name):
|
||||
self.table_name = table_name
|
||||
self.constraints = []
|
||||
self.params = {}
|
||||
self.order_clause = ""
|
||||
self.sort_clause = ""
|
||||
self.select_columns = []
|
||||
self.sub_queries = []
|
||||
self.joins = []
|
||||
self.group_by_clause = ""
|
||||
self.client = pg_client
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.pagination = {}
|
||||
|
||||
def add_constraint(self, constraint, param=None):
|
||||
self.constraints.append(constraint)
|
||||
if param:
|
||||
self.params.update(param)
|
||||
|
||||
def add_subquery(self, subquery, alias, param=None):
|
||||
self.sub_queries.append((subquery, alias))
|
||||
if param:
|
||||
self.params.update(param)
|
||||
|
||||
def add_join(self, join_clause):
|
||||
self.joins.append(join_clause)
|
||||
|
||||
def add_param(self, key, value):
|
||||
self.params[key] = value
|
||||
|
||||
def set_order_by(self, order_by):
|
||||
self.order_clause = order_by
|
||||
|
||||
def set_sort_by(self, sort_by):
|
||||
self.sort_clause = sort_by
|
||||
|
||||
def set_select_columns(self, columns):
|
||||
self.select_columns = columns
|
||||
|
||||
def set_group_by(self, group_by_clause):
|
||||
self.group_by_clause = group_by_clause
|
||||
|
||||
def set_pagination(self, page, page_size):
|
||||
"""
|
||||
Set pagination parameters for the query.
|
||||
:param page: The page number (1-indexed)
|
||||
:param page_size: Number of items per page
|
||||
"""
|
||||
self.pagination = {
|
||||
'offset': (page - 1) * page_size,
|
||||
'limit': page_size
|
||||
}
|
||||
|
||||
def build_query(self, action="select", additional_clauses=None, data=None):
|
||||
|
||||
if action == "select":
|
||||
query = f"SELECT {', '.join(self.select_columns)} FROM {self.table_name}"
|
||||
elif action == "insert":
|
||||
columns = ', '.join(data.keys())
|
||||
placeholders = ', '.join(f'%({k})s' for k in data.keys())
|
||||
query = f"INSERT INTO {self.table_name} ({columns}) VALUES ({placeholders})"
|
||||
elif action == "update":
|
||||
set_clause = ', '.join(f"{k} = %({k})s" for k in data.keys())
|
||||
query = f"UPDATE {self.table_name} SET {set_clause}"
|
||||
elif action == "delete":
|
||||
query = f"DELETE FROM {self.table_name}"
|
||||
|
||||
for join in self.joins:
|
||||
query += f" {join}"
|
||||
for subquery, alias in self.sub_queries:
|
||||
query += f", ({subquery}) AS {alias}"
|
||||
if self.constraints:
|
||||
query += " WHERE " + " AND ".join(self.constraints)
|
||||
if action == "select":
|
||||
if self.group_by_clause:
|
||||
query += " GROUP BY " + self.group_by_clause
|
||||
if self.sort_clause:
|
||||
query += " ORDER BY " + self.sort_clause
|
||||
if self.order_clause:
|
||||
query += " " + self.order_clause
|
||||
if hasattr(self, 'pagination') and self.pagination:
|
||||
query += " LIMIT %(limit)s OFFSET %(offset)s"
|
||||
self.params.update(self.pagination)
|
||||
|
||||
if additional_clauses:
|
||||
query += " " + additional_clauses
|
||||
|
||||
logger.debug(f"Query: {query}")
|
||||
return query
|
||||
|
||||
def execute_query(self, query, data=None):
|
||||
try:
|
||||
with self.client.PostgresClient() as cur:
|
||||
mogrified_query = cur.mogrify(query, {**data, **self.params} if data else self.params)
|
||||
cur.execute(mogrified_query)
|
||||
return cur.fetchall() if cur.description else None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database operation failed: {e}")
|
||||
raise
|
||||
|
||||
def fetchall(self):
|
||||
query = self.build_query()
|
||||
return self.execute_query(query)
|
||||
|
||||
def fetchone(self):
|
||||
query = self.build_query()
|
||||
result = self.execute_query(query)
|
||||
return result[0] if result else None
|
||||
|
||||
def insert(self, data):
|
||||
query = self.build_query(action="insert", data=data)
|
||||
query += " RETURNING *;"
|
||||
|
||||
result = self.execute_query(query, data)
|
||||
return result[0] if result else None
|
||||
|
||||
def update(self, data):
|
||||
query = self.build_query(action="update", data=data)
|
||||
query += " RETURNING *;"
|
||||
|
||||
result = self.execute_query(query, data)
|
||||
return result[0] if result else None
|
||||
|
||||
def delete(self):
|
||||
query = self.build_query(action="delete")
|
||||
return self.execute_query(query)
|
||||
|
||||
def batch_insert(self, items):
|
||||
if not items:
|
||||
return None
|
||||
|
||||
columns = ', '.join(items[0].keys())
|
||||
|
||||
# Building a values string with unique parameter names for each item
|
||||
all_values_query = ', '.join(
|
||||
'(' + ', '.join([f"%({key}_{i})s" for key in item]) + ')'
|
||||
for i, item in enumerate(items)
|
||||
)
|
||||
|
||||
query = f"INSERT INTO {self.table_name} ({columns}) VALUES {all_values_query} RETURNING *;"
|
||||
|
||||
try:
|
||||
with self.client.PostgresClient() as cur:
|
||||
# Flatten items into a single dictionary with unique keys
|
||||
combined_params = {f"{k}_{i}": v for i, item in enumerate(items) for k, v in item.items()}
|
||||
mogrified_query = cur.mogrify(query, combined_params)
|
||||
cur.execute(mogrified_query)
|
||||
return cur.fetchall()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database batch insert operation failed: {e}")
|
||||
raise
|
||||
|
||||
def raw_query(self, query, params=None):
|
||||
try:
|
||||
with self.client.PostgresClient() as cur:
|
||||
mogrified_query = cur.mogrify(query, params)
|
||||
cur.execute(mogrified_query)
|
||||
return cur.fetchall() if cur.description else None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database operation failed: {e}")
|
||||
raise
|
||||
|
||||
def batch_update(self, items):
|
||||
if not items:
|
||||
return None
|
||||
|
||||
id_column = list(items[0])[0]
|
||||
|
||||
# Building the set clause for the update statement
|
||||
update_columns = list(items[0].keys())
|
||||
update_columns.remove(id_column)
|
||||
set_clause = ', '.join([f"{col} = v.{col}" for col in update_columns])
|
||||
|
||||
# Building the values part for the 'VALUES' section
|
||||
values_rows = []
|
||||
for item in items:
|
||||
values = ', '.join([f"%({key})s" for key in item.keys()])
|
||||
values_rows.append(f"({values})")
|
||||
values_query = ', '.join(values_rows)
|
||||
|
||||
# Constructing the full update query
|
||||
query = f"""
|
||||
UPDATE {self.table_name} AS t
|
||||
SET {set_clause}
|
||||
FROM (VALUES {values_query}) AS v ({', '.join(items[0].keys())})
|
||||
WHERE t.{id_column} = v.{id_column};
|
||||
"""
|
||||
|
||||
try:
|
||||
with self.client.PostgresClient() as cur:
|
||||
# Flatten items into a single dictionary for mogrify
|
||||
combined_params = {k: v for item in items for k, v in item.items()}
|
||||
mogrified_query = cur.mogrify(query, combined_params)
|
||||
cur.execute(mogrified_query)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database batch update operation failed: {e}")
|
||||
raise
|
||||
777
api/chalicelib/core/errors.py
Normal file
777
api/chalicelib/core/errors.py
Normal file
|
|
@ -0,0 +1,777 @@
|
|||
import json
|
||||
|
||||
from chalicelib.utils import pg_client, helper, dev
|
||||
from chalicelib.core import sourcemaps, sessions
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.utils.metrics_helper import __get_step_size
|
||||
|
||||
|
||||
def get(error_id, family=False):
|
||||
if family:
|
||||
return get_batch([error_id])
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"SELECT * FROM events.errors AS e INNER JOIN public.errors AS re USING(error_id) WHERE error_id = %(error_id)s;",
|
||||
{"error_id": error_id})
|
||||
cur.execute(query=query)
|
||||
result = cur.fetchone()
|
||||
if result is not None:
|
||||
result["stacktrace_parsed_at"] = TimeUTC.datetime_to_timestamp(result["stacktrace_parsed_at"])
|
||||
return helper.dict_to_camel_case(result)
|
||||
|
||||
|
||||
def get_batch(error_ids):
|
||||
if len(error_ids) == 0:
|
||||
return []
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""
|
||||
WITH RECURSIVE error_family AS (
|
||||
SELECT *
|
||||
FROM public.errors
|
||||
WHERE error_id IN %(error_ids)s
|
||||
UNION
|
||||
SELECT child_errors.*
|
||||
FROM public.errors AS child_errors
|
||||
INNER JOIN error_family ON error_family.error_id = child_errors.parent_error_id OR error_family.parent_error_id = child_errors.error_id
|
||||
)
|
||||
SELECT *
|
||||
FROM error_family;""",
|
||||
{"error_ids": tuple(error_ids)})
|
||||
cur.execute(query=query)
|
||||
return helper.list_to_camel_case(cur.fetchall())
|
||||
|
||||
|
||||
def __flatten_sort_key_count_version(data, merge_nested=False):
|
||||
if data is None:
|
||||
return []
|
||||
return sorted(
|
||||
[
|
||||
{
|
||||
"name": f'{o["name"]}@{v["version"]}',
|
||||
"count": v["count"]
|
||||
} for o in data for v in o["partition"]
|
||||
],
|
||||
key=lambda o: o["count"], reverse=True) if merge_nested else \
|
||||
[
|
||||
{
|
||||
"name": o["name"],
|
||||
"count": o["count"],
|
||||
} for o in data
|
||||
]
|
||||
|
||||
|
||||
def __process_tags(row):
|
||||
return [
|
||||
{"name": "browser", "partitions": __flatten_sort_key_count_version(data=row.get("browsers_partition"))},
|
||||
{"name": "browser.ver",
|
||||
"partitions": __flatten_sort_key_count_version(data=row.pop("browsers_partition"), merge_nested=True)},
|
||||
{"name": "OS", "partitions": __flatten_sort_key_count_version(data=row.get("os_partition"))},
|
||||
{"name": "OS.ver",
|
||||
"partitions": __flatten_sort_key_count_version(data=row.pop("os_partition"), merge_nested=True)},
|
||||
{"name": "device.family", "partitions": __flatten_sort_key_count_version(data=row.get("device_partition"))},
|
||||
{"name": "device",
|
||||
"partitions": __flatten_sort_key_count_version(data=row.pop("device_partition"), merge_nested=True)},
|
||||
{"name": "country", "partitions": row.pop("country_partition")}
|
||||
]
|
||||
|
||||
|
||||
def get_details(project_id, error_id, user_id, **data):
|
||||
pg_sub_query24 = __get_basic_constraints(time_constraint=False, chart=True, step_size_name="step_size24")
|
||||
pg_sub_query24.append("error_id = %(error_id)s")
|
||||
pg_sub_query30 = __get_basic_constraints(time_constraint=False, chart=True, step_size_name="step_size30")
|
||||
pg_sub_query30.append("error_id = %(error_id)s")
|
||||
pg_basic_query = __get_basic_constraints(time_constraint=False)
|
||||
pg_basic_query.append("error_id = %(error_id)s")
|
||||
with pg_client.PostgresClient() as cur:
|
||||
data["startDate24"] = TimeUTC.now(-1)
|
||||
data["endDate24"] = TimeUTC.now()
|
||||
data["startDate30"] = TimeUTC.now(-30)
|
||||
data["endDate30"] = TimeUTC.now()
|
||||
density24 = int(data.get("density24", 24))
|
||||
step_size24 = __get_step_size(data["startDate24"], data["endDate24"], density24, factor=1)
|
||||
density30 = int(data.get("density30", 30))
|
||||
step_size30 = __get_step_size(data["startDate30"], data["endDate30"], density30, factor=1)
|
||||
params = {
|
||||
"startDate24": data['startDate24'],
|
||||
"endDate24": data['endDate24'],
|
||||
"startDate30": data['startDate30'],
|
||||
"endDate30": data['endDate30'],
|
||||
"project_id": project_id,
|
||||
"userId": user_id,
|
||||
"step_size24": step_size24,
|
||||
"step_size30": step_size30,
|
||||
"error_id": error_id}
|
||||
|
||||
main_pg_query = f"""\
|
||||
SELECT error_id,
|
||||
name,
|
||||
message,
|
||||
users,
|
||||
sessions,
|
||||
last_occurrence,
|
||||
first_occurrence,
|
||||
last_session_id,
|
||||
browsers_partition,
|
||||
os_partition,
|
||||
device_partition,
|
||||
country_partition,
|
||||
chart24,
|
||||
chart30
|
||||
FROM (SELECT error_id,
|
||||
name,
|
||||
message,
|
||||
COUNT(DISTINCT user_uuid) AS users,
|
||||
COUNT(DISTINCT session_id) AS sessions
|
||||
FROM public.errors
|
||||
INNER JOIN events.errors AS s_errors USING (error_id)
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE error_id = %(error_id)s
|
||||
GROUP BY error_id, name, message) AS details
|
||||
INNER JOIN (SELECT error_id,
|
||||
MAX(timestamp) AS last_occurrence,
|
||||
MIN(timestamp) AS first_occurrence
|
||||
FROM events.errors
|
||||
WHERE error_id = %(error_id)s
|
||||
GROUP BY error_id) AS time_details USING (error_id)
|
||||
INNER JOIN (SELECT error_id,
|
||||
session_id AS last_session_id,
|
||||
user_os,
|
||||
user_os_version,
|
||||
user_browser,
|
||||
user_browser_version,
|
||||
user_device,
|
||||
user_device_type,
|
||||
user_uuid
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE error_id = %(error_id)s
|
||||
ORDER BY errors.timestamp DESC
|
||||
LIMIT 1) AS last_session_details USING (error_id)
|
||||
INNER JOIN (SELECT jsonb_agg(browser_details) AS browsers_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_browser AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors
|
||||
INNER JOIN sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
GROUP BY user_browser
|
||||
ORDER BY count DESC) AS count_per_browser_query
|
||||
INNER JOIN LATERAL (SELECT JSONB_AGG(version_details) AS partition
|
||||
FROM (SELECT user_browser_version AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
AND sessions.user_browser = count_per_browser_query.name
|
||||
GROUP BY user_browser_version
|
||||
ORDER BY count DESC) AS version_details
|
||||
) AS browser_version_details ON (TRUE)) AS browser_details) AS browser_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(os_details) AS os_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_os AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
GROUP BY user_os
|
||||
ORDER BY count DESC) AS count_per_os_details
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_version_details) AS partition
|
||||
FROM (SELECT COALESCE(user_os_version,'unknown') AS version, COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
AND sessions.user_os = count_per_os_details.name
|
||||
GROUP BY user_os_version
|
||||
ORDER BY count DESC) AS count_per_version_details
|
||||
GROUP BY count_per_os_details.name ) AS os_version_details
|
||||
ON (TRUE)) AS os_details) AS os_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(device_details) AS device_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_device_type AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
GROUP BY user_device_type
|
||||
ORDER BY count DESC) AS count_per_device_details
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_device_v_details) AS partition
|
||||
FROM (SELECT CASE
|
||||
WHEN user_device = '' OR user_device ISNULL
|
||||
THEN 'unknown'
|
||||
ELSE user_device END AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
AND sessions.user_device_type = count_per_device_details.name
|
||||
GROUP BY user_device
|
||||
ORDER BY count DESC) AS count_per_device_v_details
|
||||
GROUP BY count_per_device_details.name ) AS device_version_details
|
||||
ON (TRUE)) AS device_details) AS device_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(count_per_country_details) AS country_partition
|
||||
FROM (SELECT user_country AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_basic_query)}
|
||||
GROUP BY user_country
|
||||
ORDER BY count DESC) AS count_per_country_details) AS country_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(chart_details) AS chart24
|
||||
FROM (SELECT generated_timestamp AS timestamp,
|
||||
COUNT(session_id) AS count
|
||||
FROM generate_series(%(startDate24)s, %(endDate24)s, %(step_size24)s) AS generated_timestamp
|
||||
LEFT JOIN LATERAL (SELECT DISTINCT session_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query24)}
|
||||
) AS chart_details ON (TRUE)
|
||||
GROUP BY generated_timestamp
|
||||
ORDER BY generated_timestamp) AS chart_details) AS chart_details24 ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(chart_details) AS chart30
|
||||
FROM (SELECT generated_timestamp AS timestamp,
|
||||
COUNT(session_id) AS count
|
||||
FROM generate_series(%(startDate30)s, %(endDate30)s, %(step_size30)s) AS generated_timestamp
|
||||
LEFT JOIN LATERAL (SELECT DISTINCT session_id
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30)}) AS chart_details
|
||||
ON (TRUE)
|
||||
GROUP BY timestamp
|
||||
ORDER BY timestamp) AS chart_details) AS chart_details30 ON (TRUE);
|
||||
"""
|
||||
|
||||
# print("--------------------")
|
||||
# print(cur.mogrify(main_pg_query, params))
|
||||
# print("--------------------")
|
||||
cur.execute(cur.mogrify(main_pg_query, params))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return {"errors": ["error not found"]}
|
||||
row["tags"] = __process_tags(row)
|
||||
|
||||
query = cur.mogrify(
|
||||
f"""SELECT error_id, status, session_id, start_ts,
|
||||
parent_error_id,session_id, user_anonymous_id,
|
||||
user_id, user_uuid, user_browser, user_browser_version,
|
||||
user_os, user_os_version, user_device, payload,
|
||||
COALESCE((SELECT TRUE
|
||||
FROM public.user_favorite_errors AS fe
|
||||
WHERE pe.error_id = fe.error_id
|
||||
AND fe.user_id = %(user_id)s), FALSE) AS favorite,
|
||||
True AS viewed
|
||||
FROM public.errors AS pe
|
||||
INNER JOIN events.errors AS ee USING (error_id)
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE pe.project_id = %(project_id)s
|
||||
AND error_id = %(error_id)s
|
||||
ORDER BY start_ts DESC
|
||||
LIMIT 1;""",
|
||||
{"project_id": project_id, "error_id": error_id, "user_id": user_id})
|
||||
cur.execute(query=query)
|
||||
status = cur.fetchone()
|
||||
|
||||
if status is not None:
|
||||
row["stack"] = format_first_stack_frame(status).pop("stack")
|
||||
row["status"] = status.pop("status")
|
||||
row["parent_error_id"] = status.pop("parent_error_id")
|
||||
row["favorite"] = status.pop("favorite")
|
||||
row["viewed"] = status.pop("viewed")
|
||||
row["last_hydrated_session"] = status
|
||||
else:
|
||||
row["stack"] = []
|
||||
row["last_hydrated_session"] = None
|
||||
row["status"] = "untracked"
|
||||
row["parent_error_id"] = None
|
||||
row["favorite"] = False
|
||||
row["viewed"] = False
|
||||
return {"data": helper.dict_to_camel_case(row)}
|
||||
|
||||
|
||||
def get_details_chart(project_id, error_id, user_id, **data):
|
||||
pg_sub_query = __get_basic_constraints()
|
||||
pg_sub_query.append("error_id = %(error_id)s")
|
||||
pg_sub_query_chart = __get_basic_constraints(time_constraint=False, chart=True)
|
||||
pg_sub_query_chart.append("error_id = %(error_id)s")
|
||||
with pg_client.PostgresClient() as cur:
|
||||
if data.get("startDate") is None:
|
||||
data["startDate"] = TimeUTC.now(-7)
|
||||
else:
|
||||
data["startDate"] = int(data["startDate"])
|
||||
if data.get("endDate") is None:
|
||||
data["endDate"] = TimeUTC.now()
|
||||
else:
|
||||
data["endDate"] = int(data["endDate"])
|
||||
density = int(data.get("density", 7))
|
||||
step_size = __get_step_size(data["startDate"], data["endDate"], density, factor=1)
|
||||
params = {
|
||||
"startDate": data['startDate'],
|
||||
"endDate": data['endDate'],
|
||||
"project_id": project_id,
|
||||
"userId": user_id,
|
||||
"step_size": step_size,
|
||||
"error_id": error_id}
|
||||
|
||||
main_pg_query = f"""\
|
||||
SELECT %(error_id)s AS error_id,
|
||||
browsers_partition,
|
||||
os_partition,
|
||||
device_partition,
|
||||
country_partition,
|
||||
chart
|
||||
FROM (SELECT jsonb_agg(browser_details) AS browsers_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_browser AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
GROUP BY user_browser
|
||||
ORDER BY count DESC) AS count_per_browser_query
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_version_details) AS partition
|
||||
FROM (SELECT user_browser_version AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
AND user_browser = count_per_browser_query.name
|
||||
GROUP BY user_browser_version
|
||||
ORDER BY count DESC) AS count_per_version_details) AS browesr_version_details
|
||||
ON (TRUE)) AS browser_details) AS browser_details
|
||||
INNER JOIN (SELECT jsonb_agg(os_details) AS os_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_os AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
GROUP BY user_os
|
||||
ORDER BY count DESC) AS count_per_os_details
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_version_query) AS partition
|
||||
FROM (SELECT COALESCE(user_os_version, 'unknown') AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
AND user_os = count_per_os_details.name
|
||||
GROUP BY user_os_version
|
||||
ORDER BY count DESC) AS count_per_version_query
|
||||
) AS os_version_query ON (TRUE)) AS os_details) AS os_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(device_details) AS device_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_device_type AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
GROUP BY user_device_type
|
||||
ORDER BY count DESC) AS count_per_device_details
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_device_details) AS partition
|
||||
FROM (SELECT CASE
|
||||
WHEN user_device = '' OR user_device ISNULL
|
||||
THEN 'unknown'
|
||||
ELSE user_device END AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
AND user_device_type = count_per_device_details.name
|
||||
GROUP BY user_device_type, user_device
|
||||
ORDER BY count DESC) AS count_per_device_details
|
||||
) AS device_version_details ON (TRUE)) AS device_details) AS device_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(count_per_country_details) AS country_partition
|
||||
FROM (SELECT user_country AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
GROUP BY user_country
|
||||
ORDER BY count DESC) AS count_per_country_details) AS country_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(chart_details) AS chart
|
||||
FROM (SELECT generated_timestamp AS timestamp,
|
||||
COUNT(session_id) AS count
|
||||
FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS generated_timestamp
|
||||
LEFT JOIN LATERAL (SELECT DISTINCT session_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query_chart)}
|
||||
) AS chart_details ON (TRUE)
|
||||
GROUP BY generated_timestamp
|
||||
ORDER BY generated_timestamp) AS chart_details) AS chart_details ON (TRUE);"""
|
||||
|
||||
cur.execute(cur.mogrify(main_pg_query, params))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return {"errors": ["error not found"]}
|
||||
row["tags"] = __process_tags(row)
|
||||
return {"data": helper.dict_to_camel_case(row)}
|
||||
|
||||
|
||||
def __get_basic_constraints(platform=None, time_constraint=True, startTime_arg_name="startDate",
|
||||
endTime_arg_name="endDate", chart=False, step_size_name="step_size",
|
||||
project_key="project_id"):
|
||||
ch_sub_query = [f"{project_key} =%(project_id)s"]
|
||||
if time_constraint:
|
||||
ch_sub_query += [f"timestamp >= %({startTime_arg_name})s",
|
||||
f"timestamp < %({endTime_arg_name})s"]
|
||||
if chart:
|
||||
ch_sub_query += [f"timestamp >= generated_timestamp",
|
||||
f"timestamp < generated_timestamp + %({step_size_name})s"]
|
||||
if platform == 'mobile':
|
||||
ch_sub_query.append("user_device_type = 'mobile'")
|
||||
elif platform == 'desktop':
|
||||
ch_sub_query.append("user_device_type = 'desktop'")
|
||||
return ch_sub_query
|
||||
|
||||
|
||||
def __get_sort_key(key):
|
||||
return {
|
||||
"datetime": "max_datetime",
|
||||
"lastOccurrence": "max_datetime",
|
||||
"firstOccurrence": "min_datetime"
|
||||
}.get(key, 'max_datetime')
|
||||
|
||||
|
||||
@dev.timed
|
||||
def search(data, project_id, user_id, flows=False, status="ALL", favorite_only=False):
|
||||
status = status.upper()
|
||||
if status.lower() not in ['all', 'unresolved', 'resolved', 'ignored']:
|
||||
return {"errors": ["invalid error status"]}
|
||||
pg_sub_query = __get_basic_constraints(data.get('platform'), project_key="sessions.project_id")
|
||||
pg_sub_query += ["sessions.start_ts>=%(startDate)s", "sessions.start_ts<%(endDate)s", "source ='js_exception'",
|
||||
"pe.project_id=%(project_id)s"]
|
||||
pg_sub_query_chart = __get_basic_constraints(data.get('platform'), time_constraint=False, chart=True)
|
||||
pg_sub_query_chart.append("source ='js_exception'")
|
||||
pg_sub_query_chart.append("errors.error_id =details.error_id")
|
||||
statuses = []
|
||||
error_ids = None
|
||||
if data.get("startDate") is None:
|
||||
data["startDate"] = TimeUTC.now(-30)
|
||||
if data.get("endDate") is None:
|
||||
data["endDate"] = TimeUTC.now(1)
|
||||
if len(data.get("events", [])) > 0 or len(data.get("filters", [])) > 0 or status != "ALL" or favorite_only:
|
||||
statuses = sessions.search2_pg(data=data, project_id=project_id, user_id=user_id, errors_only=True,
|
||||
error_status=status, favorite_only=favorite_only)
|
||||
if len(statuses) == 0:
|
||||
return {"data": {
|
||||
'total': 0,
|
||||
'errors': []
|
||||
}}
|
||||
error_ids = [e["error_id"] for e in statuses]
|
||||
with pg_client.PostgresClient() as cur:
|
||||
if data.get("startDate") is None:
|
||||
data["startDate"] = TimeUTC.now(-7)
|
||||
if data.get("endDate") is None:
|
||||
data["endDate"] = TimeUTC.now()
|
||||
density = data.get("density", 7)
|
||||
step_size = __get_step_size(data["startDate"], data["endDate"], density, factor=1)
|
||||
sort = __get_sort_key('datetime')
|
||||
if data.get("sort") is not None:
|
||||
sort = __get_sort_key(data["sort"])
|
||||
order = "DESC"
|
||||
if data.get("order") is not None:
|
||||
order = data["order"]
|
||||
|
||||
params = {
|
||||
"startDate": data['startDate'],
|
||||
"endDate": data['endDate'],
|
||||
"project_id": project_id,
|
||||
"userId": user_id,
|
||||
"step_size": step_size}
|
||||
if error_ids is not None:
|
||||
params["error_ids"] = tuple(error_ids)
|
||||
pg_sub_query.append("error_id IN %(error_ids)s")
|
||||
main_pg_query = f"""\
|
||||
SELECT error_id,
|
||||
name,
|
||||
message,
|
||||
users,
|
||||
sessions,
|
||||
last_occurrence,
|
||||
first_occurrence,
|
||||
chart
|
||||
FROM (SELECT error_id,
|
||||
name,
|
||||
message,
|
||||
COUNT(DISTINCT user_uuid) AS users,
|
||||
COUNT(DISTINCT session_id) AS sessions,
|
||||
MAX(timestamp) AS max_datetime,
|
||||
MIN(timestamp) AS min_datetime
|
||||
FROM events.errors
|
||||
INNER JOIN public.errors AS pe USING (error_id)
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query)}
|
||||
GROUP BY error_id, name, message
|
||||
ORDER BY {sort} {order}) AS details
|
||||
INNER JOIN LATERAL (SELECT MAX(timestamp) AS last_occurrence,
|
||||
MIN(timestamp) AS first_occurrence
|
||||
FROM events.errors
|
||||
WHERE errors.error_id = details.error_id) AS time_details ON (TRUE)
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(chart_details) AS chart
|
||||
FROM (SELECT generated_timestamp AS timestamp,
|
||||
COUNT(session_id) AS count
|
||||
FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS generated_timestamp
|
||||
LEFT JOIN LATERAL (SELECT DISTINCT session_id
|
||||
FROM events.errors INNER JOIN public.errors AS m_errors USING (error_id)
|
||||
WHERE {" AND ".join(pg_sub_query_chart)}
|
||||
) AS sessions ON (TRUE)
|
||||
GROUP BY timestamp
|
||||
ORDER BY timestamp) AS chart_details) AS chart_details ON (TRUE);"""
|
||||
|
||||
# print("--------------------")
|
||||
# print(cur.mogrify(main_pg_query, params))
|
||||
cur.execute(cur.mogrify(main_pg_query, params))
|
||||
total = cur.rowcount
|
||||
if flows:
|
||||
return {"data": {"count": total}}
|
||||
row = cur.fetchone()
|
||||
rows = []
|
||||
limit = 200
|
||||
while row is not None and len(rows) < limit:
|
||||
rows.append(row)
|
||||
row = cur.fetchone()
|
||||
if total == 0:
|
||||
rows = []
|
||||
else:
|
||||
if len(statuses) == 0:
|
||||
query = cur.mogrify(
|
||||
"""SELECT error_id, status, parent_error_id, payload,
|
||||
COALESCE((SELECT TRUE
|
||||
FROM public.user_favorite_errors AS fe
|
||||
WHERE errors.error_id = fe.error_id
|
||||
AND fe.user_id = %(user_id)s LIMIT 1), FALSE) AS favorite,
|
||||
COALESCE((SELECT TRUE
|
||||
FROM public.user_viewed_errors AS ve
|
||||
WHERE errors.error_id = ve.error_id
|
||||
AND ve.user_id = %(user_id)s LIMIT 1), FALSE) AS viewed
|
||||
FROM public.errors
|
||||
WHERE project_id = %(project_id)s AND error_id IN %(error_ids)s;""",
|
||||
{"project_id": project_id, "error_ids": tuple([r["error_id"] for r in rows]),
|
||||
"user_id": user_id})
|
||||
cur.execute(query=query)
|
||||
statuses = cur.fetchall()
|
||||
statuses = {
|
||||
s["error_id"]: s for s in statuses
|
||||
}
|
||||
|
||||
for r in rows:
|
||||
if r["error_id"] in statuses:
|
||||
r["status"] = statuses[r["error_id"]]["status"]
|
||||
r["parent_error_id"] = statuses[r["error_id"]]["parent_error_id"]
|
||||
r["favorite"] = statuses[r["error_id"]]["favorite"]
|
||||
r["viewed"] = statuses[r["error_id"]]["viewed"]
|
||||
r["stack"] = format_first_stack_frame(statuses[r["error_id"]])["stack"]
|
||||
else:
|
||||
r["status"] = "untracked"
|
||||
r["parent_error_id"] = None
|
||||
r["favorite"] = False
|
||||
r["viewed"] = False
|
||||
r["stack"] = None
|
||||
|
||||
offset = len(rows)
|
||||
rows = [r for r in rows if r["stack"] is None
|
||||
or (len(r["stack"]) == 0 or len(r["stack"]) > 1
|
||||
or len(r["stack"]) > 0
|
||||
and (r["message"].lower() != "script error." or len(r["stack"][0]["absPath"]) > 0))]
|
||||
offset -= len(rows)
|
||||
return {
|
||||
"data": {
|
||||
'total': total - offset,
|
||||
'errors': helper.list_to_camel_case(rows)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def __save_stacktrace(error_id, data):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""UPDATE public.errors
|
||||
SET stacktrace=%(data)s::jsonb, stacktrace_parsed_at=timezone('utc'::text, now())
|
||||
WHERE error_id = %(error_id)s;""",
|
||||
{"error_id": error_id, "data": json.dumps(data)})
|
||||
cur.execute(query=query)
|
||||
|
||||
|
||||
def get_trace(project_id, error_id):
|
||||
error = get(error_id=error_id)
|
||||
if error is None:
|
||||
return {"errors": ["error not found"]}
|
||||
if error.get("source", "") != "js_exception":
|
||||
return {"errors": ["this source of errors doesn't have a sourcemap"]}
|
||||
if error.get("payload") is None:
|
||||
return {"errors": ["null payload"]}
|
||||
if error.get("stacktrace") is not None:
|
||||
return {"sourcemapUploaded": True,
|
||||
"trace": error.get("stacktrace"),
|
||||
"preparsed": True}
|
||||
trace, all_exists = sourcemaps.get_traces_group(project_id=project_id, payload=error["payload"])
|
||||
if all_exists:
|
||||
__save_stacktrace(error_id=error_id, data=trace)
|
||||
return {"sourcemapUploaded": all_exists,
|
||||
"trace": trace,
|
||||
"preparsed": False}
|
||||
|
||||
|
||||
def get_sessions(start_date, end_date, project_id, user_id, error_id):
|
||||
extra_constraints = ["s.project_id = %(project_id)s",
|
||||
"s.start_ts >= %(startDate)s",
|
||||
"s.start_ts <= %(endDate)s",
|
||||
"e.error_id = %(error_id)s"]
|
||||
if start_date is None:
|
||||
start_date = TimeUTC.now(-7)
|
||||
if end_date is None:
|
||||
end_date = TimeUTC.now()
|
||||
|
||||
params = {
|
||||
"startDate": start_date,
|
||||
"endDate": end_date,
|
||||
"project_id": project_id,
|
||||
"userId": user_id,
|
||||
"error_id": error_id}
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
f"""SELECT s.project_id,
|
||||
s.session_id::text AS session_id,
|
||||
s.user_uuid,
|
||||
s.user_id,
|
||||
s.user_agent,
|
||||
s.user_os,
|
||||
s.user_browser,
|
||||
s.user_device,
|
||||
s.user_country,
|
||||
s.start_ts,
|
||||
s.duration,
|
||||
s.events_count,
|
||||
s.pages_count,
|
||||
s.errors_count,
|
||||
s.issue_types,
|
||||
COALESCE((SELECT TRUE
|
||||
FROM public.user_favorite_sessions AS fs
|
||||
WHERE s.session_id = fs.session_id
|
||||
AND fs.user_id = %(userId)s LIMIT 1), FALSE) AS favorite,
|
||||
COALESCE((SELECT TRUE
|
||||
FROM public.user_viewed_sessions AS fs
|
||||
WHERE s.session_id = fs.session_id
|
||||
AND fs.user_id = %(userId)s LIMIT 1), FALSE) AS viewed
|
||||
FROM public.sessions AS s INNER JOIN events.errors AS e USING (session_id)
|
||||
WHERE {" AND ".join(extra_constraints)}
|
||||
ORDER BY s.start_ts DESC;""",
|
||||
params)
|
||||
cur.execute(query=query)
|
||||
sessions_list = []
|
||||
total = cur.rowcount
|
||||
row = cur.fetchone()
|
||||
while row is not None and len(sessions_list) < 100:
|
||||
sessions_list.append(row)
|
||||
row = cur.fetchone()
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'sessions': helper.list_to_camel_case(sessions_list)
|
||||
}
|
||||
|
||||
|
||||
ACTION_STATE = {
|
||||
"unsolve": 'unresolved',
|
||||
"solve": 'resolved',
|
||||
"ignore": 'ignored'
|
||||
}
|
||||
|
||||
|
||||
def change_state(project_id, user_id, error_id, action):
|
||||
errors = get(error_id, family=True)
|
||||
print(len(errors))
|
||||
status = ACTION_STATE.get(action)
|
||||
if errors is None or len(errors) == 0:
|
||||
return {"errors": ["error not found"]}
|
||||
if errors[0]["status"] == status:
|
||||
return {"errors": [f"error is already {status}"]}
|
||||
|
||||
if errors[0]["status"] == ACTION_STATE["solve"] and status == ACTION_STATE["ignore"]:
|
||||
return {"errors": [f"state transition not permitted {errors[0]['status']} -> {status}"]}
|
||||
|
||||
params = {
|
||||
"userId": user_id,
|
||||
"error_ids": tuple([e["errorId"] for e in errors]),
|
||||
"status": status}
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""UPDATE public.errors
|
||||
SET status = %(status)s
|
||||
WHERE error_id IN %(error_ids)s
|
||||
RETURNING status""",
|
||||
params)
|
||||
cur.execute(query=query)
|
||||
row = cur.fetchone()
|
||||
if row is not None:
|
||||
for e in errors:
|
||||
e["status"] = row["status"]
|
||||
return {"data": errors}
|
||||
|
||||
|
||||
MAX_RANK = 2
|
||||
|
||||
|
||||
def __status_rank(status):
|
||||
return {
|
||||
'unresolved': MAX_RANK - 2,
|
||||
'ignored': MAX_RANK - 1,
|
||||
'resolved': MAX_RANK
|
||||
}.get(status)
|
||||
|
||||
|
||||
def merge(error_ids):
|
||||
error_ids = list(set(error_ids))
|
||||
errors = get_batch(error_ids)
|
||||
if len(error_ids) <= 1 or len(error_ids) > len(errors):
|
||||
return {"errors": ["invalid list of ids"]}
|
||||
error_ids = [e["errorId"] for e in errors]
|
||||
parent_error_id = error_ids[0]
|
||||
status = "unresolved"
|
||||
for e in errors:
|
||||
if __status_rank(status) < __status_rank(e["status"]):
|
||||
status = e["status"]
|
||||
if __status_rank(status) == MAX_RANK:
|
||||
break
|
||||
params = {
|
||||
"error_ids": tuple(error_ids),
|
||||
"parent_error_id": parent_error_id,
|
||||
"status": status
|
||||
}
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""UPDATE public.errors
|
||||
SET parent_error_id = %(parent_error_id)s, status = %(status)s
|
||||
WHERE error_id IN %(error_ids)s OR parent_error_id IN %(error_ids)s;""",
|
||||
params)
|
||||
cur.execute(query=query)
|
||||
# row = cur.fetchone()
|
||||
|
||||
return {"data": "success"}
|
||||
|
||||
|
||||
def format_first_stack_frame(error):
|
||||
error["stack"] = sourcemaps.format_payload(error.pop("payload"), truncate_to_first=True)
|
||||
for s in error["stack"]:
|
||||
for c in s.get("context", []):
|
||||
for sci, sc in enumerate(c):
|
||||
if isinstance(sc, str) and len(sc) > 1000:
|
||||
c[sci] = sc[:1000]
|
||||
# convert bytes to string:
|
||||
if isinstance(s["filename"], bytes):
|
||||
s["filename"] = s["filename"].decode("utf-8")
|
||||
return error
|
||||
|
||||
|
||||
def stats(project_id, user_id, startTimestamp=TimeUTC.now(delta_days=-7), endTimestamp=TimeUTC.now()):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""
|
||||
SELECT COUNT(errors.*) AS unresolved_and_unviewed
|
||||
FROM public.errors
|
||||
INNER JOIN (SELECT root_error.error_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.errors AS root_error USING (error_id)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND timestamp >= %(startTimestamp)s
|
||||
AND timestamp <= %(endTimestamp)s
|
||||
AND source = 'js_exception') AS timed_errors USING (error_id)
|
||||
LEFT JOIN (SELECT error_id FROM public.user_viewed_errors WHERE user_id = %(user_id)s) AS user_viewed
|
||||
USING (error_id)
|
||||
WHERE user_viewed.error_id ISNULL
|
||||
AND errors.project_id = %(project_id)s
|
||||
AND errors.status = 'unresolved'
|
||||
AND errors.source = 'js_exception';""",
|
||||
{"project_id": project_id, "user_id": user_id, "startTimestamp": startTimestamp,
|
||||
"endTimestamp": endTimestamp})
|
||||
cur.execute(query=query)
|
||||
row = cur.fetchone()
|
||||
|
||||
return {
|
||||
"data": helper.dict_to_camel_case(row)
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import logging
|
||||
|
||||
from decouple import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from . import errors_pg as errors_legacy
|
||||
|
||||
if config("EXP_ERRORS_SEARCH", cast=bool, default=False):
|
||||
logger.info(">>> Using experimental error search")
|
||||
from . import errors_ch as errors
|
||||
else:
|
||||
from . import errors_pg as errors
|
||||
|
|
@ -1,409 +0,0 @@
|
|||
import schemas
|
||||
from chalicelib.core import metadata
|
||||
from chalicelib.core.errors import errors_legacy
|
||||
from chalicelib.core.errors.modules import errors_helper
|
||||
from chalicelib.core.errors.modules import sessions
|
||||
from chalicelib.utils import ch_client, exp_ch_helper
|
||||
from chalicelib.utils import helper, metrics_helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
|
||||
def _multiple_values(values, value_key="value"):
|
||||
query_values = {}
|
||||
if values is not None and isinstance(values, list):
|
||||
for i in range(len(values)):
|
||||
k = f"{value_key}_{i}"
|
||||
query_values[k] = values[i]
|
||||
return query_values
|
||||
|
||||
|
||||
def __get_sql_operator(op: schemas.SearchEventOperator):
|
||||
return {
|
||||
schemas.SearchEventOperator.IS: "=",
|
||||
schemas.SearchEventOperator.IS_ANY: "IN",
|
||||
schemas.SearchEventOperator.ON: "=",
|
||||
schemas.SearchEventOperator.ON_ANY: "IN",
|
||||
schemas.SearchEventOperator.IS_NOT: "!=",
|
||||
schemas.SearchEventOperator.NOT_ON: "!=",
|
||||
schemas.SearchEventOperator.CONTAINS: "ILIKE",
|
||||
schemas.SearchEventOperator.NOT_CONTAINS: "NOT ILIKE",
|
||||
schemas.SearchEventOperator.STARTS_WITH: "ILIKE",
|
||||
schemas.SearchEventOperator.ENDS_WITH: "ILIKE",
|
||||
}.get(op, "=")
|
||||
|
||||
|
||||
def _isAny_opreator(op: schemas.SearchEventOperator):
|
||||
return op in [schemas.SearchEventOperator.ON_ANY, schemas.SearchEventOperator.IS_ANY]
|
||||
|
||||
|
||||
def _isUndefined_operator(op: schemas.SearchEventOperator):
|
||||
return op in [schemas.SearchEventOperator.IS_UNDEFINED]
|
||||
|
||||
|
||||
def __is_negation_operator(op: schemas.SearchEventOperator):
|
||||
return op in [schemas.SearchEventOperator.IS_NOT,
|
||||
schemas.SearchEventOperator.NOT_ON,
|
||||
schemas.SearchEventOperator.NOT_CONTAINS]
|
||||
|
||||
|
||||
def _multiple_conditions(condition, values, value_key="value", is_not=False):
|
||||
query = []
|
||||
for i in range(len(values)):
|
||||
k = f"{value_key}_{i}"
|
||||
query.append(condition.replace(value_key, k))
|
||||
return "(" + (" AND " if is_not else " OR ").join(query) + ")"
|
||||
|
||||
|
||||
def get(error_id, family=False):
|
||||
return errors_legacy.get(error_id=error_id, family=family)
|
||||
|
||||
|
||||
def get_batch(error_ids):
|
||||
return errors_legacy.get_batch(error_ids=error_ids)
|
||||
|
||||
|
||||
def __get_basic_constraints_events(platform=None, time_constraint=True, startTime_arg_name="startDate",
|
||||
endTime_arg_name="endDate", type_condition=True, project_key="project_id",
|
||||
table_name=None):
|
||||
ch_sub_query = [f"{project_key} =toUInt16(%(project_id)s)"]
|
||||
if table_name is not None:
|
||||
table_name = table_name + "."
|
||||
else:
|
||||
table_name = ""
|
||||
if type_condition:
|
||||
ch_sub_query.append(f"{table_name}`$event_name`='ERROR'")
|
||||
if time_constraint:
|
||||
ch_sub_query += [f"{table_name}created_at >= toDateTime(%({startTime_arg_name})s/1000)",
|
||||
f"{table_name}created_at < toDateTime(%({endTime_arg_name})s/1000)"]
|
||||
# if platform == schemas.PlatformType.MOBILE:
|
||||
# ch_sub_query.append("user_device_type = 'mobile'")
|
||||
# elif platform == schemas.PlatformType.DESKTOP:
|
||||
# ch_sub_query.append("user_device_type = 'desktop'")
|
||||
return ch_sub_query
|
||||
|
||||
|
||||
def __get_sort_key(key):
|
||||
return {
|
||||
schemas.ErrorSort.OCCURRENCE: "max_datetime",
|
||||
schemas.ErrorSort.USERS_COUNT: "users",
|
||||
schemas.ErrorSort.SESSIONS_COUNT: "sessions"
|
||||
}.get(key, 'max_datetime')
|
||||
|
||||
|
||||
def search(data: schemas.SearchErrorsSchema, project: schemas.ProjectContext, user_id):
|
||||
MAIN_EVENTS_TABLE = exp_ch_helper.get_main_events_table(data.startTimestamp)
|
||||
MAIN_SESSIONS_TABLE = exp_ch_helper.get_main_sessions_table(data.startTimestamp)
|
||||
|
||||
platform = None
|
||||
for f in data.filters:
|
||||
if f.type == schemas.FilterType.PLATFORM and len(f.value) > 0:
|
||||
platform = f.value[0]
|
||||
ch_sessions_sub_query = errors_helper.__get_basic_constraints_ch(platform, type_condition=False)
|
||||
# ignore platform for errors table
|
||||
ch_sub_query = __get_basic_constraints_events(None, type_condition=True)
|
||||
ch_sub_query.append("JSONExtractString(toString(`$properties`), 'source') = 'js_exception'")
|
||||
|
||||
# To ignore Script error
|
||||
ch_sub_query.append("JSONExtractString(toString(`$properties`), 'message') != 'Script error.'")
|
||||
error_ids = None
|
||||
|
||||
if data.startTimestamp is None:
|
||||
data.startTimestamp = TimeUTC.now(-7)
|
||||
if data.endTimestamp is None:
|
||||
data.endTimestamp = TimeUTC.now(1)
|
||||
|
||||
subquery_part = ""
|
||||
params = {}
|
||||
if len(data.events) > 0:
|
||||
errors_condition_count = 0
|
||||
for i, e in enumerate(data.events):
|
||||
if e.type == schemas.EventType.ERROR:
|
||||
errors_condition_count += 1
|
||||
is_any = _isAny_opreator(e.operator)
|
||||
op = __get_sql_operator(e.operator)
|
||||
e_k = f"e_value{i}"
|
||||
params = {**params, **_multiple_values(e.value, value_key=e_k)}
|
||||
if not is_any and len(e.value) > 0 and e.value[1] not in [None, "*", ""]:
|
||||
ch_sub_query.append(
|
||||
_multiple_conditions(f"(message {op} %({e_k})s OR name {op} %({e_k})s)",
|
||||
e.value, value_key=e_k))
|
||||
if len(data.events) > errors_condition_count:
|
||||
subquery_part_args, subquery_part = sessions.search_query_parts_ch(data=data, error_status=data.status,
|
||||
errors_only=True,
|
||||
project_id=project.project_id,
|
||||
user_id=user_id,
|
||||
issue=None,
|
||||
favorite_only=False)
|
||||
subquery_part = f"INNER JOIN {subquery_part} USING(session_id)"
|
||||
params = {**params, **subquery_part_args}
|
||||
if len(data.filters) > 0:
|
||||
meta_keys = None
|
||||
# to reduce include a sub-query of sessions inside events query, in order to reduce the selected data
|
||||
for i, f in enumerate(data.filters):
|
||||
if not isinstance(f.value, list):
|
||||
f.value = [f.value]
|
||||
filter_type = f.type
|
||||
f.value = helper.values_for_operator(value=f.value, op=f.operator)
|
||||
f_k = f"f_value{i}"
|
||||
params = {**params, f_k: f.value, **_multiple_values(f.value, value_key=f_k)}
|
||||
op = __get_sql_operator(f.operator) \
|
||||
if filter_type not in [schemas.FilterType.EVENTS_COUNT] else f.operator
|
||||
is_any = _isAny_opreator(f.operator)
|
||||
is_undefined = _isUndefined_operator(f.operator)
|
||||
if not is_any and not is_undefined and len(f.value) == 0:
|
||||
continue
|
||||
is_not = False
|
||||
if __is_negation_operator(f.operator):
|
||||
is_not = True
|
||||
if filter_type == schemas.FilterType.USER_BROWSER:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.user_browser)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.user_browser {op} %({f_k})s', f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.USER_OS, schemas.FilterType.USER_OS_MOBILE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.user_os)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.user_os {op} %({f_k})s', f.value, is_not=is_not, value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.USER_DEVICE, schemas.FilterType.USER_DEVICE_MOBILE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.user_device)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.user_device {op} %({f_k})s', f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.USER_COUNTRY, schemas.FilterType.USER_COUNTRY_MOBILE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.user_country)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.user_country {op} %({f_k})s', f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.UTM_SOURCE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.utm_source)')
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append('isNull(s.utm_source)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.utm_source {op} toString(%({f_k})s)', f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.UTM_MEDIUM]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.utm_medium)')
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append('isNull(s.utm_medium)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.utm_medium {op} toString(%({f_k})s)', f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
elif filter_type in [schemas.FilterType.UTM_CAMPAIGN]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.utm_campaign)')
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append('isNull(s.utm_campaign)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f's.utm_campaign {op} toString(%({f_k})s)', f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type == schemas.FilterType.DURATION:
|
||||
if len(f.value) > 0 and f.value[0] is not None:
|
||||
ch_sessions_sub_query.append("s.duration >= %(minDuration)s")
|
||||
params["minDuration"] = f.value[0]
|
||||
if len(f.value) > 1 and f.value[1] is not None and int(f.value[1]) > 0:
|
||||
ch_sessions_sub_query.append("s.duration <= %(maxDuration)s")
|
||||
params["maxDuration"] = f.value[1]
|
||||
|
||||
elif filter_type == schemas.FilterType.REFERRER:
|
||||
# extra_from += f"INNER JOIN {events.EventType.LOCATION.table} AS p USING(session_id)"
|
||||
if is_any:
|
||||
referrer_constraint = 'isNotNull(s.base_referrer)'
|
||||
else:
|
||||
referrer_constraint = _multiple_conditions(f"s.base_referrer {op} %({f_k})s", f.value,
|
||||
is_not=is_not, value_key=f_k)
|
||||
elif filter_type == schemas.FilterType.METADATA:
|
||||
# get metadata list only if you need it
|
||||
if meta_keys is None:
|
||||
meta_keys = metadata.get(project_id=project.project_id)
|
||||
meta_keys = {m["key"]: m["index"] for m in meta_keys}
|
||||
if f.source in meta_keys.keys():
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append(f"isNotNull(s.{metadata.index_to_colname(meta_keys[f.source])})")
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append(f"isNull(s.{metadata.index_to_colname(meta_keys[f.source])})")
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(
|
||||
f"s.{metadata.index_to_colname(meta_keys[f.source])} {op} toString(%({f_k})s)",
|
||||
f.value, is_not=is_not, value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.USER_ID, schemas.FilterType.USER_ID_MOBILE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.user_id)')
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append('isNull(s.user_id)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f"s.user_id {op} toString(%({f_k})s)", f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
elif filter_type in [schemas.FilterType.USER_ANONYMOUS_ID,
|
||||
schemas.FilterType.USER_ANONYMOUS_ID_MOBILE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.user_anonymous_id)')
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append('isNull(s.user_anonymous_id)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f"s.user_anonymous_id {op} toString(%({f_k})s)", f.value,
|
||||
is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type in [schemas.FilterType.REV_ID, schemas.FilterType.REV_ID_MOBILE]:
|
||||
if is_any:
|
||||
ch_sessions_sub_query.append('isNotNull(s.rev_id)')
|
||||
elif is_undefined:
|
||||
ch_sessions_sub_query.append('isNull(s.rev_id)')
|
||||
else:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f"s.rev_id {op} toString(%({f_k})s)", f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
elif filter_type == schemas.FilterType.PLATFORM:
|
||||
# op = __get_sql_operator(f.operator)
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f"s.user_device_type {op} %({f_k})s", f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
# elif filter_type == schemas.FilterType.issue:
|
||||
# if is_any:
|
||||
# ch_sessions_sub_query.append("notEmpty(s.issue_types)")
|
||||
# else:
|
||||
# ch_sessions_sub_query.append(f"hasAny(s.issue_types,%({f_k})s)")
|
||||
# # _multiple_conditions(f"%({f_k})s {op} ANY (s.issue_types)", f.value, is_not=is_not,
|
||||
# # value_key=f_k))
|
||||
#
|
||||
# if is_not:
|
||||
# extra_constraints[-1] = f"not({extra_constraints[-1]})"
|
||||
# ss_constraints[-1] = f"not({ss_constraints[-1]})"
|
||||
elif filter_type == schemas.FilterType.EVENTS_COUNT:
|
||||
ch_sessions_sub_query.append(
|
||||
_multiple_conditions(f"s.events_count {op} %({f_k})s", f.value, is_not=is_not,
|
||||
value_key=f_k))
|
||||
|
||||
with ch_client.ClickHouseClient() as ch:
|
||||
step_size = metrics_helper.get_step_size(data.startTimestamp, data.endTimestamp, data.density)
|
||||
sort = __get_sort_key('datetime')
|
||||
if data.sort is not None:
|
||||
sort = __get_sort_key(data.sort)
|
||||
order = "DESC"
|
||||
if data.order is not None:
|
||||
order = data.order
|
||||
params = {
|
||||
**params,
|
||||
"startDate": data.startTimestamp,
|
||||
"endDate": data.endTimestamp,
|
||||
"project_id": project.project_id,
|
||||
"userId": user_id,
|
||||
"step_size": step_size}
|
||||
if data.limit is not None and data.page is not None:
|
||||
params["errors_offset"] = (data.page - 1) * data.limit
|
||||
params["errors_limit"] = data.limit
|
||||
else:
|
||||
params["errors_offset"] = 0
|
||||
params["errors_limit"] = 200
|
||||
# if data.bookmarked:
|
||||
# cur.execute(cur.mogrify(f"""SELECT error_id
|
||||
# FROM public.user_favorite_errors
|
||||
# WHERE user_id = %(userId)s
|
||||
# {"" if error_ids is None else "AND error_id IN %(error_ids)s"}""",
|
||||
# {"userId": user_id, "error_ids": tuple(error_ids or [])}))
|
||||
# error_ids = cur.fetchall()
|
||||
# if len(error_ids) == 0:
|
||||
# return empty_response
|
||||
# error_ids = [e["error_id"] for e in error_ids]
|
||||
|
||||
if error_ids is not None:
|
||||
params["error_ids"] = tuple(error_ids)
|
||||
ch_sub_query.append("error_id IN %(error_ids)s")
|
||||
|
||||
main_ch_query = f"""\
|
||||
SELECT details.error_id as error_id,
|
||||
name, message, users, total,
|
||||
sessions, last_occurrence, first_occurrence, chart
|
||||
FROM (SELECT error_id,
|
||||
JSONExtractString(toString(`$properties`), 'name') AS name,
|
||||
JSONExtractString(toString(`$properties`), 'message') AS message,
|
||||
COUNT(DISTINCT user_id) AS users,
|
||||
COUNT(DISTINCT events.session_id) AS sessions,
|
||||
MAX(created_at) AS max_datetime,
|
||||
MIN(created_at) AS min_datetime,
|
||||
COUNT(DISTINCT error_id)
|
||||
OVER() AS total
|
||||
FROM {MAIN_EVENTS_TABLE} AS events
|
||||
INNER JOIN (SELECT session_id, coalesce(user_id,toString(user_uuid)) AS user_id
|
||||
FROM {MAIN_SESSIONS_TABLE} AS s
|
||||
{subquery_part}
|
||||
WHERE {" AND ".join(ch_sessions_sub_query)}) AS sessions
|
||||
ON (events.session_id = sessions.session_id)
|
||||
WHERE {" AND ".join(ch_sub_query)}
|
||||
GROUP BY error_id, name, message
|
||||
ORDER BY {sort} {order}
|
||||
LIMIT %(errors_limit)s OFFSET %(errors_offset)s) AS details
|
||||
INNER JOIN (SELECT error_id,
|
||||
toUnixTimestamp(MAX(created_at))*1000 AS last_occurrence,
|
||||
toUnixTimestamp(MIN(created_at))*1000 AS first_occurrence
|
||||
FROM {MAIN_EVENTS_TABLE}
|
||||
WHERE project_id=%(project_id)s
|
||||
AND `$event_name`='ERROR'
|
||||
GROUP BY error_id) AS time_details
|
||||
ON details.error_id=time_details.error_id
|
||||
INNER JOIN (SELECT error_id, groupArray([timestamp, count]) AS chart
|
||||
FROM (SELECT error_id,
|
||||
gs.generate_series AS timestamp,
|
||||
COUNT(DISTINCT session_id) AS count
|
||||
FROM generate_series(%(startDate)s, %(endDate)s, %(step_size)s) AS gs
|
||||
LEFT JOIN {MAIN_EVENTS_TABLE} ON(TRUE)
|
||||
WHERE {" AND ".join(ch_sub_query)}
|
||||
AND created_at >= toDateTime(timestamp / 1000)
|
||||
AND created_at < toDateTime((timestamp + %(step_size)s) / 1000)
|
||||
GROUP BY error_id, timestamp
|
||||
ORDER BY timestamp) AS sub_table
|
||||
GROUP BY error_id) AS chart_details ON details.error_id=chart_details.error_id;"""
|
||||
|
||||
# print("------------")
|
||||
# print(ch.format(main_ch_query, params))
|
||||
# print("------------")
|
||||
query = ch.format(query=main_ch_query, parameters=params)
|
||||
|
||||
rows = ch.execute(query=query)
|
||||
total = rows[0]["total"] if len(rows) > 0 else 0
|
||||
|
||||
for r in rows:
|
||||
r["chart"] = list(r["chart"])
|
||||
for i in range(len(r["chart"])):
|
||||
r["chart"][i] = {"timestamp": r["chart"][i][0], "count": r["chart"][i][1]}
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'errors': helper.list_to_camel_case(rows)
|
||||
}
|
||||
|
||||
|
||||
def get_trace(project_id, error_id):
|
||||
return errors_legacy.get_trace(project_id=project_id, error_id=error_id)
|
||||
|
||||
|
||||
def get_sessions(start_date, end_date, project_id, user_id, error_id):
|
||||
return errors_legacy.get_sessions(start_date=start_date,
|
||||
end_date=end_date,
|
||||
project_id=project_id,
|
||||
user_id=user_id,
|
||||
error_id=error_id)
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
from chalicelib.core.errors.modules import errors_helper
|
||||
|
||||
from chalicelib.utils import pg_client, helper
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.utils.metrics_helper import get_step_size
|
||||
|
||||
|
||||
def __flatten_sort_key_count_version(data, merge_nested=False):
|
||||
if data is None:
|
||||
return []
|
||||
return sorted(
|
||||
[
|
||||
{
|
||||
"name": f'{o["name"]}@{v["version"]}',
|
||||
"count": v["count"]
|
||||
} for o in data for v in o["partition"]
|
||||
],
|
||||
key=lambda o: o["count"], reverse=True) if merge_nested else \
|
||||
[
|
||||
{
|
||||
"name": o["name"],
|
||||
"count": o["count"],
|
||||
} for o in data
|
||||
]
|
||||
|
||||
|
||||
def __process_tags(row):
|
||||
return [
|
||||
{"name": "browser", "partitions": __flatten_sort_key_count_version(data=row.get("browsers_partition"))},
|
||||
{"name": "browser.ver",
|
||||
"partitions": __flatten_sort_key_count_version(data=row.pop("browsers_partition"), merge_nested=True)},
|
||||
{"name": "OS", "partitions": __flatten_sort_key_count_version(data=row.get("os_partition"))},
|
||||
{"name": "OS.ver",
|
||||
"partitions": __flatten_sort_key_count_version(data=row.pop("os_partition"), merge_nested=True)},
|
||||
{"name": "device.family", "partitions": __flatten_sort_key_count_version(data=row.get("device_partition"))},
|
||||
{"name": "device",
|
||||
"partitions": __flatten_sort_key_count_version(data=row.pop("device_partition"), merge_nested=True)},
|
||||
{"name": "country", "partitions": row.pop("country_partition")}
|
||||
]
|
||||
|
||||
|
||||
def get_details(project_id, error_id, user_id, **data):
|
||||
pg_sub_query24 = errors_helper.__get_basic_constraints(time_constraint=False, chart=True,
|
||||
step_size_name="step_size24")
|
||||
pg_sub_query24.append("error_id = %(error_id)s")
|
||||
pg_sub_query30_session = errors_helper.__get_basic_constraints(time_constraint=True, chart=False,
|
||||
startTime_arg_name="startDate30",
|
||||
endTime_arg_name="endDate30",
|
||||
project_key="sessions.project_id")
|
||||
pg_sub_query30_session.append("sessions.start_ts >= %(startDate30)s")
|
||||
pg_sub_query30_session.append("sessions.start_ts <= %(endDate30)s")
|
||||
pg_sub_query30_session.append("error_id = %(error_id)s")
|
||||
pg_sub_query30_err = errors_helper.__get_basic_constraints(time_constraint=True, chart=False,
|
||||
startTime_arg_name="startDate30",
|
||||
endTime_arg_name="endDate30",
|
||||
project_key="errors.project_id")
|
||||
pg_sub_query30_err.append("sessions.project_id = %(project_id)s")
|
||||
pg_sub_query30_err.append("sessions.start_ts >= %(startDate30)s")
|
||||
pg_sub_query30_err.append("sessions.start_ts <= %(endDate30)s")
|
||||
pg_sub_query30_err.append("error_id = %(error_id)s")
|
||||
pg_sub_query30_err.append("source ='js_exception'")
|
||||
pg_sub_query30 = errors_helper.__get_basic_constraints(time_constraint=False, chart=True,
|
||||
step_size_name="step_size30")
|
||||
pg_sub_query30.append("error_id = %(error_id)s")
|
||||
pg_basic_query = errors_helper.__get_basic_constraints(time_constraint=False)
|
||||
pg_basic_query.append("error_id = %(error_id)s")
|
||||
with pg_client.PostgresClient() as cur:
|
||||
data["startDate24"] = TimeUTC.now(-1)
|
||||
data["endDate24"] = TimeUTC.now()
|
||||
data["startDate30"] = TimeUTC.now(-30)
|
||||
data["endDate30"] = TimeUTC.now()
|
||||
density24 = int(data.get("density24", 24))
|
||||
step_size24 = get_step_size(data["startDate24"], data["endDate24"], density24, factor=1)
|
||||
density30 = int(data.get("density30", 30))
|
||||
step_size30 = get_step_size(data["startDate30"], data["endDate30"], density30, factor=1)
|
||||
params = {
|
||||
"startDate24": data['startDate24'],
|
||||
"endDate24": data['endDate24'],
|
||||
"startDate30": data['startDate30'],
|
||||
"endDate30": data['endDate30'],
|
||||
"project_id": project_id,
|
||||
"userId": user_id,
|
||||
"step_size24": step_size24,
|
||||
"step_size30": step_size30,
|
||||
"error_id": error_id}
|
||||
|
||||
main_pg_query = f"""\
|
||||
SELECT error_id,
|
||||
name,
|
||||
message,
|
||||
users,
|
||||
sessions,
|
||||
last_occurrence,
|
||||
first_occurrence,
|
||||
last_session_id,
|
||||
browsers_partition,
|
||||
os_partition,
|
||||
device_partition,
|
||||
country_partition,
|
||||
chart24,
|
||||
chart30
|
||||
FROM (SELECT error_id,
|
||||
name,
|
||||
message,
|
||||
COUNT(DISTINCT user_id) AS users,
|
||||
COUNT(DISTINCT session_id) AS sessions
|
||||
FROM public.errors
|
||||
INNER JOIN events.errors AS s_errors USING (error_id)
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_err)}
|
||||
GROUP BY error_id, name, message) AS details
|
||||
INNER JOIN (SELECT MAX(timestamp) AS last_occurrence,
|
||||
MIN(timestamp) AS first_occurrence
|
||||
FROM events.errors
|
||||
WHERE error_id = %(error_id)s) AS time_details ON (TRUE)
|
||||
INNER JOIN (SELECT session_id AS last_session_id
|
||||
FROM events.errors
|
||||
WHERE error_id = %(error_id)s
|
||||
ORDER BY errors.timestamp DESC
|
||||
LIMIT 1) AS last_session_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(browser_details) AS browsers_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_browser AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors
|
||||
INNER JOIN sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
GROUP BY user_browser
|
||||
ORDER BY count DESC) AS count_per_browser_query
|
||||
INNER JOIN LATERAL (SELECT JSONB_AGG(version_details) AS partition
|
||||
FROM (SELECT user_browser_version AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
AND sessions.user_browser = count_per_browser_query.name
|
||||
GROUP BY user_browser_version
|
||||
ORDER BY count DESC) AS version_details
|
||||
) AS browser_version_details ON (TRUE)) AS browser_details) AS browser_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(os_details) AS os_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_os AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
GROUP BY user_os
|
||||
ORDER BY count DESC) AS count_per_os_details
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_version_details) AS partition
|
||||
FROM (SELECT COALESCE(user_os_version,'unknown') AS version, COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
AND sessions.user_os = count_per_os_details.name
|
||||
GROUP BY user_os_version
|
||||
ORDER BY count DESC) AS count_per_version_details
|
||||
GROUP BY count_per_os_details.name ) AS os_version_details
|
||||
ON (TRUE)) AS os_details) AS os_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(device_details) AS device_partition
|
||||
FROM (SELECT *
|
||||
FROM (SELECT user_device_type AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
GROUP BY user_device_type
|
||||
ORDER BY count DESC) AS count_per_device_details
|
||||
INNER JOIN LATERAL (SELECT jsonb_agg(count_per_device_v_details) AS partition
|
||||
FROM (SELECT CASE
|
||||
WHEN user_device = '' OR user_device ISNULL
|
||||
THEN 'unknown'
|
||||
ELSE user_device END AS version,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
AND sessions.user_device_type = count_per_device_details.name
|
||||
GROUP BY user_device
|
||||
ORDER BY count DESC) AS count_per_device_v_details
|
||||
GROUP BY count_per_device_details.name ) AS device_version_details
|
||||
ON (TRUE)) AS device_details) AS device_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(count_per_country_details) AS country_partition
|
||||
FROM (SELECT user_country AS name,
|
||||
COUNT(session_id) AS count
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30_session)}
|
||||
GROUP BY user_country
|
||||
ORDER BY count DESC) AS count_per_country_details) AS country_details ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(chart_details) AS chart24
|
||||
FROM (SELECT generated_timestamp AS timestamp,
|
||||
COUNT(session_id) AS count
|
||||
FROM generate_series(%(startDate24)s, %(endDate24)s, %(step_size24)s) AS generated_timestamp
|
||||
LEFT JOIN LATERAL (SELECT DISTINCT session_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query24)}
|
||||
) AS chart_details ON (TRUE)
|
||||
GROUP BY generated_timestamp
|
||||
ORDER BY generated_timestamp) AS chart_details) AS chart_details24 ON (TRUE)
|
||||
INNER JOIN (SELECT jsonb_agg(chart_details) AS chart30
|
||||
FROM (SELECT generated_timestamp AS timestamp,
|
||||
COUNT(session_id) AS count
|
||||
FROM generate_series(%(startDate30)s, %(endDate30)s, %(step_size30)s) AS generated_timestamp
|
||||
LEFT JOIN LATERAL (SELECT DISTINCT session_id
|
||||
FROM events.errors INNER JOIN public.sessions USING (session_id)
|
||||
WHERE {" AND ".join(pg_sub_query30)}) AS chart_details
|
||||
ON (TRUE)
|
||||
GROUP BY timestamp
|
||||
ORDER BY timestamp) AS chart_details) AS chart_details30 ON (TRUE);
|
||||
"""
|
||||
|
||||
# print("--------------------")
|
||||
# print(cur.mogrify(main_pg_query, params))
|
||||
# print("--------------------")
|
||||
cur.execute(cur.mogrify(main_pg_query, params))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return {"errors": ["error not found"]}
|
||||
row["tags"] = __process_tags(row)
|
||||
|
||||
query = cur.mogrify(
|
||||
f"""SELECT error_id, status, session_id, start_ts,
|
||||
parent_error_id,session_id, user_anonymous_id,
|
||||
user_id, user_uuid, user_browser, user_browser_version,
|
||||
user_os, user_os_version, user_device, payload,
|
||||
FALSE AS favorite,
|
||||
True AS viewed
|
||||
FROM public.errors AS pe
|
||||
INNER JOIN events.errors AS ee USING (error_id)
|
||||
INNER JOIN public.sessions USING (session_id)
|
||||
WHERE pe.project_id = %(project_id)s
|
||||
AND error_id = %(error_id)s
|
||||
ORDER BY start_ts DESC
|
||||
LIMIT 1;""",
|
||||
{"project_id": project_id, "error_id": error_id, "user_id": user_id})
|
||||
cur.execute(query=query)
|
||||
status = cur.fetchone()
|
||||
|
||||
if status is not None:
|
||||
row["stack"] = errors_helper.format_first_stack_frame(status).pop("stack")
|
||||
row["status"] = status.pop("status")
|
||||
row["parent_error_id"] = status.pop("parent_error_id")
|
||||
row["favorite"] = status.pop("favorite")
|
||||
row["viewed"] = status.pop("viewed")
|
||||
row["last_hydrated_session"] = status
|
||||
else:
|
||||
row["stack"] = []
|
||||
row["last_hydrated_session"] = None
|
||||
row["status"] = "untracked"
|
||||
row["parent_error_id"] = None
|
||||
row["favorite"] = False
|
||||
row["viewed"] = False
|
||||
return {"data": helper.dict_to_camel_case(row)}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue