From 2e86b6eb6afddcae2cbb8a1e23dbd42deef83a0d Mon Sep 17 00:00:00 2001 From: Rajesh Rajendran Date: Fri, 21 May 2021 17:23:36 +0000 Subject: [PATCH 1/9] Bug fixes and features. (#7) * fix: changed sessions bucket * fix: text changes in login and signup forms * change: version number * change: config changes * fix: alerts image name * fix: alerts image name * Update README.md * chore(actions): pushing internalized to script. Signed-off-by: Rajesh Rajendran * feat(nginx): No redirection to HTTPS by default. Signed-off-by: Rajesh Rajendran * chore(deploy): optional nginx https redirect Signed-off-by: Rajesh Rajendran * fix: review fixes and other changes * fix: events modal openreplay logo * fix: stack event icon * Changes: - debugging - smtp status - session's issues - session's issue_types as array - changed Slack error message * Changes: - set chalice pull policy to always * fix(openreplay-cli): path issues. Signed-off-by: Rajesh Rajendran * fix(openreplay-cli): fix path Signed-off-by: Rajesh Rajendran * change: onboarding explore text changes * change: timeline issue pointers and static issue types * change: removed issues_types api call * connectors * Update README.md * Update README.md * Update README.md * Updating services * Update README.md * Updated alert-notification-string to chalice * Delete issues.md * Changes: - fixed connexion pool exhausted using Semaphores - fixed session-replay-url signing * Changes: - fixed connexion pool exhausted using Semaphores - fixed session-replay-url signing * Change pullPolicy to IfNotPresent * Fixed typo * Fixed typo * Fixed typos * Fixed typo * Fixed typo * Fixed typos * Fixed typos * Fixed typo * Fixed typo * Removed /ws * Update README.md * feat(nginx): increase minio upload size to 50M Signed-off-by: Rajesh Rajendran * fix(deploy): nginx custom changes are overriden in install Signed-off-by: Rajesh Rajendran * fix(nginx): deployment indentation issue Signed-off-by: Rajesh Rajendran * fix: revid filter crash * fix: onboarding links * fix: update password store new token * fix: report issue icon jira/github * fix: onboarding redirect on signup * Changes: - hardcoded S3_HOST * Changes: - changed "sourcemaps" env var to "sourcemaps_reader" - set "sourcemaps_reader" env var value * chore(script): remove logo Signed-off-by: Rajesh Rajendran * Making domain_name mandatory Signed-off-by: Rajesh Rajendran * Changes: - un-ignore *.js * feat(install): auto create jwt_secret for chalice. * docs(script): Adding Banner Signed-off-by: Rajesh Rajendran * chore(script): Remove verbose logging Signed-off-by: Rajesh Rajendran * Change: - use boto3-resource instead of boto3-client to check if file exists - changed .gitignore to allow *.js files - changed sourcemaps_reader env-var & env-var-value * fix (baxkend-ender): skip inputs with no label (technical) * Change: - changed DB structure * change: removed /flows api call * fix: skipping errorOnFetch check * Change: - changed sourcemaps_reader-nodejs script * Change: - changed sourcemaps_reader-nodejs script * fix (backend-postgres): correct autocomplete type-value * fix: slack webhooks PUT call * change: added external icon for integration doc links * fix: updated the sourcemap upload doc link * fix: link color of no sessions message * fix (frontend-player): show original domContentLoaded text values, while adjusted on timeline * fix (frontend-player): syntax * Changes: - changed requirements - changed slack add integration - added slack edit integration - removed sourcemaps_reader extra payload * Changes: - fixed sentry-issue-reporter - fixed telemetry reporter - fixed DB schema * fix(cli): fix logs flag Signed-off-by: Rajesh Rajendran * ci(deploy): Injecting domain_name Signed-off-by: Rajesh Rajendran * feat(nginx): Get real client ip Signed-off-by: Rajesh Rajendran * chore(nginx): restart on helm installation. Signed-off-by: Rajesh Rajendran * fix(deployment): respect image tags. * Changes: - changed sentry tags - changed asayer_session_id to openReplaySessionToken - EE full merge * fix: close the issue modal after creating * fix: show description in issue details modal * fix: integrate slack button redirect, and doc link * fix: code snippet conflict set back * fix: slack share channel selection * Changes: - fixed DB structure * Changes: - return full integration body on add slack * fix (integrations): ignore token expired + some logs * feat (sourcemaps-uploader): v.3.0.2 filename fix + logging arg * fix (tracker): 3.0.3 version: start before auth * fix: funnel calendar position * fix: fetch issue types * fix: missing icon blocking the session to play * change: sessions per browser widget bar height reduced * fix: github colored circles * Changes: - changed session-assignment-jira response * chore(nginx): pass x-forward-for Signed-off-by: Rajesh Rajendran * feat(chalice): included sourcemaps_reader It's not advised to run multiple processes in a single docker container. In Kubernetes we can run this as sidecar, but other platforms such as Heroku, and vanilla docker doesn't support such feature. So till we figure out better solution, this is the workaround. * chore(install): Remove sqs * feat(deployment): restart pods on installations. Signed-off-by: Rajesh Rajendran * Changes: - changed DB-oauth-unique constraint Co-authored-by: Shekar Siri Co-authored-by: Mehdi Osman Co-authored-by: KRAIEM Taha Yassine Co-authored-by: ourvakan Co-authored-by: ShiKhu --- .github/workflows/api.yaml | 6 +- api/.chalice/config.json | 10 +- api/.gitignore | 2 +- api/Dockerfile | 10 +- api/app.py | 36 +- api/chalicelib/blueprints/bp_core.py | 2 +- api/chalicelib/blueprints/bp_core_dynamic.py | 31 +- api/chalicelib/core/collaboration_slack.py | 17 +- api/chalicelib/core/dashboard.py | 4 - api/chalicelib/core/events.py | 2 +- .../core/integration_jira_cloud_issue.py | 2 +- api/chalicelib/core/sessions.py | 9 +- api/chalicelib/core/sessions_assignments.py | 1 - api/chalicelib/core/sessions_mobs.py | 10 +- api/chalicelib/core/sourcemaps.py | 5 + api/chalicelib/core/sourcemaps_parser.py | 9 +- api/chalicelib/core/telemetry.py | 2 +- api/chalicelib/core/tenants.py | 2 +- api/chalicelib/core/webhook.py | 10 +- api/chalicelib/utils/jira_client.py | 3 +- api/chalicelib/utils/pg_client.py | 25 +- api/chalicelib/utils/s3.py | 27 +- api/entrypoint.sh | 6 + api/requirements.txt | 3 - api/sourcemaps_reader/handler.js | 111 +++ api/sourcemaps_reader/server.js | 38 + backend/pkg/db/postgres/messages_web.go | 8 +- backend/services/db/messages.go | 1 + backend/services/ender/builder/builder.go | 25 +- .../ender/builder/inputEventBuilder.go | 8 +- .../integrations/integration/sentry.go | 2 +- backend/services/integrations/main.go | 14 +- ee/api/.chalice/config.json | 10 +- ee/api/.gitignore | 4 +- ee/api/app.py | 37 +- ee/api/chalicelib/blueprints/bp_core.py | 2 +- .../chalicelib/blueprints/bp_core_dynamic.py | 28 +- ee/api/chalicelib/core/collaboration_slack.py | 17 +- ee/api/chalicelib/core/events.py | 2 +- .../core/integration_jira_cloud_issue.py | 2 +- ee/api/chalicelib/core/sessions.py | 9 +- .../chalicelib/core/sessions_assignments.py | 1 - ee/api/chalicelib/core/sessions_mobs.py | 6 +- ee/api/chalicelib/core/sourcemaps.py | 5 + ee/api/chalicelib/core/sourcemaps_parser.py | 9 +- ee/api/chalicelib/ee/webhook.py | 12 +- ee/api/chalicelib/utils/jira_client.py | 3 +- ee/api/chalicelib/utils/pg_client.py | 26 +- ee/api/chalicelib/utils/s3.py | 59 +- ee/api/requirements.txt | 3 - ee/api/sourcemaps_reader/handler.js | 111 +++ ee/api/sourcemaps_reader/server.js | 38 + ee/connectors/bigquery_utils/create_table.py | 357 +++++++++ ee/connectors/db/api.py | 129 +++ ee/connectors/db/loaders/__init__.py | 0 ee/connectors/db/loaders/bigquery_loader.py | 34 + ee/connectors/db/loaders/clickhouse_loader.py | 4 + ee/connectors/db/loaders/postgres_loader.py | 3 + ee/connectors/db/loaders/redshift_loader.py | 19 + ee/connectors/db/loaders/snowflake_loader.py | 5 + ee/connectors/db/models.py | 389 +++++++++ ee/connectors/db/tables.py | 61 ++ ee/connectors/db/utils.py | 368 +++++++++ ee/connectors/db/writer.py | 63 ++ ee/connectors/handler.py | 647 +++++++++++++++ ee/connectors/main.py | 121 +++ ee/connectors/msgcodec/codec.py | 670 ++++++++++++++++ ee/connectors/msgcodec/messages.py | 752 ++++++++++++++++++ ee/connectors/requirements.txt | 43 + ee/connectors/sql/clickhouse_events.sql | 56 ++ .../sql/clickhouse_events_buffer.sql | 52 ++ ee/connectors/sql/clickhouse_sessions.sql | 52 ++ .../sql/clickhouse_sessions_buffer.sql | 50 ++ ee/connectors/sql/postgres_events.sql | 52 ++ ee/connectors/sql/postgres_sessions.sql | 50 ++ ee/connectors/sql/redshift_events.sql | 52 ++ ee/connectors/sql/redshift_sessions.sql | 50 ++ ee/connectors/sql/snowflake_events.sql | 52 ++ ee/connectors/sql/snowflake_sessions.sql | 50 ++ ee/connectors/utils/bigquery.env.example | 7 + .../bigquery_service_account.json.example | 12 + ee/connectors/utils/clickhouse.env.example | 7 + ee/connectors/utils/pg.env.example | 10 + ee/connectors/utils/redshift.env.example | 15 + ee/connectors/utils/snowflake.env.example | 11 + .../db/init_dbs/postgresql/init_schema.sql | 211 +++-- frontend/app/Router.js | 8 +- frontend/app/assets/apple-touch-icon.png | Bin 0 -> 6253 bytes frontend/app/assets/favicon-16x16.png | Bin 0 -> 799 bytes frontend/app/assets/favicon-32x32.png | Bin 0 -> 1090 bytes frontend/app/assets/favicon.ico | Bin 0 -> 15086 bytes frontend/app/assets/favicon@1x.png | Bin 2127 -> 0 bytes frontend/app/assets/favicon@2x.png | Bin 5829 -> 0 bytes frontend/app/assets/favicon@3x.png | Bin 10941 -> 0 bytes frontend/app/assets/favicon@4x.png | Bin 16394 -> 0 bytes frontend/app/assets/favicon@5x.png | Bin 24304 -> 0 bytes frontend/app/assets/favicon@6x.png | Bin 32078 -> 0 bytes frontend/app/assets/index.html | 9 +- .../BugFinder/CustomFilters/FilterItem.js | 2 +- .../app/components/BugFinder/DateRange.js | 5 +- .../BugFinder/SessionsMenu/SessionsMenu.js | 11 +- .../Integrations/SlackAddForm/SlackAddForm.js | 14 +- .../SlackChannelList/SlackChannelList.js | 27 +- .../Client/ManageUsers/ManageUsers.js | 54 +- .../Client/PreferencesMenu/PreferencesMenu.js | 2 +- .../Client/ProfileSettings/OptOut.js | 2 +- .../Widgets/SessionsPerBrowser/Bar.css | 2 +- .../app/components/Errors/Error/ErrorInfo.js | 2 +- .../Funnels/FunnelDetails/FunnelDetails.js | 4 +- .../Funnels/FunnelHeader/FunnelHeader.js | 1 + .../Header/Discover/featureItem.css | 2 +- .../Header/OnboardingExplore/FeatureItem.js | 2 +- .../OnboardingExplore/OnboardingExplore.js | 4 +- .../Header/OnboardingExplore/featureItem.css | 2 +- frontend/app/components/Login/Login.js | 2 +- .../OnboardingNavButton.js | 12 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 7 +- .../Session_/Issues/IssueDetails.js | 6 +- .../components/Session_/Issues/IssueForm.js | 11 +- .../components/Session_/Issues/IssueHeader.js | 3 +- .../Session_/Issues/IssueListItem.js | 3 +- .../app/components/Session_/Issues/Issues.js | 5 +- .../components/Session_/Network/Network.js | 48 +- .../Session_/Network/NetworkContent.js | 8 +- .../Session_/Player/Controls/Timeline.js | 36 +- .../StackEvents/UserEvent/UserEvent.js | 2 +- .../Signup/SignupForm/SignupForm.js | 2 +- .../shared/BannerMessage/BannerMessage.js | 28 + .../components/shared/BannerMessage/index.js | 1 + frontend/app/components/shared/DateRange.js | 3 +- .../DateRangeDropdown/DateRangeDropdown.js | 4 +- .../DateRangeDropdown/dateRangeDropdown.css | 4 + .../app/components/shared/DocLink/DocLink.js | 7 +- .../IntegrateSlackButton.js | 26 + .../shared/IntegrateSlackButton/index.js | 1 + .../NoSessionsMessage/NoSessionsMessage.js | 2 +- .../shared/SharePopup/SharePopup.js | 14 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 7 +- .../ui/ErrorDetails/ErrorDetails.js | 2 +- frontend/app/duck/assignments.js | 18 +- frontend/app/duck/integrations/slack.js | 9 + frontend/app/duck/user.js | 22 +- .../MessageDistributor/MessageDistributor.js | 33 +- frontend/app/svg/icons/funnel/cpu.svg | 3 + frontend/app/svg/icons/funnel/dizzy.svg | 1 + frontend/app/svg/icons/funnel/emoji-angry.svg | 4 + .../svg/icons/funnel/file-earmark-break.svg | 3 + frontend/app/svg/icons/funnel/image.svg | 4 + frontend/app/svg/icons/funnel/sd-card.svg | 5 +- frontend/app/types/account/account.js | 1 + .../app/types/integrations/issueTracker.js | 1 - frontend/app/types/issue/issuesType.js | 9 + frontend/app/types/session/issue.js | 43 + frontend/app/types/session/session.js | 9 +- frontend/app/types/watchdog.js | 12 +- frontend/env.js | 2 +- scripts/helm/README.md | 38 +- scripts/helm/app/README.md | 11 +- scripts/helm/app/alerts.yaml | 4 +- scripts/helm/app/assets.yaml | 4 +- scripts/helm/app/chalice.yaml | 5 +- scripts/helm/app/http.yaml | 4 +- scripts/helm/app/issues.md | 76 -- .../app/openreplay/templates/deployment.yaml | 3 +- scripts/helm/app/openreplay/values.yaml | 2 +- scripts/helm/app/storage.yaml | 8 +- .../db/init_dbs/postgresql/init_schema.sql | 544 +++++++------ scripts/helm/db/sqs/.helmignore | 23 - scripts/helm/db/sqs/Chart.yaml | 23 - scripts/helm/db/sqs/templates/NOTES.txt | 22 - scripts/helm/db/sqs/templates/_helpers.tpl | 62 -- scripts/helm/db/sqs/templates/configmap.yaml | 28 - scripts/helm/db/sqs/templates/deployment.yaml | 64 -- scripts/helm/db/sqs/templates/hpa.yaml | 28 - scripts/helm/db/sqs/templates/ingress.yaml | 41 - scripts/helm/db/sqs/templates/service.yaml | 19 - .../helm/db/sqs/templates/serviceaccount.yaml | 12 - scripts/helm/db/sqs/values.yaml | 111 --- scripts/helm/install.sh | 16 + scripts/helm/kube-install.sh | 45 +- .../nginx-ingress/nginx-ingress/README.md | 18 +- .../nginx-ingress/templates/configmap.yaml | 16 +- .../nginx-ingress/templates/deployment.yaml | 3 +- .../nginx-ingress/templates/service.yaml | 2 + scripts/helm/openreplay-cli | 22 +- .../helm/roles/openreplay/defaults/main.yml | 1 - .../roles/openreplay/tasks/install-apps.yaml | 2 +- scripts/helm/roles/openreplay/tasks/main.yml | 8 +- .../roles/openreplay/tasks/pre-check.yaml | 36 +- .../roles/openreplay/templates/alert.yaml | 4 +- .../roles/openreplay/templates/assets.yaml | 2 +- .../roles/openreplay/templates/chalice.yaml | 3 +- .../helm/roles/openreplay/templates/db.yaml | 4 +- .../roles/openreplay/templates/ender.yaml | 2 +- .../helm/roles/openreplay/templates/http.yaml | 2 +- .../openreplay/templates/integrations.yaml | 2 +- .../helm/roles/openreplay/templates/sink.yaml | 2 +- .../roles/openreplay/templates/storage.yaml | 2 +- scripts/helm/vars.yaml | 25 +- sourcemap-uploader/cli.js | 9 +- sourcemap-uploader/lib/readDir.js | 4 +- sourcemap-uploader/lib/uploadSourcemaps.js | 32 +- sourcemap-uploader/package.json | 2 +- tracker/tracker/package.json | 2 +- tracker/tracker/src/main/app/index.ts | 41 +- tracker/tracker/src/main/index.ts | 4 +- tracker/tracker/src/webworker/index.ts | 40 +- 207 files changed, 5909 insertions(+), 1429 deletions(-) create mode 100755 api/entrypoint.sh create mode 100644 api/sourcemaps_reader/handler.js create mode 100644 api/sourcemaps_reader/server.js create mode 100644 ee/api/sourcemaps_reader/handler.js create mode 100644 ee/api/sourcemaps_reader/server.js create mode 100644 ee/connectors/bigquery_utils/create_table.py create mode 100644 ee/connectors/db/api.py create mode 100644 ee/connectors/db/loaders/__init__.py create mode 100644 ee/connectors/db/loaders/bigquery_loader.py create mode 100644 ee/connectors/db/loaders/clickhouse_loader.py create mode 100644 ee/connectors/db/loaders/postgres_loader.py create mode 100644 ee/connectors/db/loaders/redshift_loader.py create mode 100644 ee/connectors/db/loaders/snowflake_loader.py create mode 100644 ee/connectors/db/models.py create mode 100644 ee/connectors/db/tables.py create mode 100644 ee/connectors/db/utils.py create mode 100644 ee/connectors/db/writer.py create mode 100644 ee/connectors/handler.py create mode 100644 ee/connectors/main.py create mode 100644 ee/connectors/msgcodec/codec.py create mode 100644 ee/connectors/msgcodec/messages.py create mode 100644 ee/connectors/requirements.txt create mode 100644 ee/connectors/sql/clickhouse_events.sql create mode 100644 ee/connectors/sql/clickhouse_events_buffer.sql create mode 100644 ee/connectors/sql/clickhouse_sessions.sql create mode 100644 ee/connectors/sql/clickhouse_sessions_buffer.sql create mode 100644 ee/connectors/sql/postgres_events.sql create mode 100644 ee/connectors/sql/postgres_sessions.sql create mode 100644 ee/connectors/sql/redshift_events.sql create mode 100644 ee/connectors/sql/redshift_sessions.sql create mode 100644 ee/connectors/sql/snowflake_events.sql create mode 100644 ee/connectors/sql/snowflake_sessions.sql create mode 100644 ee/connectors/utils/bigquery.env.example create mode 100644 ee/connectors/utils/bigquery_service_account.json.example create mode 100644 ee/connectors/utils/clickhouse.env.example create mode 100644 ee/connectors/utils/pg.env.example create mode 100644 ee/connectors/utils/redshift.env.example create mode 100644 ee/connectors/utils/snowflake.env.example create mode 100644 frontend/app/assets/apple-touch-icon.png create mode 100644 frontend/app/assets/favicon-16x16.png create mode 100644 frontend/app/assets/favicon-32x32.png create mode 100644 frontend/app/assets/favicon.ico delete mode 100644 frontend/app/assets/favicon@1x.png delete mode 100644 frontend/app/assets/favicon@2x.png delete mode 100644 frontend/app/assets/favicon@3x.png delete mode 100644 frontend/app/assets/favicon@4x.png delete mode 100644 frontend/app/assets/favicon@5x.png delete mode 100644 frontend/app/assets/favicon@6x.png create mode 100644 frontend/app/components/shared/BannerMessage/BannerMessage.js create mode 100644 frontend/app/components/shared/BannerMessage/index.js create mode 100644 frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js create mode 100644 frontend/app/components/shared/IntegrateSlackButton/index.js create mode 100644 frontend/app/svg/icons/funnel/cpu.svg create mode 100644 frontend/app/svg/icons/funnel/dizzy.svg create mode 100644 frontend/app/svg/icons/funnel/emoji-angry.svg create mode 100644 frontend/app/svg/icons/funnel/file-earmark-break.svg create mode 100644 frontend/app/svg/icons/funnel/image.svg create mode 100644 frontend/app/types/session/issue.js delete mode 100644 scripts/helm/app/issues.md delete mode 100644 scripts/helm/db/sqs/.helmignore delete mode 100644 scripts/helm/db/sqs/Chart.yaml delete mode 100644 scripts/helm/db/sqs/templates/NOTES.txt delete mode 100644 scripts/helm/db/sqs/templates/_helpers.tpl delete mode 100644 scripts/helm/db/sqs/templates/configmap.yaml delete mode 100644 scripts/helm/db/sqs/templates/deployment.yaml delete mode 100644 scripts/helm/db/sqs/templates/hpa.yaml delete mode 100644 scripts/helm/db/sqs/templates/ingress.yaml delete mode 100644 scripts/helm/db/sqs/templates/service.yaml delete mode 100644 scripts/helm/db/sqs/templates/serviceaccount.yaml delete mode 100644 scripts/helm/db/sqs/values.yaml diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index 435d07126..c247b2a68 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -39,13 +39,11 @@ jobs: ENVIRONMENT: staging run: | cd api - bash build.sh - [[ -z "${DOCKER_REPO}" ]] || { - docker push ${DOCKER_REPO}/chalice:"${IMAGE_TAG}" - } + PUSH_IMAGE=1 bash build.sh - name: Deploy to kubernetes run: | cd scripts/helm/ + 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/tag:.*/tag: \"$IMAGE_TAG\"/g" app/chalice.yaml bash kube-install.sh --app chalice diff --git a/api/.chalice/config.json b/api/.chalice/config.json index 8f2874beb..8385a17e7 100644 --- a/api/.chalice/config.json +++ b/api/.chalice/config.json @@ -28,14 +28,12 @@ "assign_link": "http://127.0.0.1:8000/async/email_assignment", "captcha_server": "", "captcha_key": "", - "sessions_bucket": "asayer-mobs", + "sessions_bucket": "mobs", "sessions_region": "us-east-1", "put_S3_TTL": "20", - "sourcemaps_bucket": "asayer-sourcemaps", - "sourcemaps_bucket_key": "", - "sourcemaps_bucket_secret": "", - "sourcemaps_bucket_region": "us-east-1", - "js_cache_bucket": "asayer-sessions-assets", + "sourcemaps_reader": "http://127.0.0.1:3000/", + "sourcemaps_bucket": "sourcemaps", + "js_cache_bucket": "sessions-assets", "async_Token": "", "EMAIL_HOST": "", "EMAIL_PORT": "587", diff --git a/api/.gitignore b/api/.gitignore index d9688e343..dd32b5d3f 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -170,7 +170,7 @@ logs*.txt *.csv *.p -*.js SUBNETS.json ./chalicelib/.configs +README/* \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 0ca8c1edf..84d1b88f5 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -4,6 +4,14 @@ WORKDIR /work COPY . . 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 sourcemaps_reader && \ + npm install # Add Tini # Startup daemon @@ -13,4 +21,4 @@ ENV ENTERPRISE_BUILD ${envarg} ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini RUN chmod +x /tini ENTRYPOINT ["/tini", "--"] -CMD python env_handler.py && chalice local --no-autoreload --host 0.0.0.0 --stage ${ENTERPRISE_BUILD} \ No newline at end of file +CMD ./entrypoint.sh diff --git a/api/app.py b/api/app.py index 469d8a42f..2c4465189 100644 --- a/api/app.py +++ b/api/app.py @@ -23,13 +23,13 @@ import traceback old_tb = traceback.print_exception old_f = sys.stdout old_e = sys.stderr -ASAYER_SESSION_ID = None +OR_SESSION_TOKEN = None class F: def write(self, x): - if ASAYER_SESSION_ID is not None and x != '\n' and not helper.is_local(): - old_f.write(f"[asayer_session_id={ASAYER_SESSION_ID}] {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) @@ -38,9 +38,8 @@ class F: def tb_print_exception(etype, value, tb, limit=None, file=None, chain=True): - if ASAYER_SESSION_ID is not None and not helper.is_local(): - # bugsnag.notify(Exception(str(value)), meta_data={"special_info": {"asayerSessionId": ASAYER_SESSION_ID}}) - value = type(value)(f"[asayer_session_id={ASAYER_SESSION_ID}] " + str(value)) + if OR_SESSION_TOKEN is not None and not helper.is_local(): + value = type(value)(f"[or_session_token={OR_SESSION_TOKEN}] " + str(value)) old_tb(etype, value, tb, limit, file, chain) @@ -55,11 +54,11 @@ sys.stderr = F() _overrides.chalice_app(app) -# v0905 + @app.middleware('http') -def asayer_middleware(event, get_response): - global ASAYER_SESSION_ID - ASAYER_SESSION_ID = app.current_request.headers.get('vnd.openreplay.com.sid', +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!!") @@ -71,19 +70,24 @@ def asayer_middleware(event, get_response): import time now = int(time.time() * 1000) response = get_response(event) + if response.status_code == 500 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: - print("middleware exception handling") - print(e) - pg_client.close() - if helper.allow_sentry() and ASAYER_SESSION_ID is not None and not helper.is_local(): + 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', ASAYER_SESSION_ID) + scope.set_tag('openReplaySessionToken', OR_SESSION_TOKEN) scope.set_extra("context", event.context) sentry_sdk.capture_exception(e) - raise e + response = Response(body={"Code": "InternalServerError", + "Message": "An internal server error occurred [level=Fatal]."}, + status_code=500) pg_client.close() return response diff --git a/api/chalicelib/blueprints/bp_core.py b/api/chalicelib/blueprints/bp_core.py index 3b2910606..bd42b2254 100644 --- a/api/chalicelib/blueprints/bp_core.py +++ b/api/chalicelib/blueprints/bp_core.py @@ -881,5 +881,5 @@ def all_issue_types(context): @app.route('/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) @app.route('/{projectId}/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) -def removed_endpoints(context): +def removed_endpoints(projectId=None, context=None): return Response(body={"errors": ["Endpoint no longer available"]}, status_code=410) diff --git a/api/chalicelib/blueprints/bp_core_dynamic.py b/api/chalicelib/blueprints/bp_core_dynamic.py index 4ec5278d7..1768896f9 100644 --- a/api/chalicelib/blueprints/bp_core_dynamic.py +++ b/api/chalicelib/blueprints/bp_core_dynamic.py @@ -35,7 +35,7 @@ def login(): 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 + for_plugin=False ) if r is None: return { @@ -73,10 +73,12 @@ def get_account(context): "projects": -1, "metadata": metadata.get_remaining_metadata_with_count(context['tenantId']) }, - **license.get_status(context["tenantId"]) + **license.get_status(context["tenantId"]), + "smtp": environ["EMAIL_HOST"] is not None and len(environ["EMAIL_HOST"]) > 0 } } + @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, @@ -156,12 +158,28 @@ 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"]} - if Slack.add_integration(tenant_id=context["tenantId"], url=data["url"], name=data["name"]): - return {"data": {"status": "success"}} - else: + n = Slack.add_channel(tenant_id=context["tenantId"], url=data["url"], name=data["name"]) + if n is None: return { - "errors": ["failed URL verification, if you received a message on slack, please notify our dev-team"] + "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): @@ -386,6 +404,7 @@ def search_sessions_by_metadata(context): m_key=key, project_id=project_id)} + @app.route('/plans', methods=['GET']) def get_current_plan(context): return { diff --git a/api/chalicelib/core/collaboration_slack.py b/api/chalicelib/core/collaboration_slack.py index 5fc80511c..b3da03a37 100644 --- a/api/chalicelib/core/collaboration_slack.py +++ b/api/chalicelib/core/collaboration_slack.py @@ -6,19 +6,18 @@ from chalicelib.core import webhook class Slack: @classmethod - def add_integration(cls, tenant_id, **args): + def add_channel(cls, tenant_id, **args): url = args["url"] name = args["name"] - if cls.__say_hello(url): - webhook.add(tenant_id=tenant_id, - endpoint=url, - webhook_type="slack", - name=name) - return True - return False + 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): + def say_hello(cls, url): r = requests.post( url=url, json={ diff --git a/api/chalicelib/core/dashboard.py b/api/chalicelib/core/dashboard.py index a778dcdfc..f306a51b4 100644 --- a/api/chalicelib/core/dashboard.py +++ b/api/chalicelib/core/dashboard.py @@ -146,7 +146,6 @@ def get_processed_sessions(project_id, startTimestamp=TimeUTC.now(delta_days=-1) ORDER BY generated_timestamp;""" params = {"step_size": step_size, "project_id": project_id, "startTimestamp": startTimestamp, "endTimestamp": endTimestamp, **__get_constraint_values(args)} - print(cur.mogrify(pg_query, params)) cur.execute(cur.mogrify(pg_query, params)) rows = cur.fetchall() results = { @@ -640,9 +639,6 @@ def search(text, resource_type, project_id, performance=False, pages_only=False, FROM events.pages INNER JOIN public.sessions USING (session_id) WHERE {" AND ".join(pg_sub_query)} LIMIT 10;""" - print(cur.mogrify(pg_query, {"project_id": project_id, - "value": helper.string_to_sql_like(text), - "platform_0": platform})) cur.execute(cur.mogrify(pg_query, {"project_id": project_id, "value": helper.string_to_sql_like(text), "platform_0": platform})) diff --git a/api/chalicelib/core/events.py b/api/chalicelib/core/events.py index 65ade49ed..69213a079 100644 --- a/api/chalicelib/core/events.py +++ b/api/chalicelib/core/events.py @@ -365,7 +365,7 @@ def __get_merged_queries(queries, value, project_id): def __get_autocomplete_table(value, project_id): with pg_client.PostgresClient() as cur: cur.execute(cur.mogrify("""SELECT DISTINCT ON(value,type) project_id, value, type - FROM (SELECT * + FROM (SELECT project_id, type, value FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY type ORDER BY value) AS Row_ID FROM public.autocomplete diff --git a/api/chalicelib/core/integration_jira_cloud_issue.py b/api/chalicelib/core/integration_jira_cloud_issue.py index 00fac2fcb..bb847007a 100644 --- a/api/chalicelib/core/integration_jira_cloud_issue.py +++ b/api/chalicelib/core/integration_jira_cloud_issue.py @@ -34,7 +34,7 @@ class JIRACloudIntegrationIssue(BaseIntegrationIssue): if len(projects_map[integration_project_id]) > 0: jql += f" AND ID IN ({','.join(projects_map[integration_project_id])})" issues = self._client.get_issues(jql, offset=0) - results += [issues] + results += issues return {"issues": results} def get(self, integration_project_id, assignment_id): diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 439bca0fd..fa127b04a 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -1,6 +1,6 @@ from chalicelib.utils import pg_client, helper from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, \ - sessions_mobs + sessions_mobs, issues from chalicelib.utils import dev from chalicelib.core import projects, errors @@ -25,7 +25,7 @@ SESSION_PROJECTION_COLS = """s.project_id, s.user_anonymous_id, s.platform, s.issue_score, - s.issue_types::text[] AS issue_types, + to_jsonb(s.issue_types) AS issue_types, favorite_sessions.session_id NOTNULL AS favorite, COALESCE((SELECT TRUE FROM public.user_viewed_sessions AS fs @@ -84,7 +84,6 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_ios(sessionId=session_id) - data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data["socket"] = socket_ios.start_replay(project_id=project_id, session_id=session_id, device=data["userDevice"], os_version=data["userOsVersion"], @@ -101,9 +100,11 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events.get_customs_by_sessionId2_pg(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_web(sessionId=session_id) - data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data['resources'] = resources.get_by_session_id(session_id=session_id) + data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) + data['issues'] = issues.get_by_session_id(session_id=session_id) + return data return None diff --git a/api/chalicelib/core/sessions_assignments.py b/api/chalicelib/core/sessions_assignments.py index 2b9c28d8f..3e0929dad 100644 --- a/api/chalicelib/core/sessions_assignments.py +++ b/api/chalicelib/core/sessions_assignments.py @@ -119,7 +119,6 @@ def get_by_session(tenant_id, user_id, project_id, session_id): continue r = integration.issue_handler.get_by_ids(saved_issues=issues[tool]) - print(r) for i in r["issues"]: i["provider"] = tool results += r["issues"] diff --git a/api/chalicelib/core/sessions_mobs.py b/api/chalicelib/core/sessions_mobs.py index 75ac59307..ea020d412 100644 --- a/api/chalicelib/core/sessions_mobs.py +++ b/api/chalicelib/core/sessions_mobs.py @@ -1,14 +1,10 @@ from chalicelib.utils.helper import environ -import boto3 +from chalicelib.utils.s3 import client def get_web(sessionId): - return boto3.client('s3', - endpoint_url=environ["S3_HOST"], - aws_access_key_id=environ["S3_KEY"], - aws_secret_access_key=environ["S3_SECRET"], - region_name=environ["sessions_region"]).generate_presigned_url( + return client.generate_presigned_url( 'get_object', Params={ 'Bucket': environ["sessions_bucket"], @@ -19,7 +15,7 @@ def get_web(sessionId): def get_ios(sessionId): - return boto3.client('s3', region_name=environ["ios_region"]).generate_presigned_url( + return client.generate_presigned_url( 'get_object', Params={ 'Bucket': environ["ios_bucket"], diff --git a/api/chalicelib/core/sourcemaps.py b/api/chalicelib/core/sourcemaps.py index c198b859b..01204847c 100644 --- a/api/chalicelib/core/sourcemaps.py +++ b/api/chalicelib/core/sourcemaps.py @@ -80,7 +80,12 @@ def get_traces_group(project_id, payload): payloads = {} all_exists = True for i, u in enumerate(frames): + print("===============================") + print(u["absPath"]) + print("converted to:") key = __get_key(project_id, u["absPath"]) # use filename instead? + print(key) + print("===============================") if key not in payloads: file_exists = s3.exists(environ['sourcemaps_bucket'], key) all_exists = all_exists and file_exists diff --git a/api/chalicelib/core/sourcemaps_parser.py b/api/chalicelib/core/sourcemaps_parser.py index cb0463d55..b7c17f3d3 100644 --- a/api/chalicelib/core/sourcemaps_parser.py +++ b/api/chalicelib/core/sourcemaps_parser.py @@ -8,14 +8,9 @@ def get_original_trace(key, positions): "key": key, "positions": positions, "padding": 5, - "bucket": environ['sourcemaps_bucket'], - "bucket_config": { - "aws_access_key_id": environ["sourcemaps_bucket_key"], - "aws_secret_access_key": environ["sourcemaps_bucket_secret"], - "aws_region": environ["sourcemaps_bucket_region"] - } + "bucket": environ['sourcemaps_bucket'] } - r = requests.post(environ["sourcemaps"], json=payload) + r = requests.post(environ["sourcemaps_reader"], json=payload) if r.status_code != 200: return {} diff --git a/api/chalicelib/core/telemetry.py b/api/chalicelib/core/telemetry.py index 362550553..48f403f57 100644 --- a/api/chalicelib/core/telemetry.py +++ b/api/chalicelib/core/telemetry.py @@ -30,7 +30,7 @@ def compute(): RETURNING *,(SELECT email FROM public.users WHERE role='owner' LIMIT 1);""" ) data = cur.fetchone() - requests.post('https://parrot.asayer.io/os/telemetry', json=process_data(data)) + requests.post('https://parrot.asayer.io/os/telemetry', json={"stats": [process_data(data)]}) def new_client(): diff --git a/api/chalicelib/core/tenants.py b/api/chalicelib/core/tenants.py index f047dcffa..4b439cfef 100644 --- a/api/chalicelib/core/tenants.py +++ b/api/chalicelib/core/tenants.py @@ -10,7 +10,7 @@ def get_by_tenant_id(tenant_id): f"""SELECT tenant_id, name, - api_key + api_key, created_at, edition, version_number, diff --git a/api/chalicelib/core/webhook.py b/api/chalicelib/core/webhook.py index 99a3b0569..fff2d4e7e 100644 --- a/api/chalicelib/core/webhook.py +++ b/api/chalicelib/core/webhook.py @@ -24,7 +24,7 @@ def get(tenant_id, webhook_id): cur.execute( cur.mogrify("""\ SELECT - w.* + webhook_id AS integration_id, webhook_id AS id, w.* FROM public.webhooks AS w where w.webhook_id =%(webhook_id)s AND deleted_at ISNULL;""", {"webhook_id": webhook_id}) @@ -40,7 +40,7 @@ def get_by_type(tenant_id, webhook_type): cur.execute( cur.mogrify("""\ SELECT - w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at + w.webhook_id AS integration_id, w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at FROM public.webhooks AS w WHERE w.type =%(type)s AND deleted_at ISNULL;""", {"type": webhook_type}) @@ -55,7 +55,7 @@ def get_by_tenant(tenant_id, replace_none=False): with pg_client.PostgresClient() as cur: cur.execute("""\ SELECT - w.* + webhook_id AS integration_id, webhook_id AS id, w.* FROM public.webhooks AS w WHERE deleted_at ISNULL;""" ) @@ -81,7 +81,7 @@ def update(tenant_id, webhook_id, changes, replace_none=False): UPDATE public.webhooks SET {','.join(sub_query)} WHERE webhook_id =%(id)s AND deleted_at ISNULL - RETURNING *;""", + RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", {"id": webhook_id, **changes}) ) w = helper.dict_to_camel_case(cur.fetchone()) @@ -98,7 +98,7 @@ def add(tenant_id, endpoint, auth_header=None, webhook_type='webhook', name="", query = cur.mogrify("""\ INSERT INTO public.webhooks(endpoint,auth_header,type,name) VALUES (%(endpoint)s, %(auth_header)s, %(type)s,%(name)s) - RETURNING *;""", + RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", {"endpoint": endpoint, "auth_header": auth_header, "type": webhook_type, "name": name}) cur.execute( diff --git a/api/chalicelib/utils/jira_client.py b/api/chalicelib/utils/jira_client.py index 6da501bbe..a7ab92932 100644 --- a/api/chalicelib/utils/jira_client.py +++ b/api/chalicelib/utils/jira_client.py @@ -68,7 +68,8 @@ class JiraManager: # print(issue.raw) issue_dict_list.append(self.__parser_issue_info(issue, include_comments=False)) - return {"total": issues.total, "issues": issue_dict_list} + # return {"total": issues.total, "issues": issue_dict_list} + return issue_dict_list def get_issue(self, issue_id: str): try: diff --git a/api/chalicelib/utils/pg_client.py b/api/chalicelib/utils/pg_client.py index 8d1e37d40..89a9dc8fa 100644 --- a/api/chalicelib/utils/pg_client.py +++ b/api/chalicelib/utils/pg_client.py @@ -9,9 +9,25 @@ PG_CONFIG = {"host": environ["pg_host"], "port": int(environ["pg_port"])} from psycopg2 import pool +from threading import Semaphore + + +class ORThreadedConnectionPool(psycopg2.pool.ThreadedConnectionPool): + def __init__(self, minconn, maxconn, *args, **kwargs): + self._semaphore = Semaphore(maxconn) + super().__init__(minconn, maxconn, *args, **kwargs) + + def getconn(self, *args, **kwargs): + self._semaphore.acquire() + return super().getconn(*args, **kwargs) + + def putconn(self, *args, **kwargs): + super().putconn(*args, **kwargs) + self._semaphore.release() + try: - postgreSQL_pool = psycopg2.pool.ThreadedConnectionPool(6, 20, **PG_CONFIG) + postgreSQL_pool = ORThreadedConnectionPool(20, 100, **PG_CONFIG) if (postgreSQL_pool): print("Connection pool created successfully") except (Exception, psycopg2.DatabaseError) as error: @@ -19,13 +35,6 @@ except (Exception, psycopg2.DatabaseError) as error: raise error -# finally: -# # closing database connection. -# # use closeall method to close all the active connection if you want to turn of the application -# if (postgreSQL_pool): -# postgreSQL_pool.closeall -# print("PostgreSQL connection pool is closed") - class PostgresClient: connection = None cursor = None diff --git a/api/chalicelib/utils/s3.py b/api/chalicelib/utils/s3.py index 29a8d28bc..49b6cfc85 100644 --- a/api/chalicelib/utils/s3.py +++ b/api/chalicelib/utils/s3.py @@ -2,7 +2,7 @@ from botocore.exceptions import ClientError from chalicelib.utils.helper import environ import boto3 - +import botocore from botocore.client import Config client = boto3.client('s3', endpoint_url=environ["S3_HOST"], @@ -13,14 +13,20 @@ client = boto3.client('s3', endpoint_url=environ["S3_HOST"], def exists(bucket, key): - response = client.list_objects_v2( - Bucket=bucket, - Prefix=key, - ) - for obj in response.get('Contents', []): - if obj['Key'] == key: - return True - return False + try: + boto3.resource('s3', endpoint_url=environ["S3_HOST"], + aws_access_key_id=environ["S3_KEY"], + aws_secret_access_key=environ["S3_SECRET"], + config=Config(signature_version='s3v4'), + region_name='us-east-1') \ + .Object(bucket, key).load() + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == "404": + return False + else: + # Something else has gone wrong. + raise + return True def get_presigned_url_for_sharing(bucket, expires_in, key, check_exists=False): @@ -49,6 +55,9 @@ def get_presigned_url_for_upload(bucket, expires_in, key): def get_file(source_bucket, source_key): + print("******************************") + print(f"looking for: {source_key} in {source_bucket}") + print("******************************") try: result = client.get_object( Bucket=source_bucket, diff --git a/api/entrypoint.sh b/api/entrypoint.sh new file mode 100755 index 000000000..3c3d12fd5 --- /dev/null +++ b/api/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd sourcemaps_reader +nohup node server.js &> /tmp/sourcemaps_reader.log & +cd .. +python env_handler.py +chalice local --no-autoreload --host 0.0.0.0 --stage ${ENTERPRISE_BUILD} diff --git a/api/requirements.txt b/api/requirements.txt index 094d32758..671aa5da5 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -5,9 +5,6 @@ pyjwt==1.7.1 psycopg2-binary==2.8.6 pytz==2020.1 sentry-sdk==0.19.1 -rollbar==0.15.1 -bugsnag==4.0.1 -kubernetes==12.0.0 elasticsearch==7.9.1 jira==2.0.0 schedule==1.1.0 diff --git a/api/sourcemaps_reader/handler.js b/api/sourcemaps_reader/handler.js new file mode 100644 index 000000000..117808cae --- /dev/null +++ b/api/sourcemaps_reader/handler.js @@ -0,0 +1,111 @@ +'use strict'; +const sourceMap = require('source-map'); +const AWS = require('aws-sdk'); +const sourceMapVersion = require('./package.json').dependencies["source-map"]; +const URL = require('url'); +const getVersion = version => version.replace(/[\^\$\=\~]/, ""); + +module.exports.sourcemapReader = async event => { + sourceMap.SourceMapConsumer.initialize({ + "lib/mappings.wasm": `https://unpkg.com/source-map@${getVersion(sourceMapVersion)}/lib/mappings.wasm` + }); + let s3; + if (process.env.S3_HOST) { + s3 = new AWS.S3({ + endpoint: process.env.S3_HOST, + accessKeyId: process.env.S3_KEY, + secretAccessKey: process.env.S3_SECRET, + s3ForcePathStyle: true, // needed with minio? + signatureVersion: 'v4' + }); + } else { + s3 = new AWS.S3({ + 'AccessKeyID': process.env.aws_access_key_id, + 'SecretAccessKey': process.env.aws_secret_access_key, + 'Region': process.env.aws_region + }); + } + + var options = { + Bucket: event.bucket, + Key: event.key + }; + return new Promise(function (resolve, reject) { + s3.getObject(options, (err, data) => { + if (err) { + console.log("Get S3 object failed"); + console.log(err); + return reject(err); + } + const sourcemap = data.Body.toString(); + + return new sourceMap.SourceMapConsumer(sourcemap) + .then(consumer => { + let results = []; + for (let i = 0; i < event.positions.length; i++) { + let original = consumer.originalPositionFor({ + line: event.positions[i].line, + column: event.positions[i].column + }); + let url = URL.parse(""); + let preview = []; + if (original.source) { + preview = consumer.sourceContentFor(original.source, true); + if (preview !== null) { + preview = preview.split("\n") + .map((line, i) => [i + 1, line]); + if (event.padding) { + let start = original.line < event.padding ? 0 : original.line - event.padding; + preview = preview.slice(start, original.line + event.padding); + } + } else { + console.log("source not found, null preview for:"); + console.log(original.source); + preview = [] + } + url = URL.parse(original.source); + } else { + console.log("couldn't find original position of:"); + console.log({ + line: event.positions[i].line, + column: event.positions[i].column + }); + } + let result = { + "absPath": url.href, + "filename": url.pathname, + "lineNo": original.line, + "colNo": original.column, + "function": original.name, + "context": preview + }; + // console.log(result); + results.push(result); + } + + // Use this code if you don't use the http event with the LAMBDA-PROXY integration + return resolve(results); + }); + }); + }); +}; + + +// let v = { +// 'key': '1725/99f96f044fa7e941dbb15d7d68b20549', +// 'positions': [{'line': 1, 'column': 943}], +// 'padding': 5, +// 'bucket': 'asayer-sourcemaps' +// }; +// let v = { +// 'key': '1/65d8d3866bb8c92f3db612cb330f270c', +// 'positions': [{'line': 1, 'column': 0}], +// 'padding': 5, +// 'bucket': 'asayer-sourcemaps-staging' +// }; +// module.exports.sourcemapReader(v).then((r) => { +// // console.log(r); +// const fs = require('fs'); +// let data = JSON.stringify(r); +// fs.writeFileSync('results.json', data); +// }); \ No newline at end of file diff --git a/api/sourcemaps_reader/server.js b/api/sourcemaps_reader/server.js new file mode 100644 index 000000000..2a1c4dcf6 --- /dev/null +++ b/api/sourcemaps_reader/server.js @@ -0,0 +1,38 @@ +const http = require('http'); +const handler = require('./handler'); +const hostname = '127.0.0.1'; +const port = 3000; + +const server = http.createServer((req, res) => { + if (req.method === 'POST') { + let data = ''; + req.on('data', chunk => { + data += chunk; + }); + req.on('end', function () { + data = JSON.parse(data); + console.log("Starting parser for: " + data.key); + // process.env = {...process.env, ...data.bucket_config}; + handler.sourcemapReader(data) + .then((results) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(results)); + }) + .catch((e) => { + console.error("Something went wrong"); + console.error(e); + res.statusCode(500); + res.end(e); + }); + }) + } else { + res.statusCode = 405; + res.setHeader('Content-Type', 'text/plain'); + res.end('Method Not Allowed'); + } +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); \ No newline at end of file diff --git a/backend/pkg/db/postgres/messages_web.go b/backend/pkg/db/postgres/messages_web.go index 9156ab78e..25e044e68 100644 --- a/backend/pkg/db/postgres/messages_web.go +++ b/backend/pkg/db/postgres/messages_web.go @@ -92,8 +92,8 @@ func (conn *Conn) InsertWebPageEvent(sessionID uint64, e *PageEvent) error { if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, url.DiscardURLQuery(path), "LOCATION") - conn.insertAutocompleteValue(sessionID, url.DiscardURLQuery(e.Referrer), "REFERRER") + conn.insertAutocompleteValue(sessionID, "LOCATION", url.DiscardURLQuery(path)) + conn.insertAutocompleteValue(sessionID, "REFERRER", url.DiscardURLQuery(e.Referrer)) return nil } @@ -123,7 +123,7 @@ func (conn *Conn) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error { if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, e.Label, "CLICK") + conn.insertAutocompleteValue(sessionID, "CLICK", e.Label) return nil } @@ -158,7 +158,7 @@ func (conn *Conn) InsertWebInputEvent(sessionID uint64, e *InputEvent) error { if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, e.Label, "INPUT") + conn.insertAutocompleteValue(sessionID, "INPUT", e.Label) return nil } diff --git a/backend/services/db/messages.go b/backend/services/db/messages.go index 6aa4ac076..511165c5f 100644 --- a/backend/services/db/messages.go +++ b/backend/services/db/messages.go @@ -16,6 +16,7 @@ func insertMessage(sessionID uint64, msg Message) error { // Web case *SessionStart: + log.Printf("Session Start: %v", sessionID) return pg.InsertWebSessionStart(sessionID, m) case *SessionEnd: return pg.InsertWebSessionEnd(sessionID, m) diff --git a/backend/services/ender/builder/builder.go b/backend/services/ender/builder/builder.go index cccf96bcf..246b2f7e0 100644 --- a/backend/services/ender/builder/builder.go +++ b/backend/services/ender/builder/builder.go @@ -82,6 +82,9 @@ func (b *builder) iterateReadyMessage(iter func(msg Message)) { } func (b *builder) buildSessionEnd() { + if b.timestamp == 0 { + return + } sessionEnd := &SessionEnd{ Timestamp: b.timestamp, // + delay? } @@ -106,16 +109,25 @@ func (b *builder) buildInputEvent() { func (b *builder) handleMessage(message Message, messageID uint64) { timestamp := uint64(message.Meta().Timestamp) - if b.timestamp <= timestamp { + if b.timestamp <= timestamp { // unnecessary. TODO: test and remove b.timestamp = timestamp } - // Start from the first timestamp. + // Before the first timestamp. switch msg := message.(type) { case *SessionStart, *Metadata, *UserID, *UserAnonymousID: b.appendReadyMessage(msg) + case *RawErrorEvent: + b.appendReadyMessage(&ErrorEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Source: msg.Source, + Name: msg.Name, + Message: msg.Message, + Payload: msg.Payload, + }) } if b.timestamp == 0 { return @@ -177,15 +189,6 @@ func (b *builder) handleMessage(message Message, messageID uint64) { Timestamp: b.timestamp, }) } - case *RawErrorEvent: - b.appendReadyMessage(&ErrorEvent{ - MessageID: messageID, - Timestamp: msg.Timestamp, - Source: msg.Source, - Name: msg.Name, - Message: msg.Message, - Payload: msg.Payload, - }) case *JSException: b.appendReadyMessage(&ErrorEvent{ MessageID: messageID, diff --git a/backend/services/ender/builder/inputEventBuilder.go b/backend/services/ender/builder/inputEventBuilder.go index 4938e47a9..98c7ebaf6 100644 --- a/backend/services/ender/builder/inputEventBuilder.go +++ b/backend/services/ender/builder/inputEventBuilder.go @@ -69,10 +69,10 @@ func (b *inputEventBuilder) Build() *InputEvent { return nil } inputEvent := b.inputEvent - label := b.inputLabels[b.inputID] - // if !ok { - // return nil - // } + label, exists := b.inputLabels[b.inputID] + if !exists { + return nil + } inputEvent.Label = label b.inputEvent = nil diff --git a/backend/services/integrations/integration/sentry.go b/backend/services/integrations/integration/sentry.go index 39443f51a..0330430c3 100644 --- a/backend/services/integrations/integration/sentry.go +++ b/backend/services/integrations/integration/sentry.go @@ -111,7 +111,7 @@ PageLoop: c.errChan <- err continue } - if sessionID == 0 { // We can't felter them on request + if token == "" && sessionID == 0 { // We can't felter them on request continue } diff --git a/backend/services/integrations/main.go b/backend/services/integrations/main.go index 68b4ec5aa..e1ea58ebd 100644 --- a/backend/services/integrations/main.go +++ b/backend/services/integrations/main.go @@ -19,7 +19,7 @@ import ( func main() { log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) - TOPIC_TRIGGER := env.String("TOPIC_TRIGGER") + TOPIC_RAW := env.String("TOPIC_RAW") POSTGRES_STRING := env.String("POSTGRES_STRING") pg := postgres.NewConn(POSTGRES_STRING) @@ -43,6 +43,7 @@ func main() { }) producer:= queue.NewProducer() + defer producer.Close(15000) listener, err := postgres.NewIntegrationsListener(POSTGRES_STRING) if err != nil { @@ -72,13 +73,14 @@ func main() { sessionID := event.SessionID if sessionID == 0 { sessData, err := tokenizer.Parse(event.Token) - if err != nil { + if err != nil && err != token.EXPIRED { log.Printf("Error on token parsing: %v; Token: %v", err, event.Token) continue } sessionID = sessData.ID } - producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(event.RawErrorEvent)) + // TODO: send to ready-events topic. Otherwise it have to go through the events worker. + producer.Produce(TOPIC_RAW, sessionID, messages.Encode(event.RawErrorEvent)) case err := <-manager.Errors: log.Printf("Integration error: %v\n", err) case i := <-manager.RequestDataUpdates: @@ -86,10 +88,10 @@ func main() { if err := pg.UpdateIntegrationRequestData(&i); err != nil { log.Printf("Postgres Update request_data error: %v\n", err) } - //case err := <-listener.Errors: - //log.Printf("Postgres listen error: %v\n", err) + case err := <-listener.Errors: + log.Printf("Postgres listen error: %v\n", err) case iPointer := <-listener.Integrations: - // log.Printf("Integration update: %v\n", *iPointer) + log.Printf("Integration update: %v\n", *iPointer) err := manager.Update(iPointer) if err != nil { log.Printf("Integration parse error: %v | Integration: %v\n", err, *iPointer) diff --git a/ee/api/.chalice/config.json b/ee/api/.chalice/config.json index 605e5b7c1..5cda73bd3 100644 --- a/ee/api/.chalice/config.json +++ b/ee/api/.chalice/config.json @@ -31,14 +31,12 @@ "assign_link": "http://127.0.0.1:8000/async/email_assignment", "captcha_server": "", "captcha_key": "", - "sessions_bucket": "asayer-mobs", + "sessions_bucket": "mobs", "sessions_region": "us-east-1", "put_S3_TTL": "20", - "sourcemaps_bucket": "asayer-sourcemaps", - "sourcemaps_bucket_key": "", - "sourcemaps_bucket_secret": "", - "sourcemaps_bucket_region": "us-east-1", - "js_cache_bucket": "asayer-sessions-assets", + "sourcemaps_reader": "http://127.0.0.1:3000/", + "sourcemaps_bucket": "sourcemaps", + "js_cache_bucket": "sessions-assets", "async_Token": "", "EMAIL_HOST": "", "EMAIL_PORT": "587", diff --git a/ee/api/.gitignore b/ee/api/.gitignore index 812abce9c..7e2873ee0 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -170,8 +170,8 @@ logs*.txt *.csv *.p -*.js SUBNETS.json chalicelib/.config -chalicelib/saas \ No newline at end of file +chalicelib/saas +README/* \ No newline at end of file diff --git a/ee/api/app.py b/ee/api/app.py index da75c1ac5..d604992a1 100644 --- a/ee/api/app.py +++ b/ee/api/app.py @@ -25,13 +25,13 @@ import traceback old_tb = traceback.print_exception old_f = sys.stdout old_e = sys.stderr -ASAYER_SESSION_ID = None +OR_SESSION_TOKEN = None class F: def write(self, x): - if ASAYER_SESSION_ID is not None and x != '\n' and not helper.is_local(): - old_f.write(f"[asayer_session_id={ASAYER_SESSION_ID}] {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) @@ -40,9 +40,8 @@ class F: def tb_print_exception(etype, value, tb, limit=None, file=None, chain=True): - if ASAYER_SESSION_ID is not None and not helper.is_local(): - # bugsnag.notify(Exception(str(value)), meta_data={"special_info": {"asayerSessionId": ASAYER_SESSION_ID}}) - value = type(value)(f"[asayer_session_id={ASAYER_SESSION_ID}] " + str(value)) + if OR_SESSION_TOKEN is not None and not helper.is_local(): + value = type(value)(f"[or_session_token={OR_SESSION_TOKEN}] " + str(value)) old_tb(etype, value, tb, limit, file, chain) @@ -59,7 +58,7 @@ _overrides.chalice_app(app) @app.middleware('http') -def asayer_middleware(event, get_response): +def or_middleware(event, get_response): from chalicelib.ee import unlock if not unlock.is_valid(): return Response(body={"errors": ["expired license"]}, status_code=403) @@ -68,12 +67,11 @@ def asayer_middleware(event, get_response): if not projects.is_authorized(project_id=event.uri_params["projectId"], tenant_id=event.context["authorizer"]["tenantId"]): print("unauthorized project") - # return {"errors": ["unauthorized project"]} pg_client.close() return Response(body={"errors": ["unauthorized project"]}, status_code=401) - global ASAYER_SESSION_ID - ASAYER_SESSION_ID = app.current_request.headers.get('vnd.openreplay.com.sid', - app.current_request.headers.get('vnd.asayer.io.sid')) + 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() @@ -84,19 +82,24 @@ def asayer_middleware(event, get_response): import time now = int(time.time() * 1000) response = get_response(event) + if response.status_code == 500 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: - print("middleware exception handling") - print(e) - pg_client.close() - if helper.allow_sentry() and ASAYER_SESSION_ID is not None and not helper.is_local(): + 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', ASAYER_SESSION_ID) + scope.set_tag('openReplaySessionToken', OR_SESSION_TOKEN) scope.set_extra("context", event.context) sentry_sdk.capture_exception(e) - raise e + response = Response(body={"Code": "InternalServerError", + "Message": "An internal server error occurred [level=Fatal]."}, + status_code=500) pg_client.close() return response diff --git a/ee/api/chalicelib/blueprints/bp_core.py b/ee/api/chalicelib/blueprints/bp_core.py index 3b2910606..bd42b2254 100644 --- a/ee/api/chalicelib/blueprints/bp_core.py +++ b/ee/api/chalicelib/blueprints/bp_core.py @@ -881,5 +881,5 @@ def all_issue_types(context): @app.route('/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) @app.route('/{projectId}/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) -def removed_endpoints(context): +def removed_endpoints(projectId=None, context=None): return Response(body={"errors": ["Endpoint no longer available"]}, status_code=410) diff --git a/ee/api/chalicelib/blueprints/bp_core_dynamic.py b/ee/api/chalicelib/blueprints/bp_core_dynamic.py index 505f10cb9..6e45627df 100644 --- a/ee/api/chalicelib/blueprints/bp_core_dynamic.py +++ b/ee/api/chalicelib/blueprints/bp_core_dynamic.py @@ -73,10 +73,12 @@ def get_account(context): "projects": -1, "metadata": metadata.get_remaining_metadata_with_count(context['tenantId']) }, - **license.get_status(context["tenantId"]) + **license.get_status(context["tenantId"]), + "smtp": environ["EMAIL_HOST"] is not None and len(environ["EMAIL_HOST"]) > 0 } } + @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, @@ -157,12 +159,27 @@ 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"]} - if Slack.add_integration(tenant_id=context["tenantId"], url=data["url"], name=data["name"]): - return {"data": {"status": "success"}} - else: + n = Slack.add_channel(tenant_id=context["tenantId"], url=data["url"], name=data["name"]) + if n is None: return { - "errors": ["failed URL verification, if you received a message on slack, please notify our dev-team"] + "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']) @@ -391,6 +408,7 @@ def search_sessions_by_metadata(context): m_key=key, project_id=project_id)} + @app.route('/plans', methods=['GET']) def get_current_plan(context): return { diff --git a/ee/api/chalicelib/core/collaboration_slack.py b/ee/api/chalicelib/core/collaboration_slack.py index 5fc80511c..b3da03a37 100644 --- a/ee/api/chalicelib/core/collaboration_slack.py +++ b/ee/api/chalicelib/core/collaboration_slack.py @@ -6,19 +6,18 @@ from chalicelib.core import webhook class Slack: @classmethod - def add_integration(cls, tenant_id, **args): + def add_channel(cls, tenant_id, **args): url = args["url"] name = args["name"] - if cls.__say_hello(url): - webhook.add(tenant_id=tenant_id, - endpoint=url, - webhook_type="slack", - name=name) - return True - return False + 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): + def say_hello(cls, url): r = requests.post( url=url, json={ diff --git a/ee/api/chalicelib/core/events.py b/ee/api/chalicelib/core/events.py index 65ade49ed..69213a079 100644 --- a/ee/api/chalicelib/core/events.py +++ b/ee/api/chalicelib/core/events.py @@ -365,7 +365,7 @@ def __get_merged_queries(queries, value, project_id): def __get_autocomplete_table(value, project_id): with pg_client.PostgresClient() as cur: cur.execute(cur.mogrify("""SELECT DISTINCT ON(value,type) project_id, value, type - FROM (SELECT * + FROM (SELECT project_id, type, value FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY type ORDER BY value) AS Row_ID FROM public.autocomplete diff --git a/ee/api/chalicelib/core/integration_jira_cloud_issue.py b/ee/api/chalicelib/core/integration_jira_cloud_issue.py index 00fac2fcb..bb847007a 100644 --- a/ee/api/chalicelib/core/integration_jira_cloud_issue.py +++ b/ee/api/chalicelib/core/integration_jira_cloud_issue.py @@ -34,7 +34,7 @@ class JIRACloudIntegrationIssue(BaseIntegrationIssue): if len(projects_map[integration_project_id]) > 0: jql += f" AND ID IN ({','.join(projects_map[integration_project_id])})" issues = self._client.get_issues(jql, offset=0) - results += [issues] + results += issues return {"issues": results} def get(self, integration_project_id, assignment_id): diff --git a/ee/api/chalicelib/core/sessions.py b/ee/api/chalicelib/core/sessions.py index 9d9ff204a..56ba7c463 100644 --- a/ee/api/chalicelib/core/sessions.py +++ b/ee/api/chalicelib/core/sessions.py @@ -1,6 +1,6 @@ from chalicelib.utils import pg_client, helper from chalicelib.utils import dev -from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, sessions_mobs +from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, sessions_mobs, issues from chalicelib.ee import projects, errors @@ -24,7 +24,7 @@ SESSION_PROJECTION_COLS = """s.project_id, s.user_anonymous_id, s.platform, s.issue_score, - s.issue_types::text[] AS issue_types, + to_jsonb(s.issue_types) AS issue_types, favorite_sessions.session_id NOTNULL AS favorite, COALESCE((SELECT TRUE FROM public.user_viewed_sessions AS fs @@ -83,7 +83,6 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_ios(sessionId=session_id) - data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data["socket"] = socket_ios.start_replay(project_id=project_id, session_id=session_id, device=data["userDevice"], os_version=data["userOsVersion"], @@ -100,9 +99,11 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events.get_customs_by_sessionId2_pg(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_web(sessionId=session_id) - data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data['resources'] = resources.get_by_session_id(session_id=session_id) + data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) + data['issues'] = issues.get_by_session_id(session_id=session_id) + return data return None diff --git a/ee/api/chalicelib/core/sessions_assignments.py b/ee/api/chalicelib/core/sessions_assignments.py index 2b9c28d8f..3e0929dad 100644 --- a/ee/api/chalicelib/core/sessions_assignments.py +++ b/ee/api/chalicelib/core/sessions_assignments.py @@ -119,7 +119,6 @@ def get_by_session(tenant_id, user_id, project_id, session_id): continue r = integration.issue_handler.get_by_ids(saved_issues=issues[tool]) - print(r) for i in r["issues"]: i["provider"] = tool results += r["issues"] diff --git a/ee/api/chalicelib/core/sessions_mobs.py b/ee/api/chalicelib/core/sessions_mobs.py index b96662c67..80fe59b28 100644 --- a/ee/api/chalicelib/core/sessions_mobs.py +++ b/ee/api/chalicelib/core/sessions_mobs.py @@ -1,11 +1,11 @@ from chalicelib.utils import helper from chalicelib.utils.helper import environ -import boto3 +from chalicelib.utils.s3 import client def get_web(sessionId): - return boto3.client('s3', region_name=environ["sessions_region"]).generate_presigned_url( + return client.generate_presigned_url( 'get_object', Params={ 'Bucket': environ["sessions_bucket"], @@ -16,7 +16,7 @@ def get_web(sessionId): def get_ios(sessionId): - return boto3.client('s3', region_name=environ["ios_region"]).generate_presigned_url( + return client.generate_presigned_url( 'get_object', Params={ 'Bucket': environ["ios_bucket"], diff --git a/ee/api/chalicelib/core/sourcemaps.py b/ee/api/chalicelib/core/sourcemaps.py index 5f82a31e2..dbd7213ea 100644 --- a/ee/api/chalicelib/core/sourcemaps.py +++ b/ee/api/chalicelib/core/sourcemaps.py @@ -79,7 +79,12 @@ def get_traces_group(project_id, payload): payloads = {} all_exists = True for i, u in enumerate(frames): + print("===============================") + print(u["absPath"]) + print("converted to:") key = __get_key(project_id, u["absPath"]) # use filename instead? + print(key) + print("===============================") if key not in payloads: file_exists = s3.exists(environ['sourcemaps_bucket'], key) all_exists = all_exists and file_exists diff --git a/ee/api/chalicelib/core/sourcemaps_parser.py b/ee/api/chalicelib/core/sourcemaps_parser.py index cb0463d55..b7c17f3d3 100644 --- a/ee/api/chalicelib/core/sourcemaps_parser.py +++ b/ee/api/chalicelib/core/sourcemaps_parser.py @@ -8,14 +8,9 @@ def get_original_trace(key, positions): "key": key, "positions": positions, "padding": 5, - "bucket": environ['sourcemaps_bucket'], - "bucket_config": { - "aws_access_key_id": environ["sourcemaps_bucket_key"], - "aws_secret_access_key": environ["sourcemaps_bucket_secret"], - "aws_region": environ["sourcemaps_bucket_region"] - } + "bucket": environ['sourcemaps_bucket'] } - r = requests.post(environ["sourcemaps"], json=payload) + r = requests.post(environ["sourcemaps_reader"], json=payload) if r.status_code != 200: return {} diff --git a/ee/api/chalicelib/ee/webhook.py b/ee/api/chalicelib/ee/webhook.py index 0a2406ab9..20e873f5c 100644 --- a/ee/api/chalicelib/ee/webhook.py +++ b/ee/api/chalicelib/ee/webhook.py @@ -8,7 +8,7 @@ def get_by_id(webhook_id): cur.execute( cur.mogrify("""\ SELECT - w.* + webhook_id AS integration_id, webhook_id AS id, w.* FROM public.webhooks AS w where w.webhook_id =%(webhook_id)s AND deleted_at ISNULL;""", {"webhook_id": webhook_id}) @@ -24,7 +24,7 @@ def get(tenant_id, webhook_id): cur.execute( cur.mogrify("""\ SELECT - w.* + webhook_id AS integration_id, webhook_id AS id, w.* FROM public.webhooks AS w where w.webhook_id =%(webhook_id)s AND w.tenant_id =%(tenant_id)s AND deleted_at ISNULL;""", {"webhook_id": webhook_id, "tenant_id": tenant_id}) @@ -40,7 +40,7 @@ def get_by_type(tenant_id, webhook_type): cur.execute( cur.mogrify("""\ SELECT - w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at + w.webhook_id AS integration_id, w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at FROM public.webhooks AS w where w.tenant_id =%(tenant_id)s @@ -59,7 +59,7 @@ def get_by_tenant(tenant_id, replace_none=False): cur.execute( cur.mogrify("""\ SELECT - w.* + webhook_id AS integration_id, webhook_id AS id,w.* FROM public.webhooks AS w where w.tenant_id =%(tenant_id)s @@ -88,7 +88,7 @@ def update(tenant_id, webhook_id, changes, replace_none=False): UPDATE public.webhooks SET {','.join(sub_query)} WHERE tenant_id =%(tenant_id)s AND webhook_id =%(id)s AND deleted_at ISNULL - RETURNING *;""", + RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", {"tenant_id": tenant_id, "id": webhook_id, **changes}) ) w = helper.dict_to_camel_case(cur.fetchone()) @@ -105,7 +105,7 @@ def add(tenant_id, endpoint, auth_header=None, webhook_type='webhook', name="", query = cur.mogrify("""\ INSERT INTO public.webhooks(tenant_id, endpoint,auth_header,type,name) VALUES (%(tenant_id)s, %(endpoint)s, %(auth_header)s, %(type)s,%(name)s) - RETURNING *;""", + RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", {"tenant_id": tenant_id, "endpoint": endpoint, "auth_header": auth_header, "type": webhook_type, "name": name}) cur.execute( diff --git a/ee/api/chalicelib/utils/jira_client.py b/ee/api/chalicelib/utils/jira_client.py index 6da501bbe..a7ab92932 100644 --- a/ee/api/chalicelib/utils/jira_client.py +++ b/ee/api/chalicelib/utils/jira_client.py @@ -68,7 +68,8 @@ class JiraManager: # print(issue.raw) issue_dict_list.append(self.__parser_issue_info(issue, include_comments=False)) - return {"total": issues.total, "issues": issue_dict_list} + # return {"total": issues.total, "issues": issue_dict_list} + return issue_dict_list def get_issue(self, issue_id: str): try: diff --git a/ee/api/chalicelib/utils/pg_client.py b/ee/api/chalicelib/utils/pg_client.py index e95527d64..4df29be39 100644 --- a/ee/api/chalicelib/utils/pg_client.py +++ b/ee/api/chalicelib/utils/pg_client.py @@ -9,11 +9,26 @@ PG_CONFIG = {"host": environ["pg_host"], "port": int(environ["pg_port"])} # connexion pool for FOS & EE - from psycopg2 import pool +from threading import Semaphore + + +class ORThreadedConnectionPool(psycopg2.pool.ThreadedConnectionPool): + def __init__(self, minconn, maxconn, *args, **kwargs): + self._semaphore = Semaphore(maxconn) + super().__init__(minconn, maxconn, *args, **kwargs) + + def getconn(self, *args, **kwargs): + self._semaphore.acquire() + return super().getconn(*args, **kwargs) + + def putconn(self, *args, **kwargs): + super().putconn(*args, **kwargs) + self._semaphore.release() + try: - postgreSQL_pool = psycopg2.pool.ThreadedConnectionPool(6, 20, **PG_CONFIG) + postgreSQL_pool = ORThreadedConnectionPool(20, 100, **PG_CONFIG) if (postgreSQL_pool): print("Connection pool created successfully") except (Exception, psycopg2.DatabaseError) as error: @@ -21,13 +36,6 @@ except (Exception, psycopg2.DatabaseError) as error: raise error -# finally: -# # closing database connection. -# # use closeall method to close all the active connection if you want to turn of the application -# if (postgreSQL_pool): -# postgreSQL_pool.closeall -# print("PostgreSQL connection pool is closed") - class PostgresClient: connection = None cursor = None diff --git a/ee/api/chalicelib/utils/s3.py b/ee/api/chalicelib/utils/s3.py index 29a8d28bc..c9516982f 100644 --- a/ee/api/chalicelib/utils/s3.py +++ b/ee/api/chalicelib/utils/s3.py @@ -3,6 +3,7 @@ from chalicelib.utils.helper import environ import boto3 +import botocore from botocore.client import Config client = boto3.client('s3', endpoint_url=environ["S3_HOST"], @@ -13,51 +14,17 @@ client = boto3.client('s3', endpoint_url=environ["S3_HOST"], def exists(bucket, key): - response = client.list_objects_v2( - Bucket=bucket, - Prefix=key, - ) - for obj in response.get('Contents', []): - if obj['Key'] == key: - return True - return False - - -def get_presigned_url_for_sharing(bucket, expires_in, key, check_exists=False): - if check_exists and not exists(bucket, key): - return None - - return client.generate_presigned_url( - 'get_object', - Params={ - 'Bucket': bucket, - 'Key': key - }, - ExpiresIn=expires_in - ) - - -def get_presigned_url_for_upload(bucket, expires_in, key): - return client.generate_presigned_url( - 'put_object', - Params={ - 'Bucket': bucket, - 'Key': key - }, - ExpiresIn=expires_in - ) - - -def get_file(source_bucket, source_key): try: - result = client.get_object( - Bucket=source_bucket, - Key=source_key - ) - except ClientError as ex: - if ex.response['Error']['Code'] == 'NoSuchKey': - print(f'======> No object found - returning None for {source_bucket}/{source_key}') - return None + boto3.resource('s3', endpoint_url=environ["S3_HOST"], + aws_access_key_id=environ["S3_KEY"], + aws_secret_access_key=environ["S3_SECRET"], + config=Config(signature_version='s3v4'), + region_name='us-east-1') \ + .Object(bucket, key).load() + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == "404": + return False else: - raise ex - return result["Body"].read().decode() + # Something else has gone wrong. + raise + return True diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 3944c0923..4fa698105 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -5,9 +5,6 @@ pyjwt==1.7.1 psycopg2-binary==2.8.6 pytz==2020.1 sentry-sdk==0.19.1 -rollbar==0.15.1 -bugsnag==4.0.1 -kubernetes==12.0.0 elasticsearch==7.9.1 jira==2.0.0 schedule==1.1.0 diff --git a/ee/api/sourcemaps_reader/handler.js b/ee/api/sourcemaps_reader/handler.js new file mode 100644 index 000000000..117808cae --- /dev/null +++ b/ee/api/sourcemaps_reader/handler.js @@ -0,0 +1,111 @@ +'use strict'; +const sourceMap = require('source-map'); +const AWS = require('aws-sdk'); +const sourceMapVersion = require('./package.json').dependencies["source-map"]; +const URL = require('url'); +const getVersion = version => version.replace(/[\^\$\=\~]/, ""); + +module.exports.sourcemapReader = async event => { + sourceMap.SourceMapConsumer.initialize({ + "lib/mappings.wasm": `https://unpkg.com/source-map@${getVersion(sourceMapVersion)}/lib/mappings.wasm` + }); + let s3; + if (process.env.S3_HOST) { + s3 = new AWS.S3({ + endpoint: process.env.S3_HOST, + accessKeyId: process.env.S3_KEY, + secretAccessKey: process.env.S3_SECRET, + s3ForcePathStyle: true, // needed with minio? + signatureVersion: 'v4' + }); + } else { + s3 = new AWS.S3({ + 'AccessKeyID': process.env.aws_access_key_id, + 'SecretAccessKey': process.env.aws_secret_access_key, + 'Region': process.env.aws_region + }); + } + + var options = { + Bucket: event.bucket, + Key: event.key + }; + return new Promise(function (resolve, reject) { + s3.getObject(options, (err, data) => { + if (err) { + console.log("Get S3 object failed"); + console.log(err); + return reject(err); + } + const sourcemap = data.Body.toString(); + + return new sourceMap.SourceMapConsumer(sourcemap) + .then(consumer => { + let results = []; + for (let i = 0; i < event.positions.length; i++) { + let original = consumer.originalPositionFor({ + line: event.positions[i].line, + column: event.positions[i].column + }); + let url = URL.parse(""); + let preview = []; + if (original.source) { + preview = consumer.sourceContentFor(original.source, true); + if (preview !== null) { + preview = preview.split("\n") + .map((line, i) => [i + 1, line]); + if (event.padding) { + let start = original.line < event.padding ? 0 : original.line - event.padding; + preview = preview.slice(start, original.line + event.padding); + } + } else { + console.log("source not found, null preview for:"); + console.log(original.source); + preview = [] + } + url = URL.parse(original.source); + } else { + console.log("couldn't find original position of:"); + console.log({ + line: event.positions[i].line, + column: event.positions[i].column + }); + } + let result = { + "absPath": url.href, + "filename": url.pathname, + "lineNo": original.line, + "colNo": original.column, + "function": original.name, + "context": preview + }; + // console.log(result); + results.push(result); + } + + // Use this code if you don't use the http event with the LAMBDA-PROXY integration + return resolve(results); + }); + }); + }); +}; + + +// let v = { +// 'key': '1725/99f96f044fa7e941dbb15d7d68b20549', +// 'positions': [{'line': 1, 'column': 943}], +// 'padding': 5, +// 'bucket': 'asayer-sourcemaps' +// }; +// let v = { +// 'key': '1/65d8d3866bb8c92f3db612cb330f270c', +// 'positions': [{'line': 1, 'column': 0}], +// 'padding': 5, +// 'bucket': 'asayer-sourcemaps-staging' +// }; +// module.exports.sourcemapReader(v).then((r) => { +// // console.log(r); +// const fs = require('fs'); +// let data = JSON.stringify(r); +// fs.writeFileSync('results.json', data); +// }); \ No newline at end of file diff --git a/ee/api/sourcemaps_reader/server.js b/ee/api/sourcemaps_reader/server.js new file mode 100644 index 000000000..2a1c4dcf6 --- /dev/null +++ b/ee/api/sourcemaps_reader/server.js @@ -0,0 +1,38 @@ +const http = require('http'); +const handler = require('./handler'); +const hostname = '127.0.0.1'; +const port = 3000; + +const server = http.createServer((req, res) => { + if (req.method === 'POST') { + let data = ''; + req.on('data', chunk => { + data += chunk; + }); + req.on('end', function () { + data = JSON.parse(data); + console.log("Starting parser for: " + data.key); + // process.env = {...process.env, ...data.bucket_config}; + handler.sourcemapReader(data) + .then((results) => { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(results)); + }) + .catch((e) => { + console.error("Something went wrong"); + console.error(e); + res.statusCode(500); + res.end(e); + }); + }) + } else { + res.statusCode = 405; + res.setHeader('Content-Type', 'text/plain'); + res.end('Method Not Allowed'); + } +}); + +server.listen(port, hostname, () => { + console.log(`Server running at http://${hostname}:${port}/`); +}); \ No newline at end of file diff --git a/ee/connectors/bigquery_utils/create_table.py b/ee/connectors/bigquery_utils/create_table.py new file mode 100644 index 000000000..4b166e4ae --- /dev/null +++ b/ee/connectors/bigquery_utils/create_table.py @@ -0,0 +1,357 @@ +import os +from google.cloud import bigquery + +from db.loaders.bigquery_loader import creds_file + + +def create_tables_bigquery(): + create_sessions_table(creds_file=creds_file, + table_id=f"{os.environ['project_id']}.{os.environ['dataset']}.{os.environ['sessions_table']}") + print(f"`{os.environ['sessions_table']}` table created succesfully.") + create_events_table(creds_file=creds_file, + table_id=f"{os.environ['project_id']}.{os.environ['dataset']}.{os.environ['events_table_name']}") + print(f"`{os.environ['events_table_name']}` table created succesfully.") + + +def create_table(creds_file, table_id, schema): + client = bigquery.Client.from_service_account_json(creds_file) + table = bigquery.Table(table_id, schema=schema) + table = client.create_table(table) # Make an API request. + print( + "Created table {}.{}.{}".format(table.project, table.dataset_id, table.table_id) + ) + + +def create_sessions_table(creds_file, table_id): + schema = [ + bigquery.SchemaField("sessionid", "INT64", mode="REQUIRED"), + bigquery.SchemaField("user_agent", "STRING"), + bigquery.SchemaField("user_browser", "STRING"), + bigquery.SchemaField("user_browser_version", "STRING"), + bigquery.SchemaField("user_country", "STRING"), + bigquery.SchemaField("user_device", "STRING"), + bigquery.SchemaField("user_device_heap_size", "INT64"), + bigquery.SchemaField("user_device_memory_size", "INT64"), + + bigquery.SchemaField("user_device_type", "STRING"), + bigquery.SchemaField("user_os", "STRING"), + bigquery.SchemaField("user_os_version", "STRING"), + bigquery.SchemaField("user_uuid", "STRING"), + bigquery.SchemaField("connection_effective_bandwidth", "INT64"), + + bigquery.SchemaField("connection_type", "STRING"), + bigquery.SchemaField("metadata_key", "STRING"), + bigquery.SchemaField("metadata_value", "STRING"), + bigquery.SchemaField("referrer", "STRING"), + bigquery.SchemaField("user_anonymous_id", "STRING"), + bigquery.SchemaField("user_id", "STRING"), + bigquery.SchemaField("session_start_timestamp", "INT64"), + bigquery.SchemaField("session_end_timestamp", "INT64"), + bigquery.SchemaField("session_duration", "INT64"), + + bigquery.SchemaField("first_contentful_paint", "INT64"), + bigquery.SchemaField("speed_index", "INT64"), + bigquery.SchemaField("visually_complete", "INT64"), + bigquery.SchemaField("timing_time_to_interactive", "INT64"), + + bigquery.SchemaField("avg_cpu", "INT64"), + bigquery.SchemaField("avg_fps", "INT64"), + bigquery.SchemaField("max_cpu", "INT64"), + bigquery.SchemaField("max_fps", "INT64"), + bigquery.SchemaField("max_total_js_heap_size", "INT64"), + bigquery.SchemaField("max_used_js_heap_size", "INT64"), + + bigquery.SchemaField("js_exceptions_count", "INT64"), + bigquery.SchemaField("long_tasks_total_duration", "INT64"), + bigquery.SchemaField("long_tasks_max_duration", "INT64"), + bigquery.SchemaField("long_tasks_count", "INT64"), + bigquery.SchemaField("inputs_count", "INT64"), + bigquery.SchemaField("clicks_count", "INT64"), + bigquery.SchemaField("issues_count", "INT64"), + bigquery.SchemaField("issues", "STRING"), + bigquery.SchemaField("urls_count", "INT64"), + bigquery.SchemaField("urls", "STRING")] + create_table(creds_file, table_id, schema) + + +def create_events_table(creds_file, table_id): + + schema = [ + bigquery.SchemaField("sessionid", "INT64"), + bigquery.SchemaField("connectioninformation_downlink", "INT64"), + bigquery.SchemaField("connectioninformation_type", "STRING"), + bigquery.SchemaField("consolelog_level", "STRING"), + bigquery.SchemaField("consolelog_value", "STRING"), + bigquery.SchemaField("customevent_messageid", "INT64"), + bigquery.SchemaField("customevent_name", "STRING"), + bigquery.SchemaField("customevent_payload", "STRING"), + bigquery.SchemaField("customevent_timestamp", "INT64"), + bigquery.SchemaField("errorevent_message", "STRING"), + bigquery.SchemaField("errorevent_messageid", "INT64"), + bigquery.SchemaField("errorevent_name", "STRING"), + bigquery.SchemaField("errorevent_payload", "STRING"), + bigquery.SchemaField("errorevent_source", "STRING"), + bigquery.SchemaField("errorevent_timestamp", "INT64"), + bigquery.SchemaField("jsexception_message", "STRING"), + bigquery.SchemaField("jsexception_name", "STRING"), + bigquery.SchemaField("jsexception_payload", "STRING"), + bigquery.SchemaField("metadata_key", "STRING"), + bigquery.SchemaField("metadata_value", "STRING"), + bigquery.SchemaField("mouseclick_id", "INT64"), + bigquery.SchemaField("mouseclick_hesitationtime", "INT64"), + bigquery.SchemaField("mouseclick_label", "STRING"), + bigquery.SchemaField("pageevent_firstcontentfulpaint", "INT64"), + bigquery.SchemaField("pageevent_firstpaint", "INT64"), + bigquery.SchemaField("pageevent_messageid", "INT64"), + bigquery.SchemaField("pageevent_referrer", "STRING"), + bigquery.SchemaField("pageevent_speedindex", "INT64"), + bigquery.SchemaField("pageevent_timestamp", "INT64"), + bigquery.SchemaField("pageevent_url", "STRING"), + bigquery.SchemaField("pagerendertiming_timetointeractive", "INT64"), + bigquery.SchemaField("pagerendertiming_visuallycomplete", "INT64"), + bigquery.SchemaField("rawcustomevent_name", "STRING"), + bigquery.SchemaField("rawcustomevent_payload", "STRING"), + bigquery.SchemaField("setviewportsize_height", "INT64"), + bigquery.SchemaField("setviewportsize_width", "INT64"), + bigquery.SchemaField("timestamp_timestamp", "INT64"), + bigquery.SchemaField("user_anonymous_id", "STRING"), + bigquery.SchemaField("user_id", "STRING"), + bigquery.SchemaField("issueevent_messageid", "INT64"), + bigquery.SchemaField("issueevent_timestamp", "INT64"), + bigquery.SchemaField("issueevent_type", "STRING"), + bigquery.SchemaField("issueevent_contextstring", "STRING"), + bigquery.SchemaField("issueevent_context", "STRING"), + bigquery.SchemaField("issueevent_payload", "STRING"), + bigquery.SchemaField("customissue_name", "STRING"), + bigquery.SchemaField("customissue_payload", "STRING"), + bigquery.SchemaField("received_at", "INT64"), + bigquery.SchemaField("batch_order_number", "INT64")] + create_table(creds_file, table_id, schema) + + +def create_table_negatives(creds_file, table_id): + client = bigquery.Client.from_service_account_json(creds_file) + + schema = [ + bigquery.SchemaField("sessionid", "INT64", mode="REQUIRED"), + bigquery.SchemaField("clickevent_hesitationtime", "INT64"), + bigquery.SchemaField("clickevent_label", "STRING"), + bigquery.SchemaField("clickevent_messageid", "INT64"), + bigquery.SchemaField("clickevent_timestamp", "INT64"), + bigquery.SchemaField("connectioninformation_downlink", "INT64"), + bigquery.SchemaField("connectioninformation_type", "STRING"), + bigquery.SchemaField("consolelog_level", "STRING"), + bigquery.SchemaField("consolelog_value", "STRING"), + bigquery.SchemaField("cpuissue_duration", "INT64"), + bigquery.SchemaField("cpuissue_rate", "INT64"), + bigquery.SchemaField("cpuissue_timestamp", "INT64"), + bigquery.SchemaField("createdocument", "BOOL"), + bigquery.SchemaField("createelementnode_id", "INT64"), + bigquery.SchemaField("createelementnode_parentid", "INT64"), + bigquery.SchemaField("cssdeleterule_index", "INT64"), + bigquery.SchemaField("cssdeleterule_stylesheetid", "INT64"), + bigquery.SchemaField("cssinsertrule_index", "INT64"), + bigquery.SchemaField("cssinsertrule_rule", "STRING"), + bigquery.SchemaField("cssinsertrule_stylesheetid", "INT64"), + bigquery.SchemaField("customevent_messageid", "INT64"), + bigquery.SchemaField("customevent_name", "STRING"), + bigquery.SchemaField("customevent_payload", "STRING"), + bigquery.SchemaField("customevent_timestamp", "INT64"), + bigquery.SchemaField("domdrop_timestamp", "INT64"), + bigquery.SchemaField("errorevent_message", "STRING"), + bigquery.SchemaField("errorevent_messageid", "INT64"), + bigquery.SchemaField("errorevent_name", "STRING"), + bigquery.SchemaField("errorevent_payload", "STRING"), + bigquery.SchemaField("errorevent_source", "STRING"), + bigquery.SchemaField("errorevent_timestamp", "INT64"), + bigquery.SchemaField("fetch_duration", "INT64"), + bigquery.SchemaField("fetch_method", "STRING"), + bigquery.SchemaField("fetch_request", "STRING"), + bigquery.SchemaField("fetch_response", "STRING"), + bigquery.SchemaField("fetch_status", "INT64"), + bigquery.SchemaField("fetch_timestamp", "INT64"), + bigquery.SchemaField("fetch_url", "STRING"), + bigquery.SchemaField("graphql_operationkind", "STRING"), + bigquery.SchemaField("graphql_operationname", "STRING"), + bigquery.SchemaField("graphql_response", "STRING"), + bigquery.SchemaField("graphql_variables", "STRING"), + bigquery.SchemaField("graphqlevent_messageid", "INT64"), + bigquery.SchemaField("graphqlevent_name", "STRING"), + bigquery.SchemaField("graphqlevent_timestamp", "INT64"), + bigquery.SchemaField("inputevent_label", "STRING"), + bigquery.SchemaField("inputevent_messageid", "INT64"), + bigquery.SchemaField("inputevent_timestamp", "INT64"), + bigquery.SchemaField("inputevent_value", "STRING"), + bigquery.SchemaField("inputevent_valuemasked", "BOOL"), + bigquery.SchemaField("is_asayer_event", "BOOL"), + bigquery.SchemaField("jsexception_message", "STRING"), + bigquery.SchemaField("jsexception_name", "STRING"), + bigquery.SchemaField("jsexception_payload", "STRING"), + bigquery.SchemaField("longtasks_timestamp", "INT64"), + bigquery.SchemaField("longtasks_duration", "INT64"), + bigquery.SchemaField("longtasks_containerid", "STRING"), + bigquery.SchemaField("longtasks_containersrc", "STRING"), + bigquery.SchemaField("memoryissue_duration", "INT64"), + bigquery.SchemaField("memoryissue_rate", "INT64"), + bigquery.SchemaField("memoryissue_timestamp", "INT64"), + bigquery.SchemaField("metadata_key", "STRING"), + bigquery.SchemaField("metadata_value", "STRING"), + bigquery.SchemaField("mobx_payload", "STRING"), + bigquery.SchemaField("mobx_type", "STRING"), + bigquery.SchemaField("mouseclick_id", "INT64"), + bigquery.SchemaField("mouseclick_hesitationtime", "INT64"), + bigquery.SchemaField("mouseclick_label", "STRING"), + bigquery.SchemaField("mousemove_x", "INT64"), + bigquery.SchemaField("mousemove_y", "INT64"), + bigquery.SchemaField("movenode_id", "INT64"), + bigquery.SchemaField("movenode_index", "INT64"), + bigquery.SchemaField("movenode_parentid", "INT64"), + bigquery.SchemaField("ngrx_action", "STRING"), + bigquery.SchemaField("ngrx_duration", "INT64"), + bigquery.SchemaField("ngrx_state", "STRING"), + bigquery.SchemaField("otable_key", "STRING"), + bigquery.SchemaField("otable_value", "STRING"), + bigquery.SchemaField("pageevent_domcontentloadedeventend", "INT64"), + bigquery.SchemaField("pageevent_domcontentloadedeventstart", "INT64"), + bigquery.SchemaField("pageevent_firstcontentfulpaint", "INT64"), + bigquery.SchemaField("pageevent_firstpaint", "INT64"), + bigquery.SchemaField("pageevent_loaded", "BOOL"), + bigquery.SchemaField("pageevent_loadeventend", "INT64"), + bigquery.SchemaField("pageevent_loadeventstart", "INT64"), + bigquery.SchemaField("pageevent_messageid", "INT64"), + bigquery.SchemaField("pageevent_referrer", "STRING"), + bigquery.SchemaField("pageevent_requeststart", "INT64"), + bigquery.SchemaField("pageevent_responseend", "INT64"), + bigquery.SchemaField("pageevent_responsestart", "INT64"), + bigquery.SchemaField("pageevent_speedindex", "INT64"), + bigquery.SchemaField("pageevent_timestamp", "INT64"), + bigquery.SchemaField("pageevent_url", "STRING"), + bigquery.SchemaField("pageloadtiming_domcontentloadedeventend", "INT64"), + bigquery.SchemaField("pageloadtiming_domcontentloadedeventstart", "INT64"), + bigquery.SchemaField("pageloadtiming_firstcontentfulpaint", "INT64"), + bigquery.SchemaField("pageloadtiming_firstpaint", "INT64"), + bigquery.SchemaField("pageloadtiming_loadeventend", "INT64"), + bigquery.SchemaField("pageloadtiming_loadeventstart", "INT64"), + bigquery.SchemaField("pageloadtiming_requeststart", "INT64"), + bigquery.SchemaField("pageloadtiming_responseend", "INT64"), + bigquery.SchemaField("pageloadtiming_responsestart", "INT64"), + bigquery.SchemaField("pagerendertiming_speedindex", "INT64"), + bigquery.SchemaField("pagerendertiming_timetointeractive", "INT64"), + bigquery.SchemaField("pagerendertiming_visuallycomplete", "INT64"), + bigquery.SchemaField("performancetrack_frames", "INT64"), + bigquery.SchemaField("performancetrack_ticks", "INT64"), + bigquery.SchemaField("performancetrack_totaljsheapsize", "INT64"), + bigquery.SchemaField("performancetrack_usedjsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_avgcpu", "INT64"), + bigquery.SchemaField("performancetrackaggr_avgfps", "INT64"), + bigquery.SchemaField("performancetrackaggr_avgtotaljsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_avgusedjsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_maxcpu", "INT64"), + bigquery.SchemaField("performancetrackaggr_maxfps", "INT64"), + bigquery.SchemaField("performancetrackaggr_maxtotaljsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_maxusedjsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_mincpu", "INT64"), + bigquery.SchemaField("performancetrackaggr_minfps", "INT64"), + bigquery.SchemaField("performancetrackaggr_mintotaljsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_minusedjsheapsize", "INT64"), + bigquery.SchemaField("performancetrackaggr_timestampend", "INT64"), + bigquery.SchemaField("performancetrackaggr_timestampstart", "INT64"), + bigquery.SchemaField("profiler_args", "STRING"), + bigquery.SchemaField("profiler_duration", "INT64"), + bigquery.SchemaField("profiler_name", "STRING"), + bigquery.SchemaField("profiler_result", "STRING"), + bigquery.SchemaField("rawcustomevent_name", "STRING"), + bigquery.SchemaField("rawcustomevent_payload", "STRING"), + bigquery.SchemaField("rawerrorevent_message", "STRING"), + bigquery.SchemaField("rawerrorevent_name", "STRING"), + bigquery.SchemaField("rawerrorevent_payload", "STRING"), + bigquery.SchemaField("rawerrorevent_source", "STRING"), + bigquery.SchemaField("rawerrorevent_timestamp", "INT64"), + bigquery.SchemaField("redux_action", "STRING"), + bigquery.SchemaField("redux_duration", "INT64"), + bigquery.SchemaField("redux_state", "STRING"), + bigquery.SchemaField("removenode_id", "INT64"), + bigquery.SchemaField("removenodeattribute_id", "INT64"), + bigquery.SchemaField("removenodeattribute_name", "STRING"), + bigquery.SchemaField("resourceevent_decodedbodysize", "INT64"), + bigquery.SchemaField("resourceevent_duration", "INT64"), + bigquery.SchemaField("resourceevent_encodedbodysize", "INT64"), + bigquery.SchemaField("resourceevent_headersize", "INT64"), + bigquery.SchemaField("resourceevent_messageid", "INT64"), + bigquery.SchemaField("resourceevent_method", "STRING"), + bigquery.SchemaField("resourceevent_status", "INT64"), + bigquery.SchemaField("resourceevent_success", "BOOL"), + bigquery.SchemaField("resourceevent_timestamp", "INT64"), + bigquery.SchemaField("resourceevent_ttfb", "INT64"), + bigquery.SchemaField("resourceevent_type", "STRING"), + bigquery.SchemaField("resourceevent_url", "STRING"), + bigquery.SchemaField("resourcetiming_decodedbodysize", "INT64"), + bigquery.SchemaField("resourcetiming_duration", "INT64"), + bigquery.SchemaField("resourcetiming_encodedbodysize", "INT64"), + bigquery.SchemaField("resourcetiming_headersize", "INT64"), + bigquery.SchemaField("resourcetiming_initiator", "STRING"), + bigquery.SchemaField("resourcetiming_timestamp", "INT64"), + bigquery.SchemaField("resourcetiming_ttfb", "INT64"), + bigquery.SchemaField("resourcetiming_url", "STRING"), + bigquery.SchemaField("sessiondisconnect", "BOOL"), + bigquery.SchemaField("sessiondisconnect_timestamp", "INT64"), + bigquery.SchemaField("sessionend", "BOOL"), + bigquery.SchemaField("sessionend_timestamp", "INT64"), + bigquery.SchemaField("sessionstart_projectid", "INT64"), + bigquery.SchemaField("sessionstart_revid", "STRING"), + bigquery.SchemaField("sessionstart_timestamp", "INT64"), + bigquery.SchemaField("sessionstart_trackerversion", "STRING"), + bigquery.SchemaField("sessionstart_useragent", "STRING"), + bigquery.SchemaField("sessionstart_userbrowser", "STRING"), + bigquery.SchemaField("sessionstart_userbrowserversion", "STRING"), + bigquery.SchemaField("sessionstart_usercountry", "STRING"), + bigquery.SchemaField("sessionstart_userdevice", "STRING"), + bigquery.SchemaField("sessionstart_userdeviceheapsize", "INT64"), + bigquery.SchemaField("sessionstart_userdevicememorysize", "INT64"), + bigquery.SchemaField("sessionstart_userdevicetype", "STRING"), + bigquery.SchemaField("sessionstart_useros", "STRING"), + bigquery.SchemaField("sessionstart_userosversion", "STRING"), + bigquery.SchemaField("sessionstart_useruuid", "STRING"), + bigquery.SchemaField("setcssdata_data", "INT64"), + bigquery.SchemaField("setcssdata_id", "INT64"), + bigquery.SchemaField("setinputchecked_checked", "INT64"), + bigquery.SchemaField("setinputchecked_id", "INT64"), + bigquery.SchemaField("setinputtarget_id", "INT64"), + bigquery.SchemaField("setinputtarget_label", "INT64"), + bigquery.SchemaField("setinputvalue_id", "INT64"), + bigquery.SchemaField("setinputvalue_mask", "INT64"), + bigquery.SchemaField("setinputvalue_value", "INT64"), + bigquery.SchemaField("setnodeattribute_id", "INT64"), + bigquery.SchemaField("setnodeattribute_name", "INT64"), + bigquery.SchemaField("setnodeattribute_value", "INT64"), + bigquery.SchemaField("setnodedata_data", "INT64"), + bigquery.SchemaField("setnodedata_id", "INT64"), + bigquery.SchemaField("setnodescroll_id", "INT64"), + bigquery.SchemaField("setnodescroll_x", "INT64"), + bigquery.SchemaField("setnodescroll_y", "INT64"), + bigquery.SchemaField("setpagelocation_navigationstart", "INT64"), + bigquery.SchemaField("setpagelocation_referrer", "STRING"), + bigquery.SchemaField("setpagelocation_url", "STRING"), + bigquery.SchemaField("setpagevisibility_hidden", "BOOL"), + bigquery.SchemaField("setviewportscroll_x", "BOOL"), + bigquery.SchemaField("setviewportscroll_y", "BOOL"), + bigquery.SchemaField("setviewportsize_height", "INT64"), + bigquery.SchemaField("setviewportsize_width", "INT64"), + bigquery.SchemaField("stateaction_type", "STRING"), + bigquery.SchemaField("stateactionevent_messageid", "INT64"), + bigquery.SchemaField("stateactionevent_timestamp", "INT64"), + bigquery.SchemaField("stateactionevent_type", "STRING"), + bigquery.SchemaField("timestamp_timestamp", "INT64"), + bigquery.SchemaField("useranonymousid_id", "STRING"), + bigquery.SchemaField("userid_id", "STRING"), + bigquery.SchemaField("vuex_mutation", "STRING"), + bigquery.SchemaField("vuex_state", "STRING"), + bigquery.SchemaField("received_at", "INT64", mode="REQUIRED"), + bigquery.SchemaField("batch_order_number", "INT64", mode="REQUIRED") + ] + + table = bigquery.Table(table_id, schema=schema) + table = client.create_table(table) # Make an API request. + print( + "Created table {}.{}.{}".format(table.project, table.dataset_id, table.table_id) + ) diff --git a/ee/connectors/db/api.py b/ee/connectors/db/api.py new file mode 100644 index 000000000..33abf67cc --- /dev/null +++ b/ee/connectors/db/api.py @@ -0,0 +1,129 @@ +from sqlalchemy import create_engine +from sqlalchemy import MetaData +from sqlalchemy.orm import sessionmaker, session +from contextlib import contextmanager +import logging +import os +from pathlib import Path + +DATABASE = os.environ['DATABASE_NAME'] +if DATABASE == 'redshift': + import pandas_redshift as pr + +base_path = Path(__file__).parent.parent + +from db.models import Base + +logger = logging.getLogger(__file__) + + +def get_class_by_tablename(tablename): + """Return class reference mapped to table. + Raise an exception if class not found + + :param tablename: String with name of table. + :return: Class reference. + """ + for c in Base._decl_class_registry.values(): + if hasattr(c, '__tablename__') and c.__tablename__ == tablename: + return c + raise AttributeError(f'No model with tablename "{tablename}"') + + +class DBConnection: + """ + Initializes connection to a database + To update models file use: + sqlacodegen --outfile models_universal.py mysql+pymysql://{user}:{pwd}@{address} + """ + _sessions = sessionmaker() + + def __init__(self, config) -> None: + self.metadata = MetaData() + self.config = config + + if config == 'redshift': + self.pdredshift = pr + self.pdredshift.connect_to_redshift(dbname=os.environ['schema'], + host=os.environ['address'], + port=os.environ['port'], + user=os.environ['user'], + password=os.environ['password']) + + self.pdredshift.connect_to_s3(aws_access_key_id=os.environ['aws_access_key_id'], + aws_secret_access_key=os.environ['aws_secret_access_key'], + bucket=os.environ['bucket'], + subdirectory=os.environ['subdirectory']) + + self.connect_str = os.environ['connect_str'].format( + user=os.environ['user'], + password=os.environ['password'], + address=os.environ['address'], + port=os.environ['port'], + schema=os.environ['schema'] + ) + self.engine = create_engine(self.connect_str) + + elif config == 'clickhouse': + self.connect_str = os.environ['connect_str'].format( + address=os.environ['address'], + database=os.environ['database'] + ) + self.engine = create_engine(self.connect_str) + elif config == 'pg': + self.connect_str = os.environ['connect_str'].format( + user=os.environ['user'], + password=os.environ['password'], + address=os.environ['address'], + port=os.environ['port'], + database=os.environ['database'] + ) + self.engine = create_engine(self.connect_str) + elif config == 'bigquery': + pass + elif config == 'snowflake': + self.connect_str = os.environ['connect_str'].format( + user=os.environ['user'], + password=os.environ['password'], + account=os.environ['account'], + database=os.environ['database'], + schema = os.environ['schema'], + warehouse = os.environ['warehouse'] + ) + self.engine = create_engine(self.connect_str) + else: + raise ValueError("This db configuration doesn't exist. Add into keys file.") + + @contextmanager + def get_test_session(self, **kwargs) -> session: + """ + Test session context, even commits won't be persisted into db. + :Keyword Arguments: + * autoflush (``bool``) -- default: True + * autocommit (``bool``) -- default: False + * expire_on_commit (``bool``) -- default: True + """ + connection = self.engine.connect() + transaction = connection.begin() + my_session = type(self)._sessions(bind=connection, **kwargs) + yield my_session + + # Do cleanup, rollback and closing, whatever happens + my_session.close() + transaction.rollback() + connection.close() + + @contextmanager + def get_live_session(self) -> session: + """ + This is a session that can be committed. + Changes will be reflected in the database. + """ + # Automatic transaction and connection handling in session + connection = self.engine.connect() + my_session = type(self)._sessions(bind=connection) + + yield my_session + + my_session.close() + connection.close() diff --git a/ee/connectors/db/loaders/__init__.py b/ee/connectors/db/loaders/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ee/connectors/db/loaders/bigquery_loader.py b/ee/connectors/db/loaders/bigquery_loader.py new file mode 100644 index 000000000..2f3747d0a --- /dev/null +++ b/ee/connectors/db/loaders/bigquery_loader.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path + +from google.oauth2.service_account import Credentials + +# obtain the JSON file: +# In the Cloud Console, go to the Create service account key page. +# +# Go to the Create Service Account Key page +# From the Service account list, select New service account. +# In the Service account name field, enter a name. +# From the Role list, select Project > Owner. +# +# Note: The Role field affects which resources your service account can access in your project. You can revoke these roles or grant additional roles later. In production environments, do not grant the Owner, Editor, or Viewer roles. For more information, see Granting, changing, and revoking access to resources. +# Click Create. A JSON file that contains your key downloads to your computer. +# +# Put it in utils under a name bigquery_service_account + +base_path = Path(__file__).parent.parent.parent +creds_file = base_path / 'utils' / 'bigquery_service_account.json' +credentials = Credentials.from_service_account_file( + creds_file) + + +def insert_to_bigquery(df, table): + df.to_gbq(destination_table=f"{os.environ['dataset']}.{table}", + project_id=os.environ['project_id'], + if_exists='append', + credentials=credentials) + + +def transit_insert_to_bigquery(db, batch): + ... + diff --git a/ee/connectors/db/loaders/clickhouse_loader.py b/ee/connectors/db/loaders/clickhouse_loader.py new file mode 100644 index 000000000..2fea7fd01 --- /dev/null +++ b/ee/connectors/db/loaders/clickhouse_loader.py @@ -0,0 +1,4 @@ + +def insert_to_clickhouse(db, df, table: str): + df.to_sql(table, db.engine, if_exists='append', index=False) + diff --git a/ee/connectors/db/loaders/postgres_loader.py b/ee/connectors/db/loaders/postgres_loader.py new file mode 100644 index 000000000..bd982c607 --- /dev/null +++ b/ee/connectors/db/loaders/postgres_loader.py @@ -0,0 +1,3 @@ + +def insert_to_postgres(db, df, table: str): + df.to_sql(table, db.engine, if_exists='append', index=False) diff --git a/ee/connectors/db/loaders/redshift_loader.py b/ee/connectors/db/loaders/redshift_loader.py new file mode 100644 index 000000000..fe31d4fc4 --- /dev/null +++ b/ee/connectors/db/loaders/redshift_loader.py @@ -0,0 +1,19 @@ +from db.models import DetailedEvent +from psycopg2.errors import InternalError_ + + +def transit_insert_to_redshift(db, df, table): + + try: + insert_df(db.pdredshift, df, table) + except InternalError_ as e: + print(repr(e)) + print("loading failed. check stl_load_errors") + + +def insert_df(pr, df, table): + # Write the DataFrame to S3 and then to redshift + pr.pandas_to_redshift(data_frame=df, + redshift_table_name=table, + append=True, + delimiter='|') diff --git a/ee/connectors/db/loaders/snowflake_loader.py b/ee/connectors/db/loaders/snowflake_loader.py new file mode 100644 index 000000000..b0bfde37f --- /dev/null +++ b/ee/connectors/db/loaders/snowflake_loader.py @@ -0,0 +1,5 @@ + +def insert_to_snowflake(db, df, table): + df.to_sql(table, db.engine, if_exists='append', index=False) + + diff --git a/ee/connectors/db/models.py b/ee/connectors/db/models.py new file mode 100644 index 000000000..46654e249 --- /dev/null +++ b/ee/connectors/db/models.py @@ -0,0 +1,389 @@ +# coding: utf-8 +import yaml +from sqlalchemy import BigInteger, Boolean, Column, Integer, ARRAY, VARCHAR, text, VARCHAR +from sqlalchemy.ext.declarative import declarative_base +from pathlib import Path +import os + +DATABASE = os.environ['DATABASE_NAME'] + +Base = declarative_base() +metadata = Base.metadata + +base_path = Path(__file__).parent.parent + +# Load configuration file +conf = yaml.load( + open(f'{base_path}/utils/config.yml'), Loader=yaml.FullLoader) +try: + db_conf = conf[DATABASE] +except KeyError: + raise KeyError("Please provide a configuration in a YAML file with a key like\n" + "'snowflake', 'pg', 'bigquery', 'clickhouse' or 'redshift'.") + +# Get a table name from a configuration file +try: + events_table_name = db_conf['events_table_name'] +except KeyError as e: + events_table_name = None + print(repr(e)) +try: + events_detailed_table_name = db_conf['events_detailed_table_name'] +except KeyError as e: + print(repr(e)) + events_detailed_table_name = None +try: + sessions_table_name = db_conf['sessions_table'] +except KeyError as e: + print(repr(e)) + raise KeyError("Please provide a table name under a key 'table' in a YAML configuration file") + + +class Session(Base): + __tablename__ = sessions_table_name + + sessionid = Column(BigInteger, primary_key=True) + user_agent = Column(VARCHAR(5000)) + user_browser = Column(VARCHAR(5000)) + user_browser_version = Column(VARCHAR(5000)) + user_country = Column(VARCHAR(5000)) + user_device = Column(VARCHAR(5000)) + user_device_heap_size = Column(BigInteger) + user_device_memory_size = Column(BigInteger) + user_device_type = Column(VARCHAR(5000)) + user_os = Column(VARCHAR(5000)) + user_os_version = Column(VARCHAR(5000)) + user_uuid = Column(VARCHAR(5000)) + connection_effective_bandwidth = Column(BigInteger) # Downlink + connection_type = Column(VARCHAR(5000)) # "bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" + metadata_key = Column(VARCHAR(5000)) + metadata_value = Column(VARCHAR(5000)) + referrer = Column(VARCHAR(5000)) + user_anonymous_id = Column(VARCHAR(5000)) + user_id = Column(VARCHAR(5000)) + + # TIME + session_start_timestamp = Column(BigInteger) + session_end_timestamp = Column(BigInteger) + session_duration = Column(BigInteger) + + # SPEED INDEX RELATED + first_contentful_paint = Column(BigInteger) + speed_index = Column(BigInteger) + visually_complete = Column(BigInteger) + timing_time_to_interactive = Column(BigInteger) + + # PERFORMANCE + avg_cpu = Column(Integer) + avg_fps = Column(BigInteger) + max_cpu = Column(Integer) + max_fps = Column(BigInteger) + max_total_js_heap_size = Column(BigInteger) + max_used_js_heap_size = Column(BigInteger) + + # ISSUES AND EVENTS + js_exceptions_count = Column(BigInteger) + long_tasks_total_duration = Column(BigInteger) + long_tasks_max_duration = Column(BigInteger) + long_tasks_count = Column(BigInteger) + inputs_count = Column(BigInteger) + clicks_count = Column(BigInteger) + issues_count = Column(BigInteger) + issues = ARRAY(VARCHAR(5000)) + urls_count = Column(BigInteger) + urls = ARRAY(VARCHAR(5000)) + + +class Event(Base): + __tablename__ = events_table_name + + sessionid = Column(BigInteger, primary_key=True) + connectioninformation_downlink = Column(BigInteger) + connectioninformation_type = Column(VARCHAR(5000)) + consolelog_level = Column(VARCHAR(5000)) + consolelog_value = Column(VARCHAR(5000)) + customevent_messageid = Column(BigInteger) + customevent_name = Column(VARCHAR(5000)) + customevent_payload = Column(VARCHAR(5000)) + customevent_timestamp = Column(BigInteger) + errorevent_message = Column(VARCHAR(5000)) + errorevent_messageid = Column(BigInteger) + errorevent_name = Column(VARCHAR(5000)) + errorevent_payload = Column(VARCHAR(5000)) + errorevent_source = Column(VARCHAR(5000)) + errorevent_timestamp = Column(BigInteger) + jsexception_message = Column(VARCHAR(5000)) + jsexception_name = Column(VARCHAR(5000)) + jsexception_payload = Column(VARCHAR(5000)) + metadata_key = Column(VARCHAR(5000)) + metadata_value = Column(VARCHAR(5000)) + mouseclick_id = Column(BigInteger) + mouseclick_hesitationtime = Column(BigInteger) + mouseclick_label = Column(VARCHAR(5000)) + pageevent_firstcontentfulpaint = Column(BigInteger) + pageevent_firstpaint = Column(BigInteger) + pageevent_messageid = Column(BigInteger) + pageevent_referrer = Column(VARCHAR(5000)) + pageevent_speedindex = Column(BigInteger) + pageevent_timestamp = Column(BigInteger) + pageevent_url = Column(VARCHAR(5000)) + pagerendertiming_timetointeractive = Column(BigInteger) + pagerendertiming_visuallycomplete = Column(BigInteger) + rawcustomevent_name = Column(VARCHAR(5000)) + rawcustomevent_payload = Column(VARCHAR(5000)) + setviewportsize_height = Column(BigInteger) + setviewportsize_width = Column(BigInteger) + timestamp_timestamp = Column(BigInteger) + user_anonymous_id = Column(VARCHAR(5000)) + user_id = Column(VARCHAR(5000)) + issueevent_messageid = Column(BigInteger) + issueevent_timestamp = Column(BigInteger) + issueevent_type = Column(VARCHAR(5000)) + issueevent_contextstring = Column(VARCHAR(5000)) + issueevent_context = Column(VARCHAR(5000)) + issueevent_payload = Column(VARCHAR(5000)) + customissue_name = Column(VARCHAR(5000)) + customissue_payload = Column(VARCHAR(5000)) + received_at = Column(BigInteger) + batch_order_number = Column(BigInteger) + + +class DetailedEvent(Base): + __tablename__ = events_detailed_table_name + + # id = Column(Integer, primary_key=True, server_default=text("\"identity\"(119029, 0, '0,1'::text)")) + sessionid = Column(BigInteger, primary_key=True) + clickevent_hesitationtime = Column(BigInteger) + clickevent_label = Column(VARCHAR(5000)) + clickevent_messageid = Column(BigInteger) + clickevent_timestamp = Column(BigInteger) + connectioninformation_downlink = Column(BigInteger) + connectioninformation_type = Column(VARCHAR(5000)) + consolelog_level = Column(VARCHAR(5000)) + consolelog_value = Column(VARCHAR(5000)) + cpuissue_duration = Column(BigInteger) + cpuissue_rate = Column(BigInteger) + cpuissue_timestamp = Column(BigInteger) + createdocument = Column(Boolean) + createelementnode_id = Column(BigInteger) + createelementnode_parentid = Column(BigInteger) + cssdeleterule_index = Column(BigInteger) + cssdeleterule_stylesheetid = Column(BigInteger) + cssinsertrule_index = Column(BigInteger) + cssinsertrule_rule = Column(VARCHAR(5000)) + cssinsertrule_stylesheetid = Column(BigInteger) + customevent_messageid = Column(BigInteger) + customevent_name = Column(VARCHAR(5000)) + customevent_payload = Column(VARCHAR(5000)) + customevent_timestamp = Column(BigInteger) + domdrop_timestamp = Column(BigInteger) + errorevent_message = Column(VARCHAR(5000)) + errorevent_messageid = Column(BigInteger) + errorevent_name = Column(VARCHAR(5000)) + errorevent_payload = Column(VARCHAR(5000)) + errorevent_source = Column(VARCHAR(5000)) + errorevent_timestamp = Column(BigInteger) + fetch_duration = Column(BigInteger) + fetch_method = Column(VARCHAR(5000)) + fetch_request = Column(VARCHAR(5000)) + fetch_response = Column(VARCHAR(5000)) + fetch_status = Column(BigInteger) + fetch_timestamp = Column(BigInteger) + fetch_url = Column(VARCHAR(5000)) + graphql_operationkind = Column(VARCHAR(5000)) + graphql_operationname = Column(VARCHAR(5000)) + graphql_response = Column(VARCHAR(5000)) + graphql_variables = Column(VARCHAR(5000)) + graphqlevent_messageid = Column(BigInteger) + graphqlevent_name = Column(VARCHAR(5000)) + graphqlevent_timestamp = Column(BigInteger) + inputevent_label = Column(VARCHAR(5000)) + inputevent_messageid = Column(BigInteger) + inputevent_timestamp = Column(BigInteger) + inputevent_value = Column(VARCHAR(5000)) + inputevent_valuemasked = Column(Boolean) + jsexception_message = Column(VARCHAR(5000)) + jsexception_name = Column(VARCHAR(5000)) + jsexception_payload = Column(VARCHAR(5000)) + memoryissue_duration = Column(BigInteger) + memoryissue_rate = Column(BigInteger) + memoryissue_timestamp = Column(BigInteger) + metadata_key = Column(VARCHAR(5000)) + metadata_value = Column(VARCHAR(5000)) + mobx_payload = Column(VARCHAR(5000)) + mobx_type = Column(VARCHAR(5000)) + mouseclick_id = Column(BigInteger) + mouseclick_hesitationtime = Column(BigInteger) + mouseclick_label = Column(VARCHAR(5000)) + mousemove_x = Column(BigInteger) + mousemove_y = Column(BigInteger) + movenode_id = Column(BigInteger) + movenode_index = Column(BigInteger) + movenode_parentid = Column(BigInteger) + ngrx_action = Column(VARCHAR(5000)) + ngrx_duration = Column(BigInteger) + ngrx_state = Column(VARCHAR(5000)) + otable_key = Column(VARCHAR(5000)) + otable_value = Column(VARCHAR(5000)) + pageevent_domcontentloadedeventend = Column(BigInteger) + pageevent_domcontentloadedeventstart = Column(BigInteger) + pageevent_firstcontentfulpaint = Column(BigInteger) + pageevent_firstpaint = Column(BigInteger) + pageevent_loaded = Column(Boolean) + pageevent_loadeventend = Column(BigInteger) + pageevent_loadeventstart = Column(BigInteger) + pageevent_messageid = Column(BigInteger) + pageevent_referrer = Column(VARCHAR(5000)) + pageevent_requeststart = Column(BigInteger) + pageevent_responseend = Column(BigInteger) + pageevent_responsestart = Column(BigInteger) + pageevent_speedindex = Column(BigInteger) + pageevent_timestamp = Column(BigInteger) + pageevent_url = Column(VARCHAR(5000)) + pageloadtiming_domcontentloadedeventend = Column(BigInteger) + pageloadtiming_domcontentloadedeventstart = Column(BigInteger) + pageloadtiming_firstcontentfulpaint = Column(BigInteger) + pageloadtiming_firstpaint = Column(BigInteger) + pageloadtiming_loadeventend = Column(BigInteger) + pageloadtiming_loadeventstart = Column(BigInteger) + pageloadtiming_requeststart = Column(BigInteger) + pageloadtiming_responseend = Column(BigInteger) + pageloadtiming_responsestart = Column(BigInteger) + pagerendertiming_speedindex = Column(BigInteger) + pagerendertiming_timetointeractive = Column(BigInteger) + pagerendertiming_visuallycomplete = Column(BigInteger) + performancetrack_frames = Column(BigInteger) + performancetrack_ticks = Column(BigInteger) + performancetrack_totaljsheapsize = Column(BigInteger) + performancetrack_usedjsheapsize = Column(BigInteger) + performancetrackaggr_avgcpu = Column(BigInteger) + performancetrackaggr_avgfps = Column(BigInteger) + performancetrackaggr_avgtotaljsheapsize = Column(BigInteger) + performancetrackaggr_avgusedjsheapsize = Column(BigInteger) + performancetrackaggr_maxcpu = Column(BigInteger) + performancetrackaggr_maxfps = Column(BigInteger) + performancetrackaggr_maxtotaljsheapsize = Column(BigInteger) + performancetrackaggr_maxusedjsheapsize = Column(BigInteger) + performancetrackaggr_mincpu = Column(BigInteger) + performancetrackaggr_minfps = Column(BigInteger) + performancetrackaggr_mintotaljsheapsize = Column(BigInteger) + performancetrackaggr_minusedjsheapsize = Column(BigInteger) + performancetrackaggr_timestampend = Column(BigInteger) + performancetrackaggr_timestampstart = Column(BigInteger) + profiler_args = Column(VARCHAR(5000)) + profiler_duration = Column(BigInteger) + profiler_name = Column(VARCHAR(5000)) + profiler_result = Column(VARCHAR(5000)) + rawcustomevent_name = Column(VARCHAR(5000)) + rawcustomevent_payload = Column(VARCHAR(5000)) + rawerrorevent_message = Column(VARCHAR(5000)) + rawerrorevent_name = Column(VARCHAR(5000)) + rawerrorevent_payload = Column(VARCHAR(5000)) + rawerrorevent_source = Column(VARCHAR(5000)) + rawerrorevent_timestamp = Column(BigInteger) + redux_action = Column(VARCHAR(5000)) + redux_duration = Column(BigInteger) + redux_state = Column(VARCHAR(5000)) + removenode_id = Column(BigInteger) + removenodeattribute_id = Column(BigInteger) + removenodeattribute_name = Column(VARCHAR(5000)) + resourceevent_decodedbodysize = Column(BigInteger) + resourceevent_duration = Column(BigInteger) + resourceevent_encodedbodysize = Column(BigInteger) + resourceevent_headersize = Column(BigInteger) + resourceevent_messageid = Column(BigInteger) + resourceevent_method = Column(VARCHAR(5000)) + resourceevent_status = Column(BigInteger) + resourceevent_success = Column(Boolean) + resourceevent_timestamp = Column(BigInteger) + resourceevent_ttfb = Column(BigInteger) + resourceevent_type = Column(VARCHAR(5000)) + resourceevent_url = Column(VARCHAR(5000)) + resourcetiming_decodedbodysize = Column(BigInteger) + resourcetiming_duration = Column(BigInteger) + resourcetiming_encodedbodysize = Column(BigInteger) + resourcetiming_headersize = Column(BigInteger) + resourcetiming_initiator = Column(VARCHAR(5000)) + resourcetiming_timestamp = Column(BigInteger) + resourcetiming_ttfb = Column(BigInteger) + resourcetiming_url = Column(VARCHAR(5000)) + sessiondisconnect = Column(Boolean) + sessiondisconnect_timestamp = Column(BigInteger) + sessionend = Column(Boolean) + sessionend_timestamp = Column(BigInteger) + sessionstart_projectid = Column(BigInteger) + sessionstart_revid = Column(VARCHAR(5000)) + sessionstart_timestamp = Column(BigInteger) + sessionstart_trackerversion = Column(VARCHAR(5000)) + sessionstart_useragent = Column(VARCHAR(5000)) + sessionstart_userbrowser = Column(VARCHAR(5000)) + sessionstart_userbrowserversion = Column(VARCHAR(5000)) + sessionstart_usercountry = Column(VARCHAR(5000)) + sessionstart_userdevice = Column(VARCHAR(5000)) + sessionstart_userdeviceheapsize = Column(BigInteger) + sessionstart_userdevicememorysize = Column(BigInteger) + sessionstart_userdevicetype = Column(VARCHAR(5000)) + sessionstart_useros = Column(VARCHAR(5000)) + sessionstart_userosversion = Column(VARCHAR(5000)) + sessionstart_useruuid = Column(VARCHAR(5000)) + setcssdata_data = Column(BigInteger) + setcssdata_id = Column(BigInteger) + setinputchecked_checked = Column(BigInteger) + setinputchecked_id = Column(BigInteger) + setinputtarget_id = Column(BigInteger) + setinputtarget_label = Column(BigInteger) + setinputvalue_id = Column(BigInteger) + setinputvalue_mask = Column(BigInteger) + setinputvalue_value = Column(BigInteger) + setnodeattribute_id = Column(BigInteger) + setnodeattribute_name = Column(BigInteger) + setnodeattribute_value = Column(BigInteger) + setnodedata_data = Column(BigInteger) + setnodedata_id = Column(BigInteger) + setnodescroll_id = Column(BigInteger) + setnodescroll_x = Column(BigInteger) + setnodescroll_y = Column(BigInteger) + setpagelocation_navigationstart = Column(BigInteger) + setpagelocation_referrer = Column(VARCHAR(5000)) + setpagelocation_url = Column(VARCHAR(5000)) + setpagevisibility_hidden = Column(Boolean) + setviewportscroll_x = Column(BigInteger) + setviewportscroll_y = Column(BigInteger) + setviewportsize_height = Column(BigInteger) + setviewportsize_width = Column(BigInteger) + stateaction_type = Column(VARCHAR(5000)) + stateactionevent_messageid = Column(BigInteger) + stateactionevent_timestamp = Column(BigInteger) + stateactionevent_type = Column(VARCHAR(5000)) + timestamp_timestamp = Column(BigInteger) + useranonymousid_id = Column(VARCHAR(5000)) + userid_id = Column(VARCHAR(5000)) + vuex_mutation = Column(VARCHAR(5000)) + vuex_state = Column(VARCHAR(5000)) + longtask_timestamp = Column(BigInteger) + longtask_duration = Column(BigInteger) + longtask_context = Column(BigInteger) + longtask_containertype = Column(BigInteger) + longtask_containersrc = Column(VARCHAR(5000)) + longtask_containerid = Column(VARCHAR(5000)) + longtask_containername = Column(VARCHAR(5000)) + setnodeurlbasedattribute_id = Column(BigInteger) + setnodeurlbasedattribute_name = Column(VARCHAR(5000)) + setnodeurlbasedattribute_value = Column(VARCHAR(5000)) + setnodeurlbasedattribute_baseurl = Column(VARCHAR(5000)) + setstyledata_id = Column(BigInteger) + setstyledata_data = Column(VARCHAR(5000)) + setstyledata_baseurl = Column(VARCHAR(5000)) + issueevent_messageid = Column(BigInteger) + issueevent_timestamp = Column(BigInteger) + issueevent_type = Column(VARCHAR(5000)) + issueevent_contextstring = Column(VARCHAR(5000)) + issueevent_context = Column(VARCHAR(5000)) + issueevent_payload = Column(VARCHAR(5000)) + technicalinfo_type = Column(VARCHAR(5000)) + technicalinfo_value = Column(VARCHAR(5000)) + customissue_name = Column(VARCHAR(5000)) + customissue_payload = Column(VARCHAR(5000)) + pageclose = Column(Boolean) + received_at = Column(BigInteger) + batch_order_number = Column(BigInteger) diff --git a/ee/connectors/db/tables.py b/ee/connectors/db/tables.py new file mode 100644 index 000000000..0127cbbd1 --- /dev/null +++ b/ee/connectors/db/tables.py @@ -0,0 +1,61 @@ +from pathlib import Path + +base_path = Path(__file__).parent.parent + + +def create_tables_clickhouse(db): + with open(base_path / 'sql' / 'clickhouse_events.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_user_events` table created succesfully.") + + with open(base_path / 'sql' / 'clickhouse_events_buffer.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_user_events_buffer` table created succesfully.") + + with open(base_path / 'sql' / 'clickhouse_sessions.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_sessions` table created succesfully.") + + with open(base_path / 'sql' / 'clickhouse_sessions_buffer.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_sessions_buffer` table created succesfully.") + + +def create_tables_postgres(db): + with open(base_path / 'sql' / 'postgres_events.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_user_events` table created succesfully.") + + with open(base_path / 'sql' / 'postgres_sessions.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_sessions` table created succesfully.") + + +def create_tables_snowflake(db): + with open(base_path / 'sql' / 'snowflake_events.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_user_events` table created succesfully.") + + with open(base_path / 'sql' / 'snowflake_sessions.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_sessions` table created succesfully.") + + +def create_tables_redshift(db): + with open(base_path / 'sql' / 'redshift_events.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_user_events` table created succesfully.") + + with open(base_path / 'sql' / 'redshift_sessions.sql') as f: + q = f.read() + db.engine.execute(q) + print(f"`connector_sessions` table created succesfully.") diff --git a/ee/connectors/db/utils.py b/ee/connectors/db/utils.py new file mode 100644 index 000000000..7c268c6b3 --- /dev/null +++ b/ee/connectors/db/utils.py @@ -0,0 +1,368 @@ +import pandas as pd +from db.models import DetailedEvent, Event, Session, DATABASE + +dtypes_events = {'sessionid': "Int64", + 'connectioninformation_downlink': "Int64", + 'connectioninformation_type': "string", + 'consolelog_level': "string", + 'consolelog_value': "string", + 'customevent_messageid': "Int64", + 'customevent_name': "string", + 'customevent_payload': "string", + 'customevent_timestamp': "Int64", + 'errorevent_message': "string", + 'errorevent_messageid': "Int64", + 'errorevent_name': "string", + 'errorevent_payload': "string", + 'errorevent_source': "string", + 'errorevent_timestamp': "Int64", + 'jsexception_message': "string", + 'jsexception_name': "string", + 'jsexception_payload': "string", + 'metadata_key': "string", + 'metadata_value': "string", + 'mouseclick_id': "Int64", + 'mouseclick_hesitationtime': "Int64", + 'mouseclick_label': "string", + 'pageevent_firstcontentfulpaint': "Int64", + 'pageevent_firstpaint': "Int64", + 'pageevent_messageid': "Int64", + 'pageevent_referrer': "string", + 'pageevent_speedindex': "Int64", + 'pageevent_timestamp': "Int64", + 'pageevent_url': "string", + 'pagerendertiming_timetointeractive': "Int64", + 'pagerendertiming_visuallycomplete': "Int64", + 'rawcustomevent_name': "string", + 'rawcustomevent_payload': "string", + 'setviewportsize_height': "Int64", + 'setviewportsize_width': "Int64", + 'timestamp_timestamp': "Int64", + 'user_anonymous_id': "string", + 'user_id': "string", + 'issueevent_messageid': "Int64", + 'issueevent_timestamp': "Int64", + 'issueevent_type': "string", + 'issueevent_contextstring': "string", + 'issueevent_context': "string", + 'issueevent_payload': "string", + 'customissue_name': "string", + 'customissue_payload': "string", + 'received_at': "Int64", + 'batch_order_number': "Int64"} +dtypes_detailed_events = { + "sessionid": "Int64", + "clickevent_hesitationtime": "Int64", + "clickevent_label": "object", + "clickevent_messageid": "Int64", + "clickevent_timestamp": "Int64", + "connectioninformation_downlink": "Int64", + "connectioninformation_type": "object", + "consolelog_level": "object", + "consolelog_value": "object", + "cpuissue_duration": "Int64", + "cpuissue_rate": "Int64", + "cpuissue_timestamp": "Int64", + "createdocument": "boolean", + "createelementnode_id": "Int64", + "createelementnode_parentid": "Int64", + "cssdeleterule_index": "Int64", + "cssdeleterule_stylesheetid": "Int64", + "cssinsertrule_index": "Int64", + "cssinsertrule_rule": "object", + "cssinsertrule_stylesheetid": "Int64", + "customevent_messageid": "Int64", + "customevent_name": "object", + "customevent_payload": "object", + "customevent_timestamp": "Int64", + "domdrop_timestamp": "Int64", + "errorevent_message": "object", + "errorevent_messageid": "Int64", + "errorevent_name": "object", + "errorevent_payload": "object", + "errorevent_source": "object", + "errorevent_timestamp": "Int64", + "fetch_duration": "Int64", + "fetch_method": "object", + "fetch_request": "object", + "fetch_response": "object", + "fetch_status": "Int64", + "fetch_timestamp": "Int64", + "fetch_url": "object", + "graphql_operationkind": "object", + "graphql_operationname": "object", + "graphql_response": "object", + "graphql_variables": "object", + "graphqlevent_messageid": "Int64", + "graphqlevent_name": "object", + "graphqlevent_timestamp": "Int64", + "inputevent_label": "object", + "inputevent_messageid": "Int64", + "inputevent_timestamp": "Int64", + "inputevent_value": "object", + "inputevent_valuemasked": "boolean", + "jsexception_message": "object", + "jsexception_name": "object", + "jsexception_payload": "object", + "longtasks_timestamp": "Int64", + "longtasks_duration": "Int64", + "longtasks_containerid": "object", + "longtasks_containersrc": "object", + "memoryissue_duration": "Int64", + "memoryissue_rate": "Int64", + "memoryissue_timestamp": "Int64", + "metadata_key": "object", + "metadata_value": "object", + "mobx_payload": "object", + "mobx_type": "object", + "mouseclick_id": "Int64", + "mouseclick_hesitationtime": "Int64", + "mouseclick_label": "object", + "mousemove_x": "Int64", + "mousemove_y": "Int64", + "movenode_id": "Int64", + "movenode_index": "Int64", + "movenode_parentid": "Int64", + "ngrx_action": "object", + "ngrx_duration": "Int64", + "ngrx_state": "object", + "otable_key": "object", + "otable_value": "object", + "pageevent_domcontentloadedeventend": "Int64", + "pageevent_domcontentloadedeventstart": "Int64", + "pageevent_firstcontentfulpaint": "Int64", + "pageevent_firstpaint": "Int64", + "pageevent_loaded": "boolean", + "pageevent_loadeventend": "Int64", + "pageevent_loadeventstart": "Int64", + "pageevent_messageid": "Int64", + "pageevent_referrer": "object", + "pageevent_requeststart": "Int64", + "pageevent_responseend": "Int64", + "pageevent_responsestart": "Int64", + "pageevent_speedindex": "Int64", + "pageevent_timestamp": "Int64", + "pageevent_url": "object", + "pageloadtiming_domcontentloadedeventend": "Int64", + "pageloadtiming_domcontentloadedeventstart": "Int64", + "pageloadtiming_firstcontentfulpaint": "Int64", + "pageloadtiming_firstpaint": "Int64", + "pageloadtiming_loadeventend": "Int64", + "pageloadtiming_loadeventstart": "Int64", + "pageloadtiming_requeststart": "Int64", + "pageloadtiming_responseend": "Int64", + "pageloadtiming_responsestart": "Int64", + "pagerendertiming_speedindex": "Int64", + "pagerendertiming_timetointeractive": "Int64", + "pagerendertiming_visuallycomplete": "Int64", + "performancetrack_frames": "Int64", + "performancetrack_ticks": "Int64", + "performancetrack_totaljsheapsize": "Int64", + "performancetrack_usedjsheapsize": "Int64", + "performancetrackaggr_avgcpu": "Int64", + "performancetrackaggr_avgfps": "Int64", + "performancetrackaggr_avgtotaljsheapsize": "Int64", + "performancetrackaggr_avgusedjsheapsize": "Int64", + "performancetrackaggr_maxcpu": "Int64", + "performancetrackaggr_maxfps": "Int64", + "performancetrackaggr_maxtotaljsheapsize": "Int64", + "performancetrackaggr_maxusedjsheapsize": "Int64", + "performancetrackaggr_mincpu": "Int64", + "performancetrackaggr_minfps": "Int64", + "performancetrackaggr_mintotaljsheapsize": "Int64", + "performancetrackaggr_minusedjsheapsize": "Int64", + "performancetrackaggr_timestampend": "Int64", + "performancetrackaggr_timestampstart": "Int64", + "profiler_args": "object", + "profiler_duration": "Int64", + "profiler_name": "object", + "profiler_result": "object", + "rawcustomevent_name": "object", + "rawcustomevent_payload": "object", + "rawerrorevent_message": "object", + "rawerrorevent_name": "object", + "rawerrorevent_payload": "object", + "rawerrorevent_source": "object", + "rawerrorevent_timestamp": "Int64", + "redux_action": "object", + "redux_duration": "Int64", + "redux_state": "object", + "removenode_id": "Int64", + "removenodeattribute_id": "Int64", + "removenodeattribute_name": "object", + "resourceevent_decodedbodysize": "Int64", + "resourceevent_duration": "Int64", + "resourceevent_encodedbodysize": "Int64", + "resourceevent_headersize": "Int64", + "resourceevent_messageid": "Int64", + "resourceevent_method": "object", + "resourceevent_status": "Int64", + "resourceevent_success": "boolean", + "resourceevent_timestamp": "Int64", + "resourceevent_ttfb": "Int64", + "resourceevent_type": "object", + "resourceevent_url": "object", + "resourcetiming_decodedbodysize": "Int64", + "resourcetiming_duration": "Int64", + "resourcetiming_encodedbodysize": "Int64", + "resourcetiming_headersize": "Int64", + "resourcetiming_initiator": "object", + "resourcetiming_timestamp": "Int64", + "resourcetiming_ttfb": "Int64", + "resourcetiming_url": "object", + "sessiondisconnect": "boolean", + "sessiondisconnect_timestamp": "Int64", + "sessionend": "boolean", + "sessionend_timestamp": "Int64", + "sessionstart_projectid": "Int64", + "sessionstart_revid": "object", + "sessionstart_timestamp": "Int64", + "sessionstart_trackerversion": "object", + "sessionstart_useragent": "object", + "sessionstart_userbrowser": "object", + "sessionstart_userbrowserversion": "object", + "sessionstart_usercountry": "object", + "sessionstart_userdevice": "object", + "sessionstart_userdeviceheapsize": "Int64", + "sessionstart_userdevicememorysize": "Int64", + "sessionstart_userdevicetype": "object", + "sessionstart_useros": "object", + "sessionstart_userosversion": "object", + "sessionstart_useruuid": "object", + "setcssdata_data": "Int64", + "setcssdata_id": "Int64", + "setinputchecked_checked": "Int64", + "setinputchecked_id": "Int64", + "setinputtarget_id": "Int64", + "setinputtarget_label": "Int64", + "setinputvalue_id": "Int64", + "setinputvalue_mask": "Int64", + "setinputvalue_value": "Int64", + "setnodeattribute_id": "Int64", + "setnodeattribute_name": "Int64", + "setnodeattribute_value": "Int64", + "setnodedata_data": "Int64", + "setnodedata_id": "Int64", + "setnodescroll_id": "Int64", + "setnodescroll_x": "Int64", + "setnodescroll_y": "Int64", + "setpagelocation_navigationstart": "Int64", + "setpagelocation_referrer": "object", + "setpagelocation_url": "object", + "setpagevisibility_hidden": "boolean", + "setviewportscroll_x": "Int64", + "setviewportscroll_y": "Int64", + "setviewportsize_height": "Int64", + "setviewportsize_width": "Int64", + "stateaction_type": "object", + "stateactionevent_messageid": "Int64", + "stateactionevent_timestamp": "Int64", + "stateactionevent_type": "object", + "timestamp_timestamp": "Int64", + "useranonymousid_id": "object", + "userid_id": "object", + "vuex_mutation": "object", + "vuex_state": "string", + "received_at": "Int64", + "batch_order_number": "Int64" +} +dtypes_sessions = {'sessionid': 'Int64', + 'user_agent': 'string', + 'user_browser': 'string', + 'user_browser_version': 'string', + 'user_country': 'string', + 'user_device': 'string', + 'user_device_heap_size': 'Int64', + 'user_device_memory_size': 'Int64', + 'user_device_type': 'string', + 'user_os': 'string', + 'user_os_version': 'string', + 'user_uuid': 'string', + 'connection_effective_bandwidth': 'Int64', + 'connection_type': 'string', + 'metadata_key': 'string', + 'metadata_value': 'string', + 'referrer': 'string', + 'user_anonymous_id': 'string', + 'user_id': 'string', + 'session_start_timestamp': 'Int64', + 'session_end_timestamp': 'Int64', + 'session_duration': 'Int64', + 'first_contentful_paint': 'Int64', + 'speed_index': 'Int64', + 'visually_complete': 'Int64', + 'timing_time_to_interactive': 'Int64', + 'avg_cpu': 'Int64', + 'avg_fps': 'Int64', + 'max_cpu': 'Int64', + 'max_fps': 'Int64', + 'max_total_js_heap_size': 'Int64', + 'max_used_js_heap_size': 'Int64', + 'js_exceptions_count': 'Int64', + 'long_tasks_total_duration': 'Int64', + 'long_tasks_max_duration': 'Int64', + 'long_tasks_count': 'Int64', + 'inputs_count': 'Int64', + 'clicks_count': 'Int64', + 'issues_count': 'Int64', + 'issues': 'object', + 'urls_count': 'Int64', + 'urls': 'object'} + +if DATABASE == 'bigquery': + dtypes_sessions['urls'] = 'string' + dtypes_sessions['issues'] = 'string' + +detailed_events_col = [] +for col in DetailedEvent.__dict__: + if not col.startswith('_'): + detailed_events_col.append(col) + +events_col = [] +for col in Event.__dict__: + if not col.startswith('_'): + events_col.append(col) + +sessions_col = [] +for col in Session.__dict__: + if not col.startswith('_'): + sessions_col.append(col) + + +def get_df_from_batch(batch, level): + if level == 'normal': + df = pd.DataFrame([b.__dict__ for b in batch], columns=events_col) + if level == 'detailed': + df = pd.DataFrame([b.__dict__ for b in batch], columns=detailed_events_col) + if level == 'sessions': + df = pd.DataFrame([b.__dict__ for b in batch], columns=sessions_col) + + try: + df = df.drop('_sa_instance_state', axis=1) + except KeyError: + pass + + if level == 'normal': + df = df.astype(dtypes_events) + if level == 'detailed': + df['inputevent_value'] = None + df['customevent_payload'] = None + df = df.astype(dtypes_detailed_events) + if level == 'sessions': + df = df.astype(dtypes_sessions) + + if DATABASE == 'clickhouse' and level == 'sessions': + df['issues'] = df['issues'].fillna('') + df['urls'] = df['urls'].fillna('') + + for x in df.columns: + try: + if df[x].dtype == 'string': + df[x] = df[x].str.slice(0, 255) + df[x] = df[x].str.replace("|", "") + except TypeError as e: + print(repr(e)) + if df[x].dtype == 'str': + df[x] = df[x].str.slice(0, 255) + df[x] = df[x].str.replace("|", "") + return df diff --git a/ee/connectors/db/writer.py b/ee/connectors/db/writer.py new file mode 100644 index 000000000..b999b773f --- /dev/null +++ b/ee/connectors/db/writer.py @@ -0,0 +1,63 @@ +import os +DATABASE = os.environ['DATABASE_NAME'] + +from db.api import DBConnection +from db.utils import get_df_from_batch +from db.tables import * + +if DATABASE == 'redshift': + from db.loaders.redshift_loader import transit_insert_to_redshift +if DATABASE == 'clickhouse': + from db.loaders.clickhouse_loader import insert_to_clickhouse +if DATABASE == 'pg': + from db.loaders.postgres_loader import insert_to_postgres +if DATABASE == 'bigquery': + from db.loaders.bigquery_loader import insert_to_bigquery + from bigquery_utils.create_table import create_tables_bigquery +if DATABASE == 'snowflake': + from db.loaders.snowflake_loader import insert_to_snowflake + + +# create tables if don't exist +try: + db = DBConnection(DATABASE) + if DATABASE == 'pg': + create_tables_postgres(db) + if DATABASE == 'clickhouse': + create_tables_clickhouse(db) + if DATABASE == 'snowflake': + create_tables_snowflake(db) + if DATABASE == 'bigquery': + create_tables_bigquery() + if DATABASE == 'redshift': + create_tables_redshift(db) + db.engine.dispose() + db = None +except Exception as e: + print(repr(e)) + print("Please create the tables with scripts provided in " + "'/sql/{DATABASE}_sessions.sql' and '/sql/{DATABASE}_events.sql'") + + +def insert_batch(db: DBConnection, batch, table, level='normal'): + + if len(batch) == 0: + return + df = get_df_from_batch(batch, level=level) + + if db.config == 'redshift': + transit_insert_to_redshift(db=db, df=df, table=table) + return + + if db.config == 'clickhouse': + insert_to_clickhouse(db=db, df=df, table=table) + + if db.config == 'pg': + insert_to_postgres(db=db, df=df, table=table) + + if db.config == 'bigquery': + insert_to_bigquery(df=df, table=table) + + if db.config == 'snowflake': + insert_to_snowflake(db=db, df=df, table=table) + diff --git a/ee/connectors/handler.py b/ee/connectors/handler.py new file mode 100644 index 000000000..5167c7800 --- /dev/null +++ b/ee/connectors/handler.py @@ -0,0 +1,647 @@ +from typing import Optional, Union + +from db.models import Event, DetailedEvent, Session +from msgcodec.messages import * + + +def handle_normal_message(message: Message) -> Optional[Event]: + + n = Event() + + if isinstance(message, ConnectionInformation): + n.connectioninformation_downlink = message.downlink + n.connectioninformation_type = message.type + return n + + if isinstance(message, ConsoleLog): + n.consolelog_level = message.level + n.consolelog_value = message.value + return n + + if isinstance(message, CustomEvent): + n.customevent_messageid = message.message_id + n.customevent_name = message.name + n.customevent_timestamp = message.timestamp + n.customevent_payload = message.payload + return n + + if isinstance(message, ErrorEvent): + n.errorevent_message = message.message + n.errorevent_messageid = message.message_id + n.errorevent_name = message.name + n.errorevent_payload = message.payload + n.errorevent_source = message.source + n.errorevent_timestamp = message.timestamp + return n + + if isinstance(message, JSException): + n.jsexception_name = message.name + n.jsexception_payload = message.payload + n.jsexception_message = message.message + return n + + if isinstance(message, Metadata): + n.metadata_key = message.key + n.metadata_value = message.value + return n + + if isinstance(message, MouseClick): + n.mouseclick_hesitationtime = message.hesitation_time + n.mouseclick_id = message.id + n.mouseclick_label = message.label + return n + + if isinstance(message, PageEvent): + n.pageevent_firstcontentfulpaint = message.first_contentful_paint + n.pageevent_firstpaint = message.first_paint + n.pageevent_messageid = message.message_id + n.pageevent_referrer = message.referrer + n.pageevent_speedindex = message.speed_index + n.pageevent_timestamp = message.timestamp + n.pageevent_url = message.url + return n + + if isinstance(message, PageRenderTiming): + n.pagerendertiming_timetointeractive = message.time_to_interactive + n.pagerendertiming_visuallycomplete = message.visually_complete + return n + + if isinstance(message, RawCustomEvent): + n.rawcustomevent_name = message.name + n.rawcustomevent_payload = message.payload + return n + + if isinstance(message, SetViewportSize): + n.setviewportsize_height = message.height + n.setviewportsize_width = message.width + return n + + if isinstance(message, Timestamp): + n.timestamp_timestamp = message.timestamp + return n + + if isinstance(message, UserAnonymousID): + n.user_anonymous_id = message.id + return n + + if isinstance(message, UserID): + n.user_id = message.id + return n + + if isinstance(message, IssueEvent): + n.issueevent_messageid = message.message_id + n.issueevent_timestamp = message.timestamp + n.issueevent_type = message.type + n.issueevent_contextstring = message.context_string + n.issueevent_context = message.context + n.issueevent_payload = message.payload + return n + + if isinstance(message, CustomIssue): + n.customissue_name = message.name + n.customissue_payload = message.payload + return n + + +def handle_session(n: Session, message: Message) -> Optional[Session]: + + if not n: + n = Session() + + if isinstance(message, SessionStart): + n.session_start_timestamp = message.timestamp + + n.user_uuid = message.user_uuid + n.user_agent = message.user_agent + n.user_os = message.user_os + n.user_os_version = message.user_os_version + n.user_browser = message.user_browser + n.user_browser_version = message.user_browser_version + n.user_device = message.user_device + n.user_device_type = message.user_device_type + n.user_device_memory_size = message.user_device_memory_size + n.user_device_heap_size = message.user_device_heap_size + n.user_country = message.user_country + return n + + if isinstance(message, SessionEnd): + n.session_end_timestamp = message.timestamp + try: + n.session_duration = n.session_end_timestamp - n.session_start_timestamp + except TypeError: + pass + return n + + if isinstance(message, ConnectionInformation): + n.connection_effective_bandwidth = message.downlink + n.connection_type = message.type + return n + + if isinstance(message, Metadata): + n.metadata_key = message.key + n.metadata_value = message.value + return n + + if isinstance(message, PageEvent): + n.referrer = message.referrer + n.first_contentful_paint = message.first_contentful_paint + n.speed_index = message.speed_index + n.timing_time_to_interactive = message.time_to_interactive + n.visually_complete = message.visually_complete + try: + n.urls_count += 1 + except TypeError: + n.urls_count = 1 + try: + n.urls.append(message.url) + except AttributeError: + n.urls = [message.url] + return n + + if isinstance(message, PerformanceTrackAggr): + n.avg_cpu = message.avg_cpu + n.avg_fps = message.avg_fps + n.max_cpu = message.max_cpu + n.max_fps = message.max_fps + n.max_total_js_heap_size = message.max_total_js_heap_size + n.max_used_js_heap_size = message.max_used_js_heap_size + return n + + if isinstance(message, UserID): + n.user_id = message.id + return n + + if isinstance(message, UserAnonymousID): + n.user_anonymous_id = message.id + return n + + if isinstance(message, JSException): + try: + n.js_exceptions_count += 1 + except TypeError: + n.js_exceptions_count = 1 + return n + + if isinstance(message, LongTask): + try: + n.long_tasks_total_duration += message.duration + except TypeError: + n.long_tasks_total_duration = message.duration + + try: + if n.long_tasks_max_duration > message.duration: + n.long_tasks_max_duration = message.duration + except TypeError: + n.long_tasks_max_duration = message.duration + + try: + n.long_tasks_count += 1 + except TypeError: + n.long_tasks_count = 1 + return n + + if isinstance(message, InputEvent): + try: + n.inputs_count += 1 + except TypeError: + n.inputs_count = 1 + return n + + if isinstance(message, MouseClick): + try: + n.inputs_count += 1 + except TypeError: + n.inputs_count = 1 + return n + + if isinstance(message, IssueEvent): + try: + n.issues_count += 1 + except TypeError: + n.issues_count = 1 + + + n.inputs_count = 1 + return n + + if isinstance(message, MouseClick): + try: + n.inputs_count += 1 + except TypeError: + n.inputs_count = 1 + return n + + if isinstance(message, IssueEvent): + try: + n.issues_count += 1 + except TypeError: + n.issues_count = 1 + + try: + n.issues.append(message.type) + except AttributeError: + n.issues = [message.type] + return n + + +def handle_message(message: Message) -> Optional[DetailedEvent]: + n = DetailedEvent() + + if isinstance(message, SessionEnd): + n.sessionend = True + n.sessionend_timestamp = message.timestamp + return n + + if isinstance(message, Timestamp): + n.timestamp_timestamp = message.timestamp + return n + + if isinstance(message, SessionDisconnect): + n.sessiondisconnect = True + n.sessiondisconnect_timestamp = message.timestamp + return n + + if isinstance(message, SessionStart): + n.sessionstart_trackerversion = message.tracker_version + n.sessionstart_revid = message.rev_id + n.sessionstart_timestamp = message.timestamp + n.sessionstart_useruuid = message.user_uuid + n.sessionstart_useragent = message.user_agent + n.sessionstart_useros = message.user_os + n.sessionstart_userosversion = message.user_os_version + n.sessionstart_userbrowser = message.user_browser + n.sessionstart_userbrowserversion = message.user_browser_version + n.sessionstart_userdevice = message.user_device + n.sessionstart_userdevicetype = message.user_device_type + n.sessionstart_userdevicememorysize = message.user_device_memory_size + n.sessionstart_userdeviceheapsize = message.user_device_heap_size + n.sessionstart_usercountry = message.user_country + return n + + if isinstance(message, SetViewportSize): + n.setviewportsize_width = message.width + n.setviewportsize_height = message.height + return n + + if isinstance(message, SetViewportScroll): + n.setviewportscroll_x = message.x + n.setviewportscroll_y = message.y + return n + + if isinstance(message, SetNodeScroll): + n.setnodescroll_id = message.id + n.setnodescroll_x = message.x + n.setnodescroll_y = message.y + return n + + if isinstance(message, ConsoleLog): + n.consolelog_level = message.level + n.consolelog_value = message.value + return n + + if isinstance(message, PageLoadTiming): + n.pageloadtiming_requeststart = message.request_start + n.pageloadtiming_responsestart = message.response_start + n.pageloadtiming_responseend = message.response_end + n.pageloadtiming_domcontentloadedeventstart = message.dom_content_loaded_event_start + n.pageloadtiming_domcontentloadedeventend = message.dom_content_loaded_event_end + n.pageloadtiming_loadeventstart = message.load_event_start + n.pageloadtiming_loadeventend = message.load_event_end + n.pageloadtiming_firstpaint = message.first_paint + n.pageloadtiming_firstcontentfulpaint = message.first_contentful_paint + return n + + if isinstance(message, PageRenderTiming): + n.pagerendertiming_speedindex = message.speed_index + n.pagerendertiming_visuallycomplete = message.visually_complete + n.pagerendertiming_timetointeractive = message.time_to_interactive + return n + + if isinstance(message, ResourceTiming): + n.resourcetiming_timestamp = message.timestamp + n.resourcetiming_duration = message.duration + n.resourcetiming_ttfb = message.ttfb + n.resourcetiming_headersize = message.header_size + n.resourcetiming_encodedbodysize = message.encoded_body_size + n.resourcetiming_decodedbodysize = message.decoded_body_size + n.resourcetiming_url = message.url + n.resourcetiming_initiator = message.initiator + return n + + if isinstance(message, JSException): + n.jsexception_name = message.name + n.jsexception_message = message.message + n.jsexception_payload = message.payload + return n + + if isinstance(message, RawErrorEvent): + n.rawerrorevent_timestamp = message.timestamp + n.rawerrorevent_source = message.source + n.rawerrorevent_name = message.name + n.rawerrorevent_message = message.message + n.rawerrorevent_payload = message.payload + return n + + if isinstance(message, RawCustomEvent): + n.rawcustomevent_name = message.name + n.rawcustomevent_payload = message.payload + return n + + if isinstance(message, UserID): + n.userid_id = message.id + return n + + if isinstance(message, UserAnonymousID): + n.useranonymousid_id = message.id + return n + + if isinstance(message, Metadata): + n.metadata_key = message.key + n.metadata_value = message.value + return n + + if isinstance(message, PerformanceTrack): + n.performancetrack_frames = message.frames + n.performancetrack_ticks = message.ticks + n.performancetrack_totaljsheapsize = message.total_js_heap_size + n.performancetrack_usedjsheapsize = message.used_js_heap_size + return n + + if isinstance(message, PerformanceTrackAggr): + n.performancetrackaggr_timestampstart = message.timestamp_start + n.performancetrackaggr_timestampend = message.timestamp_end + n.performancetrackaggr_minfps = message.min_fps + n.performancetrackaggr_avgfps = message.avg_fps + n.performancetrackaggr_maxfps = message.max_fps + n.performancetrackaggr_mincpu = message.min_cpu + n.performancetrackaggr_avgcpu = message.avg_cpu + n.performancetrackaggr_maxcpu = message.max_cpu + n.performancetrackaggr_mintotaljsheapsize = message.min_total_js_heap_size + n.performancetrackaggr_avgtotaljsheapsize = message.avg_total_js_heap_size + n.performancetrackaggr_maxtotaljsheapsize = message.max_total_js_heap_size + n.performancetrackaggr_minusedjsheapsize = message.min_used_js_heap_size + n.performancetrackaggr_avgusedjsheapsize = message.avg_used_js_heap_size + n.performancetrackaggr_maxusedjsheapsize = message.max_used_js_heap_size + return n + + if isinstance(message, ConnectionInformation): + n.connectioninformation_downlink = message.downlink + n.connectioninformation_type = message.type + return n + + if isinstance(message, PageEvent): + n.pageevent_messageid = message.message_id + n.pageevent_timestamp = message.timestamp + n.pageevent_url = message.url + n.pageevent_referrer = message.referrer + n.pageevent_loaded = message.loaded + n.pageevent_requeststart = message.request_start + n.pageevent_responsestart = message.response_start + n.pageevent_responseend = message.response_end + n.pageevent_domcontentloadedeventstart = message.dom_content_loaded_event_start + n.pageevent_domcontentloadedeventend = message.dom_content_loaded_event_end + n.pageevent_loadeventstart = message.load_event_start + n.pageevent_loadeventend = message.load_event_end + n.pageevent_firstpaint = message.first_paint + n.pageevent_firstcontentfulpaint = message.first_contentful_paint + n.pageevent_speedindex = message.speed_index + return n + + if isinstance(message, InputEvent): + n.inputevent_messageid = message.message_id + n.inputevent_timestamp = message.timestamp + n.inputevent_value = message.value + n.inputevent_valuemasked = message.value_masked + n.inputevent_label = message.label + return n + + if isinstance(message, ClickEvent): + n.clickevent_messageid = message.message_id + n.clickevent_timestamp = message.timestamp + n.clickevent_hesitationtime = message.hesitation_time + n.clickevent_label = message.label + return n + + if isinstance(message, ErrorEvent): + n.errorevent_messageid = message.message_id + n.errorevent_timestamp = message.timestamp + n.errorevent_source = message.source + n.errorevent_name = message.name + n.errorevent_message = message.message + n.errorevent_payload = message.payload + return n + + if isinstance(message, ResourceEvent): + n.resourceevent_messageid = message.message_id + n.resourceevent_timestamp = message.timestamp + n.resourceevent_duration = message.duration + n.resourceevent_ttfb = message.ttfb + n.resourceevent_headersize = message.header_size + n.resourceevent_encodedbodysize = message.encoded_body_size + n.resourceevent_decodedbodysize = message.decoded_body_size + n.resourceevent_url = message.url + n.resourceevent_type = message.type + n.resourceevent_success = message.success + n.resourceevent_method = message.method + n.resourceevent_status = message.status + return n + + if isinstance(message, CustomEvent): + n.customevent_messageid = message.message_id + n.customevent_timestamp = message.timestamp + n.customevent_name = message.name + n.customevent_payload = message.payload + return n + + # if isinstance(message, CreateDocument): + # n.createdocument = True + # return n + # + # if isinstance(message, CreateElementNode): + # n.createelementnode_id = message.id + # if isinstance(message.parent_id, tuple): + # n.createelementnode_parentid = message.parent_id[0] + # else: + # n.createelementnode_parentid = message.parent_id + # return n + + # if isinstance(message, CSSInsertRule): + # n.cssinsertrule_stylesheetid = message.id + # n.cssinsertrule_rule = message.rule + # n.cssinsertrule_index = message.index + # return n + # + # if isinstance(message, CSSDeleteRule): + # n.cssdeleterule_stylesheetid = message.id + # n.cssdeleterule_index = message.index + # return n + + if isinstance(message, Fetch): + n.fetch_method = message.method + n.fetch_url = message.url + n.fetch_request = message.request + n.fetch_status = message.status + n.fetch_timestamp = message.timestamp + n.fetch_duration = message.duration + return n + + if isinstance(message, Profiler): + n.profiler_name = message.name + n.profiler_duration = message.duration + n.profiler_args = message.args + n.profiler_result = message.result + return n + + if isinstance(message, GraphQL): + n.graphql_operationkind = message.operation_kind + n.graphql_operationname = message.operation_name + n.graphql_variables = message.variables + n.graphql_response = message.response + return n + + if isinstance(message, GraphQLEvent): + n.graphqlevent_messageid = message.message_id + n.graphqlevent_timestamp = message.timestamp + n.graphqlevent_name = message.name + return n + + if isinstance(message, DomDrop): + n.domdrop_timestamp = message.timestamp + return n + + if isinstance(message, MouseClick): + n.mouseclick_id = message.id + n.mouseclick_hesitationtime = message.hesitation_time + n.mouseclick_label = message.label + return n + + if isinstance(message, SetPageLocation): + n.setpagelocation_url = message.url + n.setpagelocation_referrer = message.referrer + n.setpagelocation_navigationstart = message.navigation_start + return n + + if isinstance(message, MouseMove): + n.mousemove_x = message.x + n.mousemove_y = message.y + return n + + if isinstance(message, LongTask): + n.longtasks_timestamp = message.timestamp + n.longtasks_duration = message.duration + n.longtask_context = message.context + n.longtask_containertype = message.container_type + n.longtasks_containersrc = message.container_src + n.longtasks_containerid = message.container_id + n.longtasks_containername = message.container_name + return n + + if isinstance(message, SetNodeURLBasedAttribute): + n.setnodeurlbasedattribute_id = message.id + n.setnodeurlbasedattribute_name = message.name + n.setnodeurlbasedattribute_value = message.value + n.setnodeurlbasedattribute_baseurl = message.base_url + return n + + if isinstance(message, SetStyleData): + n.setstyledata_id = message.id + n.setstyledata_data = message.data + n.setstyledata_baseurl = message.base_url + return n + + if isinstance(message, IssueEvent): + n.issueevent_messageid = message.message_id + n.issueevent_timestamp = message.timestamp + n.issueevent_type = message.type + n.issueevent_contextstring = message.context_string + n.issueevent_context = message.context + n.issueevent_payload = message.payload + return n + + if isinstance(message, TechnicalInfo): + n.technicalinfo_type = message.type + n.technicalinfo_value = message.value + return n + + if isinstance(message, CustomIssue): + n.customissue_name = message.name + n.customissue_payload = message.payload + return n + + if isinstance(message, PageClose): + n.pageclose = True + return n + + if isinstance(message, IOSSessionStart): + n.iossessionstart_timestamp = message.timestamp + n.iossessionstart_projectid = message.project_id + n.iossessionstart_trackerversion = message.tracker_version + n.iossessionstart_revid = message.rev_id + n.iossessionstart_useruuid = message.user_uuid + n.iossessionstart_useros = message.user_os + n.iossessionstart_userosversion = message.user_os_version + n.iossessionstart_userdevice = message.user_device + n.iossessionstart_userdevicetype = message.user_device_type + n.iossessionstart_usercountry = message.user_country + return n + + if isinstance(message, IOSSessionEnd): + n.iossessionend_timestamp = message.timestamp + return n + + if isinstance(message, IOSMetadata): + n.iosmetadata_timestamp = message.timestamp + n.iosmetadata_length = message.length + n.iosmetadata_key = message.key + n.iosmetadata_value = message.value + return n + + if isinstance(message, IOSUserID): + n.iosuserid_timestamp = message.timestamp + n.iosuserid_length = message.length + n.iosuserid_value = message.value + return n + + if isinstance(message, IOSUserAnonymousID): + n.iosuseranonymousid_timestamp = message.timestamp + n.iosuseranonymousid_length = message.length + n.iosuseranonymousid_value = message.value + return n + + if isinstance(message, IOSScreenLeave): + n.iosscreenleave_timestamp = message.timestamp + n.iosscreenleave_length = message.length + n.iosscreenleave_title = message.title + n.iosscreenleave_viewname = message.view_name + return n + + if isinstance(message, IOSLog): + n.ioslog_timestamp = message.timestamp + n.ioslog_length = message.length + n.ioslog_severity = message.severity + n.ioslog_content = message.content + return n + + if isinstance(message, IOSInternalError): + n.iosinternalerror_timestamp = message.timestamp + n.iosinternalerror_length = message.length + n.iosinternalerror_content = message.content + return n + + if isinstance(message, IOSPerformanceAggregated): + n.iosperformanceaggregated_timestampstart = message.timestamp_start + n.iosperformanceaggregated_timestampend = message.timestamp_end + n.iosperformanceaggregated_minfps = message.min_fps + n.iosperformanceaggregated_avgfps = message.avg_fps + n.iosperformanceaggregated_maxfps = message.max_fps + n.iosperformanceaggregated_mincpu = message.min_cpu + n.iosperformanceaggregated_avgcpu = message.avg_cpu + n.iosperformanceaggregated_maxcpu = message.max_cpu + n.iosperformanceaggregated_minmemory = message.min_memory + n.iosperformanceaggregated_avgmemory = message.avg_memory + n.iosperformanceaggregated_maxmemory = message.max_memory + n.iosperformanceaggregated_minbattery = message.min_battery + n.iosperformanceaggregated_avgbattery = message.avg_battery + n.iosperformanceaggregated_maxbattery = message.max_battery + return n + return None diff --git a/ee/connectors/main.py b/ee/connectors/main.py new file mode 100644 index 000000000..57349f6e9 --- /dev/null +++ b/ee/connectors/main.py @@ -0,0 +1,121 @@ +import os +from kafka import KafkaConsumer +from datetime import datetime +from collections import defaultdict + +from msgcodec.codec import MessageCodec +from msgcodec.messages import SessionEnd +from db.api import DBConnection +from db.models import events_detailed_table_name, events_table_name, sessions_table_name, conf +from db.writer import insert_batch +from handler import handle_message, handle_normal_message, handle_session + +DATABASE = os.environ['DATABASE_NAME'] +LEVEL = conf[DATABASE]['level'] + +db = DBConnection(DATABASE) + +if LEVEL == 'detailed': + table_name = events_detailed_table_name +elif LEVEL == 'normal': + table_name = events_table_name + + +def main(): + batch_size = 4000 + sessions_batch_size = 400 + batch = [] + sessions = defaultdict(lambda: None) + sessions_batch = [] + + codec = MessageCodec() + consumer = KafkaConsumer(security_protocol="SSL", + bootstrap_servers=[os.environ['KAFKA_SERVER_1'], + os.environ['KAFKA_SERVER_2']], + group_id=f"connector_{DATABASE}", + auto_offset_reset="earliest", + enable_auto_commit=False) + + consumer.subscribe(topics=["events", "messages"]) + print("Kafka consumer subscribed") + for msg in consumer: + message = codec.decode(msg.value) + if message is None: + print('-') + continue + + if LEVEL == 'detailed': + n = handle_message(message) + elif LEVEL == 'normal': + n = handle_normal_message(message) + + session_id = codec.decode_key(msg.key) + sessions[session_id] = handle_session(sessions[session_id], message) + if sessions[session_id]: + sessions[session_id].sessionid = session_id + + # put in a batch for insertion if received a SessionEnd + if isinstance(message, SessionEnd): + if sessions[session_id]: + sessions_batch.append(sessions[session_id]) + + # try to insert sessions + if len(sessions_batch) >= sessions_batch_size: + attempt_session_insert(sessions_batch) + for s in sessions_batch: + try: + del sessions[s.sessionid] + except KeyError as e: + print(repr(e)) + sessions_batch = [] + + if n: + n.sessionid = session_id + n.received_at = int(datetime.now().timestamp() * 1000) + n.batch_order_number = len(batch) + batch.append(n) + else: + continue + + # insert a batch of events + if len(batch) >= batch_size: + attempt_batch_insert(batch) + batch = [] + consumer.commit() + print("sessions in cache:", len(sessions)) + + +def attempt_session_insert(sess_batch): + if sess_batch: + try: + print("inserting sessions...") + insert_batch(db, sess_batch, table=sessions_table_name, level='sessions') + print("inserted sessions succesfully") + except TypeError as e: + print("Type conversion error") + print(repr(e)) + except ValueError as e: + print("Message value could not be processed or inserted correctly") + print(repr(e)) + except Exception as e: + print(repr(e)) + + +def attempt_batch_insert(batch): + # insert a batch + try: + print("inserting...") + insert_batch(db=db, batch=batch, table=table_name, level=LEVEL) + print("inserted succesfully") + except TypeError as e: + print("Type conversion error") + print(repr(e)) + except ValueError as e: + print("Message value could not be processed or inserted correctly") + print(repr(e)) + except Exception as e: + print(repr(e)) + + +if __name__ == '__main__': + main() diff --git a/ee/connectors/msgcodec/codec.py b/ee/connectors/msgcodec/codec.py new file mode 100644 index 000000000..18f074a33 --- /dev/null +++ b/ee/connectors/msgcodec/codec.py @@ -0,0 +1,670 @@ +import io + +from msgcodec.messages import * + + +class Codec: + """ + Implements encode/decode primitives + """ + + @staticmethod + def read_boolean(reader: io.BytesIO): + b = reader.read(1) + return b == 1 + + @staticmethod + def read_uint(reader: io.BytesIO): + """ + The ending "big" doesn't play any role here, + since we're dealing with data per one byte + """ + x = 0 # the result + s = 0 # the shift (our result is big-ending) + i = 0 # n of byte (max 9 for uint64) + while True: + b = reader.read(1) + num = int.from_bytes(b, "big", signed=False) + # print(i, x) + + if num < 0x80: + if i > 9 | i == 9 & num > 1: + raise OverflowError() + return int(x | num << s) + x |= (num & 0x7f) << s + s += 7 + i += 1 + + @staticmethod + def read_int(reader: io.BytesIO) -> int: + """ + ux, err := ReadUint(reader) + x := int64(ux >> 1) + if err != nil { + return x, err + } + if ux&1 != 0 { + x = ^x + } + return x, err + """ + ux = Codec.read_uint(reader) + x = int(ux >> 1) + + if ux & 1 != 0: + x = - x - 1 + return x + + @staticmethod + def read_string(reader: io.BytesIO) -> str: + length = Codec.read_uint(reader) + s = reader.read(length) + try: + return s.decode("utf-8", errors="replace").replace("\x00", "\uFFFD") + except UnicodeDecodeError: + return None + + +class MessageCodec(Codec): + + def encode(self, m: Message) -> bytes: + ... + + def decode(self, b: bytes) -> Message: + reader = io.BytesIO(b) + message_id = self.read_message_id(reader) + + if message_id == 0: + return Timestamp( + timestamp=self.read_uint(reader) + ) + if message_id == 1: + return SessionStart( + timestamp=self.read_uint(reader), + project_id=self.read_uint(reader), + tracker_version=self.read_string(reader), + rev_id=self.read_string(reader), + user_uuid=self.read_string(reader), + user_agent=self.read_string(reader), + user_os=self.read_string(reader), + user_os_version=self.read_string(reader), + user_browser=self.read_string(reader), + user_browser_version=self.read_string(reader), + user_device=self.read_string(reader), + user_device_type=self.read_string(reader), + user_device_memory_size=self.read_uint(reader), + user_device_heap_size=self.read_uint(reader), + user_country=self.read_string(reader) + ) + + if message_id == 2: + return SessionDisconnect( + timestamp=self.read_uint(reader) + ) + + if message_id == 3: + return SessionEnd( + timestamp=self.read_uint(reader) + ) + + if message_id == 4: + return SetPageLocation( + url=self.read_string(reader), + referrer=self.read_string(reader), + navigation_start=self.read_uint(reader) + ) + + if message_id == 5: + return SetViewportSize( + width=self.read_uint(reader), + height=self.read_uint(reader) + ) + + if message_id == 6: + return SetViewportScroll( + x=self.read_int(reader), + y=self.read_int(reader) + ) + + if message_id == 7: + return CreateDocument() + + if message_id == 8: + return CreateElementNode( + id=self.read_uint(reader), + parent_id=self.read_uint(reader), + index=self.read_uint(reader), + tag=self.read_string(reader), + svg=self.read_boolean(reader), + ) + + if message_id == 9: + return CreateTextNode( + id=self.read_uint(reader), + parent_id=self.read_uint(reader), + index=self.read_uint(reader) + ) + + if message_id == 10: + return MoveNode( + id=self.read_uint(reader), + parent_id=self.read_uint(reader), + index=self.read_uint(reader) + ) + + if message_id == 11: + return RemoveNode( + id=self.read_uint(reader) + ) + + if message_id == 12: + return SetNodeAttribute( + id=self.read_uint(reader), + name=self.read_string(reader), + value=self.read_string(reader) + ) + + if message_id == 13: + return RemoveNodeAttribute( + id=self.read_uint(reader), + name=self.read_string(reader) + ) + + if message_id == 14: + return SetNodeData( + id=self.read_uint(reader), + data=self.read_string(reader) + ) + + if message_id == 15: + return SetCSSData( + id=self.read_uint(reader), + data=self.read_string(reader) + ) + + if message_id == 16: + return SetNodeScroll( + id=self.read_uint(reader), + x=self.read_int(reader), + y=self.read_int(reader), + ) + + if message_id == 17: + return SetInputTarget( + id=self.read_uint(reader), + label=self.read_string(reader) + ) + + if message_id == 18: + return SetInputValue( + id=self.read_uint(reader), + value=self.read_string(reader), + mask=self.read_int(reader), + ) + + if message_id == 19: + return SetInputChecked( + id=self.read_uint(reader), + checked=self.read_boolean(reader) + ) + + if message_id == 20: + return MouseMove( + x=self.read_uint(reader), + y=self.read_uint(reader) + ) + + if message_id == 21: + return MouseClick( + id=self.read_uint(reader), + hesitation_time=self.read_uint(reader), + label=self.read_string(reader) + ) + + if message_id == 22: + return ConsoleLog( + level=self.read_string(reader), + value=self.read_string(reader) + ) + + if message_id == 23: + return PageLoadTiming( + request_start=self.read_uint(reader), + response_start=self.read_uint(reader), + response_end=self.read_uint(reader), + dom_content_loaded_event_start=self.read_uint(reader), + dom_content_loaded_event_end=self.read_uint(reader), + load_event_start=self.read_uint(reader), + load_event_end=self.read_uint(reader), + first_paint=self.read_uint(reader), + first_contentful_paint=self.read_uint(reader) + ) + + if message_id == 24: + return PageRenderTiming( + speed_index=self.read_uint(reader), + visually_complete=self.read_uint(reader), + time_to_interactive=self.read_uint(reader), + ) + + if message_id == 25: + return JSException( + name=self.read_string(reader), + message=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 26: + return RawErrorEvent( + timestamp=self.read_uint(reader), + source=self.read_string(reader), + name=self.read_string(reader), + message=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 27: + return RawCustomEvent( + name=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 28: + return UserID( + id=self.read_string(reader) + ) + + if message_id == 29: + return UserAnonymousID( + id=self.read_string(reader) + ) + + if message_id == 30: + return Metadata( + key=self.read_string(reader), + value=self.read_string(reader) + ) + + if message_id == 31: + return PageEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + url=self.read_string(reader), + referrer=self.read_string(reader), + loaded=self.read_boolean(reader), + request_start=self.read_uint(reader), + response_start=self.read_uint(reader), + response_end=self.read_uint(reader), + dom_content_loaded_event_start=self.read_uint(reader), + dom_content_loaded_event_end=self.read_uint(reader), + load_event_start=self.read_uint(reader), + load_event_end=self.read_uint(reader), + first_paint=self.read_uint(reader), + first_contentful_paint=self.read_uint(reader), + speed_index=self.read_uint(reader), + visually_complete=self.read_uint(reader), + time_to_interactive=self.read_uint(reader) + ) + + if message_id == 32: + return InputEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + value=self.read_string(reader), + value_masked=self.read_boolean(reader), + label=self.read_string(reader), + ) + + if message_id == 33: + return ClickEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + hesitation_time=self.read_uint(reader), + label=self.read_string(reader) + ) + + if message_id == 34: + return ErrorEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + source=self.read_string(reader), + name=self.read_string(reader), + message=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 35: + + message_id = self.read_uint(reader) + ts = self.read_uint(reader) + if ts > 9999999999999: + ts = None + return ResourceEvent( + message_id=message_id, + timestamp=ts, + duration=self.read_uint(reader), + ttfb=self.read_uint(reader), + header_size=self.read_uint(reader), + encoded_body_size=self.read_uint(reader), + decoded_body_size=self.read_uint(reader), + url=self.read_string(reader), + type=self.read_string(reader), + success=self.read_boolean(reader), + method=self.read_string(reader), + status=self.read_uint(reader) + ) + + if message_id == 36: + return CustomEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + name=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 37: + return CSSInsertRule( + id=self.read_uint(reader), + rule=self.read_string(reader), + index=self.read_uint(reader) + ) + + if message_id == 38: + return CSSDeleteRule( + id=self.read_uint(reader), + index=self.read_uint(reader) + ) + + if message_id == 39: + return Fetch( + method=self.read_string(reader), + url=self.read_string(reader), + request=self.read_string(reader), + response=self.read_string(reader), + status=self.read_uint(reader), + timestamp=self.read_uint(reader), + duration=self.read_uint(reader) + ) + + if message_id == 40: + return Profiler( + name=self.read_string(reader), + duration=self.read_uint(reader), + args=self.read_string(reader), + result=self.read_string(reader) + ) + + if message_id == 41: + return OTable( + key=self.read_string(reader), + value=self.read_string(reader) + ) + + if message_id == 42: + return StateAction( + type=self.read_string(reader) + ) + + if message_id == 43: + return StateActionEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + type=self.read_string(reader) + ) + + if message_id == 44: + return Redux( + action=self.read_string(reader), + state=self.read_string(reader), + duration=self.read_uint(reader) + ) + + if message_id == 45: + return Vuex( + mutation=self.read_string(reader), + state=self.read_string(reader), + ) + + if message_id == 46: + return MobX( + type=self.read_string(reader), + payload=self.read_string(reader), + ) + + if message_id == 47: + return NgRx( + action=self.read_string(reader), + state=self.read_string(reader), + duration=self.read_uint(reader) + ) + + if message_id == 48: + return GraphQL( + operation_kind=self.read_string(reader), + operation_name=self.read_string(reader), + variables=self.read_string(reader), + response=self.read_string(reader) + ) + + if message_id == 49: + return PerformanceTrack( + frames=self.read_int(reader), + ticks=self.read_int(reader), + total_js_heap_size=self.read_uint(reader), + used_js_heap_size=self.read_uint(reader) + ) + + if message_id == 50: + return GraphQLEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + name=self.read_string(reader) + ) + + if message_id == 52: + return DomDrop( + timestamp=self.read_uint(reader) + ) + + if message_id == 53: + return ResourceTiming( + timestamp=self.read_uint(reader), + duration=self.read_uint(reader), + ttfb=self.read_uint(reader), + header_size=self.read_uint(reader), + encoded_body_size=self.read_uint(reader), + decoded_body_size=self.read_uint(reader), + url=self.read_string(reader), + initiator=self.read_string(reader) + ) + + if message_id == 54: + return ConnectionInformation( + downlink=self.read_uint(reader), + type=self.read_string(reader) + ) + + if message_id == 55: + return SetPageVisibility( + hidden=self.read_boolean(reader) + ) + + if message_id == 56: + return PerformanceTrackAggr( + timestamp_start=self.read_uint(reader), + timestamp_end=self.read_uint(reader), + min_fps=self.read_uint(reader), + avg_fps=self.read_uint(reader), + max_fps=self.read_uint(reader), + min_cpu=self.read_uint(reader), + avg_cpu=self.read_uint(reader), + max_cpu=self.read_uint(reader), + min_total_js_heap_size=self.read_uint(reader), + avg_total_js_heap_size=self.read_uint(reader), + max_total_js_heap_size=self.read_uint(reader), + min_used_js_heap_size=self.read_uint(reader), + avg_used_js_heap_size=self.read_uint(reader), + max_used_js_heap_size=self.read_uint(reader) + ) + + if message_id == 59: + return LongTask( + timestamp=self.read_uint(reader), + duration=self.read_uint(reader), + context=self.read_uint(reader), + container_type=self.read_uint(reader), + container_src=self.read_string(reader), + container_id=self.read_string(reader), + container_name=self.read_string(reader) + ) + + if message_id == 60: + return SetNodeURLBasedAttribute( + id=self.read_uint(reader), + name=self.read_string(reader), + value=self.read_string(reader), + base_url=self.read_string(reader) + ) + + if message_id == 61: + return SetStyleData( + id=self.read_uint(reader), + data=self.read_string(reader), + base_url=self.read_string(reader) + ) + + if message_id == 62: + return IssueEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + type=self.read_string(reader), + context_string=self.read_string(reader), + context=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 63: + return TechnicalInfo( + type=self.read_string(reader), + value=self.read_string(reader) + ) + + if message_id == 64: + return CustomIssue( + name=self.read_string(reader), + payload=self.read_string(reader) + ) + + if message_id == 65: + return PageClose() + + if message_id == 90: + return IOSSessionStart( + timestamp=self.read_uint(reader), + project_id=self.read_uint(reader), + tracker_version=self.read_string(reader), + rev_id=self.read_string(reader), + user_uuid=self.read_string(reader), + user_os=self.read_string(reader), + user_os_version=self.read_string(reader), + user_device=self.read_string(reader), + user_device_type=self.read_string(reader), + user_country=self.read_string(reader) + ) + + if message_id == 91: + return IOSSessionEnd( + timestamp=self.read_uint(reader) + ) + + if message_id == 92: + return IOSMetadata( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + key=self.read_string(reader), + value=self.read_string(reader) + ) + + if message_id == 94: + return IOSUserID( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + value=self.read_string(reader) + ) + + if message_id == 95: + return IOSUserAnonymousID( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + value=self.read_string(reader) + ) + + if message_id == 99: + return IOSScreenLeave( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + title=self.read_string(reader), + view_name=self.read_string(reader) + ) + + if message_id == 103: + return IOSLog( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + severity=self.read_string(reader), + content=self.read_string(reader) + ) + + if message_id == 104: + return IOSInternalError( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + content=self.read_string(reader) + ) + + if message_id == 110: + return IOSPerformanceAggregated( + timestamp_start=self.read_uint(reader), + timestamp_end=self.read_uint(reader), + min_fps=self.read_uint(reader), + avg_fps=self.read_uint(reader), + max_fps=self.read_uint(reader), + min_cpu=self.read_uint(reader), + avg_cpu=self.read_uint(reader), + max_cpu=self.read_uint(reader), + min_memory=self.read_uint(reader), + avg_memory=self.read_uint(reader), + max_memory=self.read_uint(reader), + min_battery=self.read_uint(reader), + avg_battery=self.read_uint(reader), + max_battery=self.read_uint(reader) + ) + + def read_message_id(self, reader: io.BytesIO) -> int: + """ + Read and return the first byte where the message id is encoded + """ + id_ = self.read_uint(reader) + return id_ + + @staticmethod + def check_message_id(b: bytes) -> int: + """ + todo: make it static and without reader. It's just the first byte + Read and return the first byte where the message id is encoded + """ + reader = io.BytesIO(b) + id_ = Codec.read_uint(reader) + + return id_ + + @staticmethod + def decode_key(b) -> int: + """ + Decode the message key (encoded with little endian) + """ + try: + decoded = int.from_bytes(b, "little", signed=False) + except Exception as e: + raise UnicodeDecodeError(f"Error while decoding message key (SessionID) from {b}\n{e}") + return decoded diff --git a/ee/connectors/msgcodec/messages.py b/ee/connectors/msgcodec/messages.py new file mode 100644 index 000000000..c6e53b445 --- /dev/null +++ b/ee/connectors/msgcodec/messages.py @@ -0,0 +1,752 @@ +""" +Representations of Kafka messages +""" +from abc import ABC + + +class Message(ABC): + pass + + +class Timestamp(Message): + __id__ = 0 + + def __init__(self, timestamp): + self.timestamp = timestamp + + +class SessionStart(Message): + __id__ = 1 + + def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid, + user_agent, user_os, user_os_version, user_browser, user_browser_version, + user_device, user_device_type, user_device_memory_size, user_device_heap_size, + user_country): + self.timestamp = timestamp + self.project_id = project_id + self.tracker_version = tracker_version + self.rev_id = rev_id + self.user_uuid = user_uuid + self.user_agent = user_agent + self.user_os = user_os + self.user_os_version = user_os_version + self.user_browser = user_browser + self.user_browser_version = user_browser_version + self.user_device = user_device + self.user_device_type = user_device_type + self.user_device_memory_size = user_device_memory_size + self.user_device_heap_size = user_device_heap_size + self.user_country = user_country + + +class SessionDisconnect(Message): + __id__ = 2 + + def __init__(self, timestamp): + self.timestamp = timestamp + + +class SessionEnd(Message): + __id__ = 3 + __name__ = 'SessionEnd' + + def __init__(self, timestamp): + self.timestamp = timestamp + + +class SetPageLocation(Message): + __id__ = 4 + + def __init__(self, url, referrer, navigation_start): + self.url = url + self.referrer = referrer + self.navigation_start = navigation_start + + +class SetViewportSize(Message): + __id__ = 5 + + def __init__(self, width, height): + self.width = width + self.height = height + + +class SetViewportScroll(Message): + __id__ = 6 + + def __init__(self, x, y): + self.x = x + self.y = y + + +class CreateDocument(Message): + __id__ = 7 + + +class CreateElementNode(Message): + __id__ = 8 + + def __init__(self, id, parent_id, index, tag, svg): + self.id = id + self.parent_id = parent_id, + self.index = index + self.tag = tag + self.svg = svg + + +class CreateTextNode(Message): + __id__ = 9 + + def __init__(self, id, parent_id, index): + self.id = id + self.parent_id = parent_id + self.index = index + + +class MoveNode(Message): + __id__ = 10 + + def __init__(self, id, parent_id, index): + self.id = id + self.parent_id = parent_id + self.index = index + + +class RemoveNode(Message): + __id__ = 11 + + def __init__(self, id): + self.id = id + + +class SetNodeAttribute(Message): + __id__ = 12 + + def __init__(self, id, name: str, value: str): + self.id = id + self.name = name + self.value = value + + +class RemoveNodeAttribute(Message): + __id__ = 13 + + def __init__(self, id, name: str): + self.id = id + self.name = name + + +class SetNodeData(Message): + __id__ = 14 + + def __init__(self, id, data: str): + self.id = id + self.data = data + + +class SetCSSData(Message): + __id__ = 15 + + def __init__(self, id, data: str): + self.id = id + self.data = data + + +class SetNodeScroll(Message): + __id__ = 16 + + def __init__(self, id, x: int, y: int): + self.id = id + self.x = x + self.y = y + + +class SetInputTarget(Message): + __id__ = 17 + + def __init__(self, id, label: str): + self.id = id + self.label = label + + +class SetInputValue(Message): + __id__ = 18 + + def __init__(self, id, value: str, mask: int): + self.id = id + self.value = value + self.mask = mask + + +class SetInputChecked(Message): + __id__ = 19 + + def __init__(self, id, checked: bool): + self.id = id + self.checked = checked + + +class MouseMove(Message): + __id__ = 20 + + def __init__(self, x, y): + self.x = x + self.y = y + + +class MouseClick(Message): + __id__ = 21 + + def __init__(self, id, hesitation_time, label: str): + self.id = id + self.hesitation_time = hesitation_time + self.label = label + + +class ConsoleLog(Message): + __id__ = 22 + + def __init__(self, level: str, value: str): + self.level = level + self.value = value + + +class PageLoadTiming(Message): + __id__ = 23 + + def __init__(self, request_start, response_start, response_end, dom_content_loaded_event_start, + dom_content_loaded_event_end, load_event_start, load_event_end, + first_paint, first_contentful_paint): + self.request_start = request_start + self.response_start = response_start + self.response_end = response_end + self.dom_content_loaded_event_start = dom_content_loaded_event_start + self.dom_content_loaded_event_end = dom_content_loaded_event_end + self.load_event_start = load_event_start + self.load_event_end = load_event_end + self.first_paint = first_paint + self.first_contentful_paint = first_contentful_paint + + +class PageRenderTiming(Message): + __id__ = 24 + + def __init__(self, speed_index, visually_complete, time_to_interactive): + self.speed_index = speed_index + self.visually_complete = visually_complete + self.time_to_interactive = time_to_interactive + +class JSException(Message): + __id__ = 25 + + def __init__(self, name: str, message: str, payload: str): + self.name = name + self.message = message + self.payload = payload + + +class RawErrorEvent(Message): + __id__ = 26 + + def __init__(self, timestamp, source: str, name: str, message: str, + payload: str): + self.timestamp = timestamp + self.source = source + self.name = name + self.message = message + self.payload = payload + + +class RawCustomEvent(Message): + __id__ = 27 + + def __init__(self, name: str, payload: str): + self.name = name + self.payload = payload + + +class UserID(Message): + __id__ = 28 + + def __init__(self, id: str): + self.id = id + + +class UserAnonymousID(Message): + __id__ = 29 + + def __init__(self, id: str): + self.id = id + + +class Metadata(Message): + __id__ = 30 + + def __init__(self, key: str, value: str): + self.key = key + self.value = value + + +class PerformanceTrack(Message): + __id__ = 49 + + def __init__(self, frames: int, ticks: int, total_js_heap_size, + used_js_heap_size): + self.frames = frames + self.ticks = ticks + self.total_js_heap_size = total_js_heap_size + self.used_js_heap_size = used_js_heap_size + + +class PageEvent(Message): + __id__ = 31 + + def __init__(self, message_id, timestamp, url: str, referrer: str, + loaded: bool, request_start, response_start, response_end, + dom_content_loaded_event_start, dom_content_loaded_event_end, + load_event_start, load_event_end, first_paint, first_contentful_paint, + speed_index, visually_complete, time_to_interactive): + self.message_id = message_id + self.timestamp = timestamp + self.url = url + self.referrer = referrer + self.loaded = loaded + self.request_start = request_start + self.response_start = response_start + self.response_end = response_end + self.dom_content_loaded_event_start = dom_content_loaded_event_start + self.dom_content_loaded_event_end = dom_content_loaded_event_end + self.load_event_start = load_event_start + self.load_event_end = load_event_end + self.first_paint = first_paint + self.first_contentful_paint = first_contentful_paint + self.speed_index = speed_index + self.visually_complete = visually_complete + self.time_to_interactive = time_to_interactive + + +class InputEvent(Message): + __id__ = 32 + + def __init__(self, message_id, timestamp, value: str, value_masked: bool, label: str): + self.message_id = message_id + self.timestamp = timestamp + self.value = value + self.value_masked = value_masked + self.label = label + + +class ClickEvent(Message): + __id__ = 33 + + def __init__(self, message_id, timestamp, hesitation_time, label: str): + self.message_id = message_id + self.timestamp = timestamp + self.hesitation_time = hesitation_time + self.label = label + + +class ErrorEvent(Message): + __id__ = 34 + + def __init__(self, message_id, timestamp, source: str, name: str, message: str, + payload: str): + self.message_id = message_id + self.timestamp = timestamp + self.source = source + self.name = name + self.message = message + self.payload = payload + + +class ResourceEvent(Message): + __id__ = 35 + + def __init__(self, message_id, timestamp, duration, ttfb, header_size, encoded_body_size, + decoded_body_size, url: str, type: str, success: bool, method: str, status): + self.message_id = message_id + self.timestamp = timestamp + self.duration = duration + self.ttfb = ttfb + self.header_size = header_size + self.encoded_body_size = encoded_body_size + self.decoded_body_size = decoded_body_size + self.url = url + self.type = type + self.success = success + self.method = method + self.status = status + + +class CustomEvent(Message): + __id__ = 36 + + def __init__(self, message_id, timestamp, name: str, payload: str): + self.message_id = message_id + self.timestamp = timestamp + self.name = name + self.payload = payload + + +class CSSInsertRule(Message): + __id__ = 37 + + def __init__(self, id, rule: str, index): + self.id = id + self.rule = rule + self.index = index + + +class CSSDeleteRule(Message): + __id__ = 38 + + def __init__(self, id, index): + self.id = id + self.index = index + + +class Fetch(Message): + __id__ = 39 + + def __init__(self, method: str, url: str, request: str, response: str, status, + timestamp, duration): + self.method = method + self.url = url + self.request = request + self.response = response + self.status = status + self.timestamp = timestamp + self.duration = duration + + +class Profiler(Message): + __id__ = 40 + + def __init__(self, name: str, duration, args: str, result: str): + self.name = name + self.duration = duration + self.args = args + self.result = result + + +class OTable(Message): + __id__ = 41 + + def __init__(self, key: str, value: str): + self.key = key + self.value = value + + +class StateAction(Message): + __id__ = 42 + + def __init__(self, type: str): + self.type = type + + +class StateActionEvent(Message): + __id__ = 43 + + def __init__(self, message_id, timestamp, type: str): + self.message_id = message_id + self.timestamp = timestamp + self.type = type + + +class Redux(Message): + __id__ = 44 + + def __init__(self, action: str, state: str, duration): + self.action = action + self.state = state + self.duration = duration + + +class Vuex(Message): + __id__ = 45 + + def __init__(self, mutation: str, state: str): + self.mutation = mutation + self.state = state + + +class MobX(Message): + __id__ = 46 + + def __init__(self, type: str, payload: str): + self.type = type + self.payload = payload + + +class NgRx(Message): + __id__ = 47 + + def __init__(self, action: str, state: str, duration): + self.action = action + self.state = state + self.duration = duration + + +class GraphQL(Message): + __id__ = 48 + + def __init__(self, operation_kind: str, operation_name: str, + variables: str, response: str): + self.operation_kind = operation_kind + self.operation_name = operation_name + self.variables = variables + self.response = response + + +class PerformanceTrack(Message): + __id__ = 49 + + def __init__(self, frames: int, ticks: int, + total_js_heap_size, used_js_heap_size): + self.frames = frames + self.ticks = ticks + self.total_js_heap_size = total_js_heap_size + self.used_js_heap_size = used_js_heap_size + + +class GraphQLEvent(Message): + __id__ = 50 + + def __init__(self, message_id, timestamp, name: str): + self.message_id = message_id + self.timestamp = timestamp + self.name = name + + +class DomDrop(Message): + __id__ = 52 + + def __init__(self, timestamp): + self.timestamp = timestamp + + +class ResourceTiming(Message): + __id__ = 53 + + def __init__(self, timestamp, duration, ttfb, header_size, encoded_body_size, + decoded_body_size, url, initiator): + self.timestamp = timestamp + self.duration = duration + self.ttfb = ttfb + self.header_size = header_size + self.encoded_body_size = encoded_body_size + self.decoded_body_size = decoded_body_size + self.url = url + self.initiator = initiator + + +class ConnectionInformation(Message): + __id__ = 54 + + def __init__(self, downlink, type: str): + self.downlink = downlink + self.type = type + + +class SetPageVisibility(Message): + __id__ = 55 + + def __init__(self, hidden: bool): + self.hidden = hidden + + +class PerformanceTrackAggr(Message): + __id__ = 56 + + def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, + max_fps, min_cpu, avg_cpu, max_cpu, + min_total_js_heap_size, avg_total_js_heap_size, + max_total_js_heap_size, min_used_js_heap_size, + avg_used_js_heap_size, max_used_js_heap_size + ): + self.timestamp_start = timestamp_start + self.timestamp_end = timestamp_end + self.min_fps = min_fps + self.avg_fps = avg_fps + self.max_fps = max_fps + self.min_cpu = min_cpu + self.avg_cpu = avg_cpu + self.max_cpu = max_cpu + self.min_total_js_heap_size = min_total_js_heap_size + self.avg_total_js_heap_size = avg_total_js_heap_size + self.max_total_js_heap_size = max_total_js_heap_size + self.min_used_js_heap_size = min_used_js_heap_size + self.avg_used_js_heap_size = avg_used_js_heap_size + self.max_used_js_heap_size = max_used_js_heap_size + + +class LongTask(Message): + __id__ = 59 + + def __init__(self, timestamp, duration, context, container_type, container_src: str, + container_id: str, container_name: str): + self.timestamp = timestamp + self.duration = duration + self.context = context + self.container_type = container_type + self.container_src = container_src + self.container_id = container_id + self.container_name = container_name + + +class SetNodeURLBasedAttribute(Message): + __id__ = 60 + + def __init__(self, id, name: str, value: str, base_url: str): + self.id = id + self.name = name + self.value = value + self.base_url = base_url + + +class SetStyleData(Message): + __id__ = 61 + + def __init__(self, id, data: str, base_url: str): + self.id = id + self.data = data + self.base_url = base_url + + +class IssueEvent(Message): + __id__ = 62 + + def __init__(self, message_id, timestamp, type: str, context_string: str, + context: str, payload: str): + self.message_id = message_id + self.timestamp = timestamp + self.type = type + self.context_string = context_string + self.context = context + self.payload = payload + + +class TechnicalInfo(Message): + __id__ = 63 + + def __init__(self, type: str, value: str): + self.type = type + self.value = value + + +class CustomIssue(Message): + __id__ = 64 + + def __init__(self, name: str, payload: str): + self.name = name + self.payload = payload + + +class PageClose(Message): + __id__ = 65 + + +class IOSSessionStart(Message): + __id__ = 90 + + def __init__(self, timestamp, project_id, tracker_version: str, + rev_id: str, user_uuid: str, user_os: str, user_os_version: str, + user_device: str, user_device_type: str, user_country: str): + self.timestamp = timestamp + self.project_id = project_id + self.tracker_version = tracker_version + self.rev_id = rev_id + self.user_uuid = user_uuid + self.user_os = user_os + self.user_os_version = user_os_version + self.user_device = user_device + self.user_device_type = user_device_type + self.user_country = user_country + + +class IOSSessionEnd(Message): + __id__ = 91 + + def __init__(self, timestamp): + self.timestamp = timestamp + + +class IOSMetadata(Message): + __id__ = 92 + + def __init__(self, timestamp, length, key: str, value: str): + self.timestamp = timestamp + self.length = length + self.key = key + self.value = value + + +class IOSUserID(Message): + __id__ = 94 + + def __init__(self, timestamp, length, value: str): + self.timestamp = timestamp + self.length = length + self.value = value + + +class IOSUserAnonymousID(Message): + __id__ = 95 + + def __init__(self, timestamp, length, value: str): + self.timestamp = timestamp + self.length = length + self.value = value + + +class IOSScreenLeave(Message): + __id__ = 99 + + def __init__(self, timestamp, length, title: str, view_name: str): + self.timestamp = timestamp + self.length = length + self.title = title + self.view_name = view_name + + +class IOSLog(Message): + __id__ = 103 + + def __init__(self, timestamp, length, severity: str, content: str): + self.timestamp = timestamp + self.length = length + self.severity = severity + self.content = content + + +class IOSInternalError(Message): + __id__ = 104 + + def __init__(self, timestamp, length, content: str): + self.timestamp = timestamp + self.length = length + self.content = content + + +class IOSPerformanceAggregated(Message): + __id__ = 110 + + def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, + max_fps, min_cpu, avg_cpu, max_cpu, + min_memory, avg_memory, max_memory, + min_battery, avg_battery, max_battery + ): + self.timestamp_start = timestamp_start + self.timestamp_end = timestamp_end + self.min_fps = min_fps + self.avg_fps = avg_fps + self.max_fps = max_fps + self.min_cpu = min_cpu + self.avg_cpu = avg_cpu + self.max_cpu = max_cpu + self.min_memory = min_memory + self.avg_memory = avg_memory + self.max_memory = max_memory + self.min_battery = min_battery + self.avg_battery = avg_battery + self.max_battery = max_battery diff --git a/ee/connectors/requirements.txt b/ee/connectors/requirements.txt new file mode 100644 index 000000000..a6b6a0720 --- /dev/null +++ b/ee/connectors/requirements.txt @@ -0,0 +1,43 @@ +certifi==2020.12.5 +chardet==4.0.0 +clickhouse-driver==0.2.0 +clickhouse-sqlalchemy==0.1.5 +idna==2.10 +kafka-python==2.0.2 +pandas==1.2.3 +psycopg2-binary==2.8.6 +pytz==2021.1 +requests==2.25.1 +SQLAlchemy==1.3.23 +tzlocal==2.1 +urllib3==1.26.3 +PyYAML==5.4.1 +pandas-redshift +awswrangler +google-auth-httplib2 +google-auth-oauthlib +google-cloud-bigquery +pandas-gbq +snowflake-connector-python==2.4.1 +snowflake-sqlalchemy==1.2.4 +asn1crypto==1.4.0 +azure-common==1.1.25 +azure-core==1.8.2 +azure-storage-blob==12.5.0 +boto3==1.15.18 +botocore==1.18.18 +cffi==1.14.3 +cryptography==2.9.2 +isodate==0.6.0 +jmespath==0.10.0 +msrest==0.6.19 +oauthlib==3.1.0 +oscrypto==1.2.1 +pycparser==2.20 +pycryptodomex==3.9.8 +PyJWT==1.7.1 +pyOpenSSL==19.1.0 +python-dateutil==2.8.1 +requests-oauthlib==1.3.0 +s3transfer==0.3.3 +six==1.15.0 diff --git a/ee/connectors/sql/clickhouse_events.sql b/ee/connectors/sql/clickhouse_events.sql new file mode 100644 index 000000000..b5eb8b440 --- /dev/null +++ b/ee/connectors/sql/clickhouse_events.sql @@ -0,0 +1,56 @@ +CREATE TABLE IF NOT EXISTS connector_events +( + sessionid UInt64, + connectioninformation_downlink Nullable(UInt64), + connectioninformation_type Nullable(String), + consolelog_level Nullable(String), + consolelog_value Nullable(String), + customevent_messageid Nullable(UInt64), + customevent_name Nullable(String), + customevent_payload Nullable(String), + customevent_timestamp Nullable(UInt64), + errorevent_message Nullable(String), + errorevent_messageid Nullable(UInt64), + errorevent_name Nullable(String), + errorevent_payload Nullable(String), + errorevent_source Nullable(String), + errorevent_timestamp Nullable(UInt64), + jsexception_message Nullable(String), + jsexception_name Nullable(String), + jsexception_payload Nullable(String), + metadata_key Nullable(String), + metadata_value Nullable(String), + mouseclick_id Nullable(UInt64), + mouseclick_hesitationtime Nullable(UInt64), + mouseclick_label Nullable(String), + pageevent_firstcontentfulpaint Nullable(UInt64), + pageevent_firstpaint Nullable(UInt64), + pageevent_messageid Nullable(UInt64), + pageevent_referrer Nullable(String), + pageevent_speedindex Nullable(UInt64), + pageevent_timestamp Nullable(UInt64), + pageevent_url Nullable(String), + pagerendertiming_timetointeractive Nullable(UInt64), + pagerendertiming_visuallycomplete Nullable(UInt64), + rawcustomevent_name Nullable(String), + rawcustomevent_payload Nullable(String), + setviewportsize_height Nullable(UInt64), + setviewportsize_width Nullable(UInt64), + timestamp_timestamp Nullable(UInt64), + user_anonymous_id Nullable(String), + user_id Nullable(String), + issueevent_messageid Nullable(UInt64), + issueevent_timestamp Nullable(UInt64), + issueevent_type Nullable(String), + issueevent_contextstring Nullable(String), + issueevent_context Nullable(String), + issueevent_payload Nullable(String), + customissue_name Nullable(String), + customissue_payload Nullable(String), + received_at UInt64, + batch_order_number UInt64 +) ENGINE = MergeTree() +PARTITION BY intDiv(received_at, 100000) +ORDER BY (received_at, batch_order_number, sessionid) +PRIMARY KEY (received_at) +SETTINGS use_minimalistic_part_header_in_zookeeper=1, index_granularity=1000; \ No newline at end of file diff --git a/ee/connectors/sql/clickhouse_events_buffer.sql b/ee/connectors/sql/clickhouse_events_buffer.sql new file mode 100644 index 000000000..ed291c824 --- /dev/null +++ b/ee/connectors/sql/clickhouse_events_buffer.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS connector_events_buffer +( + sessionid UInt64, + connectioninformation_downlink Nullable(UInt64), + connectioninformation_type Nullable(String), + consolelog_level Nullable(String), + consolelog_value Nullable(String), + customevent_messageid Nullable(UInt64), + customevent_name Nullable(String), + customevent_payload Nullable(String), + customevent_timestamp Nullable(UInt64), + errorevent_message Nullable(String), + errorevent_messageid Nullable(UInt64), + errorevent_name Nullable(String), + errorevent_payload Nullable(String), + errorevent_source Nullable(String), + errorevent_timestamp Nullable(UInt64), + jsexception_message Nullable(String), + jsexception_name Nullable(String), + jsexception_payload Nullable(String), + metadata_key Nullable(String), + metadata_value Nullable(String), + mouseclick_id Nullable(UInt64), + mouseclick_hesitationtime Nullable(UInt64), + mouseclick_label Nullable(String), + pageevent_firstcontentfulpaint Nullable(UInt64), + pageevent_firstpaint Nullable(UInt64), + pageevent_messageid Nullable(UInt64), + pageevent_referrer Nullable(String), + pageevent_speedindex Nullable(UInt64), + pageevent_timestamp Nullable(UInt64), + pageevent_url Nullable(String), + pagerendertiming_timetointeractive Nullable(UInt64), + pagerendertiming_visuallycomplete Nullable(UInt64), + rawcustomevent_name Nullable(String), + rawcustomevent_payload Nullable(String), + setviewportsize_height Nullable(UInt64), + setviewportsize_width Nullable(UInt64), + timestamp_timestamp Nullable(UInt64), + user_anonymous_id Nullable(String), + user_id Nullable(String), + issueevent_messageid Nullable(UInt64), + issueevent_timestamp Nullable(UInt64), + issueevent_type Nullable(String), + issueevent_contextstring Nullable(String), + issueevent_context Nullable(String), + issueevent_payload Nullable(String), + customissue_name Nullable(String), + customissue_payload Nullable(String), + received_at UInt64, + batch_order_number UInt64 +) ENGINE = Buffer(default, connector_events, 16, 10, 120, 10000, 1000000, 10000, 100000000); diff --git a/ee/connectors/sql/clickhouse_sessions.sql b/ee/connectors/sql/clickhouse_sessions.sql new file mode 100644 index 000000000..4d648553e --- /dev/null +++ b/ee/connectors/sql/clickhouse_sessions.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS connector_user_sessions +( +-- SESSION METADATA + sessionid UInt64, + user_agent Nullable(String), + user_browser Nullable(String), + user_browser_version Nullable(String), + user_country Nullable(String), + user_device Nullable(String), + user_device_heap_size Nullable(UInt64), + user_device_memory_size Nullable(UInt64), + user_device_type Nullable(String), + user_os Nullable(String), + user_os_version Nullable(String), + user_uuid Nullable(String), + connection_effective_bandwidth Nullable(UInt64), -- Downlink + connection_type Nullable(String), --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" + metadata_key Nullable(String), + metadata_value Nullable(String), + referrer Nullable(String), + user_anonymous_id Nullable(String), + user_id Nullable(String), +-- TIME + session_start_timestamp Nullable(UInt64), + session_end_timestamp Nullable(UInt64), + session_duration Nullable(UInt64), +-- SPEED INDEX RELATED + first_contentful_paint Nullable(UInt64), + speed_index Nullable(UInt64), + visually_complete Nullable(UInt64), + timing_time_to_interactive Nullable(UInt64), +-- PERFORMANCE + avg_cpu Nullable(UInt64), + avg_fps Nullable(UInt64), + max_cpu Nullable(UInt64), + max_fps Nullable(UInt64), + max_total_js_heap_size Nullable(UInt64), + max_used_js_heap_size Nullable(UInt64), +-- ISSUES AND EVENTS + js_exceptions_count Nullable(UInt64), + long_tasks_total_duration Nullable(UInt64), + long_tasks_max_duration Nullable(UInt64), + long_tasks_count Nullable(UInt64), + inputs_count Nullable(UInt64), + clicks_count Nullable(UInt64), + issues_count Nullable(UInt64), + issues Array(Nullable(String)), + urls_count Nullable(UInt64), + urls Array(Nullable(String)) +) ENGINE = MergeTree() +ORDER BY (sessionid) +PRIMARY KEY (sessionid); \ No newline at end of file diff --git a/ee/connectors/sql/clickhouse_sessions_buffer.sql b/ee/connectors/sql/clickhouse_sessions_buffer.sql new file mode 100644 index 000000000..540700d45 --- /dev/null +++ b/ee/connectors/sql/clickhouse_sessions_buffer.sql @@ -0,0 +1,50 @@ +CREATE TABLE IF NOT EXISTS connector_user_sessions_buffer +( +-- SESSION METADATA + sessionid UInt64, + user_agent Nullable(String), + user_browser Nullable(String), + user_browser_version Nullable(String), + user_country Nullable(String), + user_device Nullable(String), + user_device_heap_size Nullable(UInt64), + user_device_memory_size Nullable(UInt64), + user_device_type Nullable(String), + user_os Nullable(String), + user_os_version Nullable(String), + user_uuid Nullable(String), + connection_effective_bandwidth Nullable(UInt64), -- Downlink + connection_type Nullable(String), --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" + metadata_key Nullable(String), + metadata_value Nullable(String), + referrer Nullable(String), + user_anonymous_id Nullable(String), + user_id Nullable(String), +-- TIME + session_start_timestamp Nullable(UInt64), + session_end_timestamp Nullable(UInt64), + session_duration Nullable(UInt64), +-- SPEED INDEX RELATED + first_contentful_paint Nullable(UInt64), + speed_index Nullable(UInt64), + visually_complete Nullable(UInt64), + timing_time_to_interactive Nullable(UInt64), +-- PERFORMANCE + avg_cpu Nullable(UInt64), + avg_fps Nullable(UInt64), + max_cpu Nullable(UInt64), + max_fps Nullable(UInt64), + max_total_js_heap_size Nullable(UInt64), + max_used_js_heap_size Nullable(UInt64), +-- ISSUES AND EVENTS + js_exceptions_count Nullable(UInt64), + long_tasks_total_duration Nullable(UInt64), + long_tasks_max_duration Nullable(UInt64), + long_tasks_count Nullable(UInt64), + inputs_count Nullable(UInt64), + clicks_count Nullable(UInt64), + issues_count Nullable(UInt64), + issues Array(Nullable(String)), + urls_count Nullable(UInt64), + urls Array(Nullable(String)) +) ENGINE = Buffer(default, connector_user_sessions, 16, 10, 120, 10000, 1000000, 10000, 100000000); diff --git a/ee/connectors/sql/postgres_events.sql b/ee/connectors/sql/postgres_events.sql new file mode 100644 index 000000000..986de4df9 --- /dev/null +++ b/ee/connectors/sql/postgres_events.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS connector_events +( + sessionid bigint, + connectioninformation_downlink bigint, + connectioninformation_type text, + consolelog_level text, + consolelog_value text, + customevent_messageid bigint, + customevent_name text, + customevent_payload text, + customevent_timestamp bigint, + errorevent_message text, + errorevent_messageid bigint, + errorevent_name text, + errorevent_payload text, + errorevent_source text, + errorevent_timestamp bigint, + jsexception_message text, + jsexception_name text, + jsexception_payload text, + metadata_key text, + metadata_value text, + mouseclick_id bigint, + mouseclick_hesitationtime bigint, + mouseclick_label text, + pageevent_firstcontentfulpaint bigint, + pageevent_firstpaint bigint, + pageevent_messageid bigint, + pageevent_referrer text, + pageevent_speedindex bigint, + pageevent_timestamp bigint, + pageevent_url text, + pagerendertiming_timetointeractive bigint, + pagerendertiming_visuallycomplete bigint, + rawcustomevent_name text, + rawcustomevent_payload text, + setviewportsize_height bigint, + setviewportsize_width bigint, + timestamp_timestamp bigint, + user_anonymous_id text, + user_id text, + issueevent_messageid bigint, + issueevent_timestamp bigint, + issueevent_type text, + issueevent_contextstring text, + issueevent_context text, + issueevent_payload text, + customissue_name text, + customissue_payload text, + received_at bigint, + batch_order_number bigint +); \ No newline at end of file diff --git a/ee/connectors/sql/postgres_sessions.sql b/ee/connectors/sql/postgres_sessions.sql new file mode 100644 index 000000000..1f68309c2 --- /dev/null +++ b/ee/connectors/sql/postgres_sessions.sql @@ -0,0 +1,50 @@ +CREATE TABLE IF NOT EXISTS connector_user_sessions +( +-- SESSION METADATA + sessionid bigint, + user_agent text, + user_browser text, + user_browser_version text, + user_country text, + user_device text, + user_device_heap_size bigint, + user_device_memory_size bigint, + user_device_type text, + user_os text, + user_os_version text, + user_uuid text, + connection_effective_bandwidth bigint, -- Downlink + connection_type text, --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" + metadata_key text, + metadata_value text, + referrer text, + user_anonymous_id text, + user_id text, +-- TIME + session_start_timestamp bigint, + session_end_timestamp bigint, + session_duration bigint, +-- SPEED INDEX RELATED + first_contentful_paint bigint, + speed_index bigint, + visually_complete bigint, + timing_time_to_interactive bigint, +-- PERFORMANCE + avg_cpu bigint, + avg_fps bigint, + max_cpu bigint, + max_fps bigint, + max_total_js_heap_size bigint, + max_used_js_heap_size bigint, +-- ISSUES AND EVENTS + js_exceptions_count bigint, + long_tasks_total_duration bigint, + long_tasks_max_duration bigint, + long_tasks_count bigint, + inputs_count bigint, + clicks_count bigint, + issues_count bigint, + issues text[], + urls_count bigint, + urls text[] +); \ No newline at end of file diff --git a/ee/connectors/sql/redshift_events.sql b/ee/connectors/sql/redshift_events.sql new file mode 100644 index 000000000..c310e3202 --- /dev/null +++ b/ee/connectors/sql/redshift_events.sql @@ -0,0 +1,52 @@ +CREATE TABLE connector_events +( + sessionid BIGINT, + connectioninformation_downlink BIGINT, + connectioninformation_type VARCHAR(300), + consolelog_level VARCHAR(300), + consolelog_value VARCHAR(300), + customevent_messageid BIGINT, + customevent_name VARCHAR(300), + customevent_payload VARCHAR(300), + customevent_timestamp BIGINT, + errorevent_message VARCHAR(300), + errorevent_messageid BIGINT, + errorevent_name VARCHAR(300), + errorevent_payload VARCHAR(300), + errorevent_source VARCHAR(300), + errorevent_timestamp BIGINT, + jsexception_message VARCHAR(300), + jsexception_name VARCHAR(300), + jsexception_payload VARCHAR(300), + metadata_key VARCHAR(300), + metadata_value VARCHAR(300), + mouseclick_id BIGINT, + mouseclick_hesitationtime BIGINT, + mouseclick_label VARCHAR(300), + pageevent_firstcontentfulpaint BIGINT, + pageevent_firstpaint BIGINT, + pageevent_messageid BIGINT, + pageevent_referrer VARCHAR(300), + pageevent_speedindex BIGINT, + pageevent_timestamp BIGINT, + pageevent_url VARCHAR(300), + pagerendertiming_timetointeractive BIGINT, + pagerendertiming_visuallycomplete BIGINT, + rawcustomevent_name VARCHAR(300), + rawcustomevent_payload VARCHAR(300), + setviewportsize_height BIGINT, + setviewportsize_width BIGINT, + timestamp_timestamp BIGINT, + user_anonymous_id VARCHAR(300), + user_id VARCHAR(300), + issueevent_messageid BIGINT, + issueevent_timestamp BIGINT, + issueevent_type VARCHAR(300), + issueevent_contextstring VARCHAR(300), + issueevent_context VARCHAR(300), + issueevent_payload VARCHAR(300), + customissue_name VARCHAR(300), + customissue_payload VARCHAR(300), + received_at BIGINT, + batch_order_number BIGINT +); \ No newline at end of file diff --git a/ee/connectors/sql/redshift_sessions.sql b/ee/connectors/sql/redshift_sessions.sql new file mode 100644 index 000000000..f1750dcc2 --- /dev/null +++ b/ee/connectors/sql/redshift_sessions.sql @@ -0,0 +1,50 @@ +CREATE TABLE connector_user_sessions +( +-- SESSION METADATA + sessionid bigint, + user_agent VARCHAR, + user_browser VARCHAR, + user_browser_version VARCHAR, + user_country VARCHAR, + user_device VARCHAR, + user_device_heap_size bigint, + user_device_memory_size bigint, + user_device_type VARCHAR, + user_os VARCHAR, + user_os_version VARCHAR, + user_uuid VARCHAR, + connection_effective_bandwidth bigint, -- Downlink + connection_type VARCHAR, --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" + metadata_key VARCHAR, + metadata_value VARCHAR, + referrer VARCHAR, + user_anonymous_id VARCHAR, + user_id VARCHAR, +-- TIME + session_start_timestamp bigint, + session_end_timestamp bigint, + session_duration bigint, +-- SPEED INDEX RELATED + first_contentful_paint bigint, + speed_index bigint, + visually_complete bigint, + timing_time_to_interactive bigint, +-- PERFORMANCE + avg_cpu bigint, + avg_fps bigint, + max_cpu bigint, + max_fps bigint, + max_total_js_heap_size bigint, + max_used_js_heap_size bigint, +-- ISSUES AND EVENTS + js_exceptions_count bigint, + long_tasks_total_duration bigint, + long_tasks_max_duration bigint, + long_tasks_count bigint, + inputs_count bigint, + clicks_count bigint, + issues_count bigint, + issues VARCHAR, + urls_count bigint, + urls VARCHAR +); \ No newline at end of file diff --git a/ee/connectors/sql/snowflake_events.sql b/ee/connectors/sql/snowflake_events.sql new file mode 100644 index 000000000..986de4df9 --- /dev/null +++ b/ee/connectors/sql/snowflake_events.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS connector_events +( + sessionid bigint, + connectioninformation_downlink bigint, + connectioninformation_type text, + consolelog_level text, + consolelog_value text, + customevent_messageid bigint, + customevent_name text, + customevent_payload text, + customevent_timestamp bigint, + errorevent_message text, + errorevent_messageid bigint, + errorevent_name text, + errorevent_payload text, + errorevent_source text, + errorevent_timestamp bigint, + jsexception_message text, + jsexception_name text, + jsexception_payload text, + metadata_key text, + metadata_value text, + mouseclick_id bigint, + mouseclick_hesitationtime bigint, + mouseclick_label text, + pageevent_firstcontentfulpaint bigint, + pageevent_firstpaint bigint, + pageevent_messageid bigint, + pageevent_referrer text, + pageevent_speedindex bigint, + pageevent_timestamp bigint, + pageevent_url text, + pagerendertiming_timetointeractive bigint, + pagerendertiming_visuallycomplete bigint, + rawcustomevent_name text, + rawcustomevent_payload text, + setviewportsize_height bigint, + setviewportsize_width bigint, + timestamp_timestamp bigint, + user_anonymous_id text, + user_id text, + issueevent_messageid bigint, + issueevent_timestamp bigint, + issueevent_type text, + issueevent_contextstring text, + issueevent_context text, + issueevent_payload text, + customissue_name text, + customissue_payload text, + received_at bigint, + batch_order_number bigint +); \ No newline at end of file diff --git a/ee/connectors/sql/snowflake_sessions.sql b/ee/connectors/sql/snowflake_sessions.sql new file mode 100644 index 000000000..c66bac2e6 --- /dev/null +++ b/ee/connectors/sql/snowflake_sessions.sql @@ -0,0 +1,50 @@ +CREATE TABLE IF NOT EXISTS connector_user_sessions +( +-- SESSION METADATA + sessionid bigint, + user_agent text, + user_browser text, + user_browser_version text, + user_country text, + user_device text, + user_device_heap_size bigint, + user_device_memory_size bigint, + user_device_type text, + user_os text, + user_os_version text, + user_uuid text, + connection_effective_bandwidth bigint, -- Downlink + connection_type text, --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" + metadata_key text, + metadata_value text, + referrer text, + user_anonymous_id text, + user_id text, +-- TIME + session_start_timestamp bigint, + session_end_timestamp bigint, + session_duration bigint, +-- SPEED INDEX RELATED + first_contentful_paint bigint, + speed_index bigint, + visually_complete bigint, + timing_time_to_interactive bigint, +-- PERFORMANCE + avg_cpu bigint, + avg_fps bigint, + max_cpu bigint, + max_fps bigint, + max_total_js_heap_size bigint, + max_used_js_heap_size bigint, +-- ISSUES AND EVENTS + js_exceptions_count bigint, + long_tasks_total_duration bigint, + long_tasks_max_duration bigint, + long_tasks_count bigint, + inputs_count bigint, + clicks_count bigint, + issues_count bigint, + issues array, + urls_count bigint, + urls array +); \ No newline at end of file diff --git a/ee/connectors/utils/bigquery.env.example b/ee/connectors/utils/bigquery.env.example new file mode 100644 index 000000000..16d970501 --- /dev/null +++ b/ee/connectors/utils/bigquery.env.example @@ -0,0 +1,7 @@ +table_id='{project_id}.{dataset}.{table}' +project_id=name-123456 +dataset=datasetname +sessions_table=connector_user_sessions +events_table_name=connector_events +events_detailed_table_name=connector_events_detailed +level=normal diff --git a/ee/connectors/utils/bigquery_service_account.json.example b/ee/connectors/utils/bigquery_service_account.json.example new file mode 100644 index 000000000..e6473eed7 --- /dev/null +++ b/ee/connectors/utils/bigquery_service_account.json.example @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "aaaaaa-123456", + "private_key_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "private_key": "-----BEGIN PRIVATE KEY-----\some_letters_and_numbers\n-----END PRIVATE KEY-----\n", + "client_email": "abc-aws@aaaaa-123456.iam.gserviceaccount.com", + "client_id": "12345678910111213", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/bigquery-connector-aws%40asayer-143408.iam.gserviceaccount.com" +} diff --git a/ee/connectors/utils/clickhouse.env.example b/ee/connectors/utils/clickhouse.env.example new file mode 100644 index 000000000..038fa2a87 --- /dev/null +++ b/ee/connectors/utils/clickhouse.env.example @@ -0,0 +1,7 @@ +connect_str='clickhouse+native://{address}/{database}' +address=1.1.1.1:9000 +database=default +sessions_table=connector_user_sessions_buffer +events_table_name=connector_events_buffer +events_detailed_table_name=connector_events_detailed_buffer +level=normal diff --git a/ee/connectors/utils/pg.env.example b/ee/connectors/utils/pg.env.example new file mode 100644 index 000000000..e50b041f8 --- /dev/null +++ b/ee/connectors/utils/pg.env.example @@ -0,0 +1,10 @@ +connect_str='postgresql://{user}:{password}@{address}:{port}/{database}' +address=1.1.1.1 +port=8080 +database=dev +user=qwerty +password=qwertyQWERTY12345 +sessions_table=connector_user_sessions +events_table_name=connector_events +events_detailed_table_name=connector_events_detailed +level=normal diff --git a/ee/connectors/utils/redshift.env.example b/ee/connectors/utils/redshift.env.example new file mode 100644 index 000000000..d78b9a8a2 --- /dev/null +++ b/ee/connectors/utils/redshift.env.example @@ -0,0 +1,15 @@ +aws_access_key_id=QWERTYQWERTYQWERTY +aws_secret_access_key=abcdefgh12345678 +region_name=eu-central-3 +bucket=name_of_the_bucket +subdirectory=name_of_the_bucket_subdirectory +connect_str='postgresql://{user}:{password}@{address}:{port}/{schema}' +address=redshift-cluster-1.aaaaaaaaa.eu-central-3.redshift.amazonaws.com +port=5439 +schema=dev +user=admin +password=admin +sessions_table=connector_user_sessions +events_table_name=connector_events +events_detailed_table_name=connector_events_detailed +level=normal diff --git a/ee/connectors/utils/snowflake.env.example b/ee/connectors/utils/snowflake.env.example new file mode 100644 index 000000000..deed20462 --- /dev/null +++ b/ee/connectors/utils/snowflake.env.example @@ -0,0 +1,11 @@ +connect_str='snowflake://{user}:{password}@{account}/{database}/{schema}?warehouse={warehouse}' +user=admin +password=12345678 +account=aaaaaaa.eu-central-3 +database=dev +schema=public +warehouse=SOME_WH +sessions_table=connector_user_sessions +events_table_name=connector_events +events_detailed_table_name=connector_events_detailed +level=normal diff --git a/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql b/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql index 9010cb07a..e880024d3 100644 --- a/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql @@ -47,77 +47,78 @@ CREATE TABLE tenants CREATE TYPE user_role AS ENUM ('owner', 'admin', 'member'); CREATE TABLE users ( - user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - tenant_id integer NOT NULL REFERENCES tenants (tenant_id) ON DELETE CASCADE, - email text NOT NULL UNIQUE, - role user_role NOT NULL DEFAULT 'member', - name text NOT NULL, - created_at timestamp without time zone NOT NULL default (now() at time zone 'utc'), - deleted_at timestamp without time zone NULL DEFAULT NULL, - appearance jsonb NOT NULL default '{ + user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + tenant_id integer NOT NULL REFERENCES tenants (tenant_id) ON DELETE CASCADE, + email text NOT NULL UNIQUE, + role user_role NOT NULL DEFAULT 'member', + name text NOT NULL, + created_at timestamp without time zone NOT NULL default (now() at time zone 'utc'), + deleted_at timestamp without time zone NULL DEFAULT NULL, + appearance jsonb NOT NULL default '{ + "role": "dev", "dashboard": { - "applicationActivity": true, - "avgCpu": true, - "avgDomContentLoadStart": true, - "avgFirstContentfulPixel": false, - "avgFirstPaint": false, - "avgFps": false, - "avgImageLoadTime": true, - "avgPageLoadTime": true, - "avgPagesDomBuildtime": true, - "avgPagesResponseTime": false, - "avgRequestLoadTime": true, - "avgSessionDuration": false, - "avgTillFirstBit": false, - "avgTimeToInteractive": true, - "avgTimeToRender": true, - "avgUsedJsHeapSize": true, - "avgVisitedPages": false, - "busiestTimeOfDay": true, - "callsErrors_4xx": true, - "callsErrors_5xx": true, - "countSessions": true, - "cpu": true, - "crashes": true, - "errors": true, - "errorsPerDomains": true, - "errorsPerType": true, - "errorsTrend": true, + "cpu": false, "fps": false, - "impactedSessionsByJsErrors": true, - "impactedSessionsBySlowPages": true, - "memoryConsumption": true, - "missingResources": true, + "avgCpu": false, + "avgFps": false, + "errors": true, + "crashes": false, "overview": true, - "pageMetrics": true, - "pagesResponseTime": true, - "pagesResponseTimeDistribution": true, - "performance": true, - "resourceTypeVsResponseEnd": true, - "resourcesByParty": false, - "resourcesCountByType": true, - "resourcesLoadingTime": true, - "resourcesVsVisuallyComplete": true, "sessions": true, - "sessionsFeedback": false, - "sessionsFrustration": false, - "sessionsPerBrowser": false, - "slowestDomains": true, - "slowestImages": true, - "slowestResources": true, - "speedLocation": true, - "timeToRender": false, "topMetrics": true, - "userActivity": false + "callsErrors": false, + "pageMetrics": true, + "performance": true, + "timeToRender": false, + "userActivity": false, + "avgFirstPaint": false, + "countSessions": false, + "errorsPerType": false, + "slowestImages": true, + "speedLocation": false, + "slowestDomains": false, + "avgPageLoadTime": false, + "avgTillFirstBit": false, + "avgTimeToRender": false, + "avgVisitedPages": false, + "avgImageLoadTime": false, + "busiestTimeOfDay": true, + "errorsPerDomains": false, + "missingResources": false, + "resourcesByParty": false, + "sessionsFeedback": false, + "slowestResources": false, + "avgUsedJsHeapSize": false, + "domainsErrors_4xx": false, + "domainsErrors_5xx": false, + "memoryConsumption": false, + "pagesDomBuildtime": false, + "pagesResponseTime": false, + "avgRequestLoadTime": false, + "avgSessionDuration": false, + "sessionsPerBrowser": false, + "applicationActivity": true, + "sessionsFrustration": false, + "avgPagesDomBuildtime": false, + "avgPagesResponseTime": false, + "avgTimeToInteractive": false, + "resourcesCountByType": false, + "resourcesLoadingTime": false, + "avgDomContentLoadStart": false, + "avgFirstContentfulPixel": false, + "resourceTypeVsResponseEnd": false, + "impactedSessionsByJsErrors": false, + "impactedSessionsBySlowPages": false, + "resourcesVsVisuallyComplete": false, + "pagesResponseTimeDistribution": false }, - "runs": false, - "tests": false, - "pagesDomBuildtime": false + "sessionsLive": false, + "sessionsDevtools": true }'::jsonb, - api_key text UNIQUE default generate_api_key(20) not null, - jwt_iat timestamp without time zone NULL DEFAULT NULL, - data jsonb NOT NULL DEFAULT '{}'::jsonb, - weekly_report boolean NOT NULL DEFAULT TRUE + api_key text UNIQUE default generate_api_key(20) not null, + jwt_iat timestamp without time zone NULL DEFAULT NULL, + data jsonb NOT NULL DEFAULT '{}'::jsonb, + weekly_report boolean NOT NULL DEFAULT TRUE ); @@ -140,7 +141,7 @@ CREATE TABLE oauth_authentication provider oauth_provider NOT NULL, provider_user_id text NOT NULL, token text NOT NULL, - UNIQUE (provider, provider_user_id) + UNIQUE (user_id, provider) ); @@ -445,7 +446,6 @@ CREATE INDEX user_viewed_errors_error_id_idx ON public.user_viewed_errors (error -- --- sessions.sql --- - CREATE TYPE device_type AS ENUM ('desktop', 'tablet', 'mobile', 'other'); CREATE TYPE country AS ENUM ('UN', 'RW', 'SO', 'YE', 'IQ', 'SA', 'IR', 'CY', 'TZ', 'SY', 'AM', 'KE', 'CD', 'DJ', 'UG', 'CF', 'SC', 'JO', 'LB', 'KW', 'OM', 'QA', 'BH', 'AE', 'IL', 'TR', 'ET', 'ER', 'EG', 'SD', 'GR', 'BI', 'EE', 'LV', 'AZ', 'LT', 'SJ', 'GE', 'MD', 'BY', 'FI', 'AX', 'UA', 'MK', 'HU', 'BG', 'AL', 'PL', 'RO', 'XK', 'ZW', 'ZM', 'KM', 'MW', 'LS', 'BW', 'MU', 'SZ', 'RE', 'ZA', 'YT', 'MZ', 'MG', 'AF', 'PK', 'BD', 'TM', 'TJ', 'LK', 'BT', 'IN', 'MV', 'IO', 'NP', 'MM', 'UZ', 'KZ', 'KG', 'TF', 'HM', 'CC', 'PW', 'VN', 'TH', 'ID', 'LA', 'TW', 'PH', 'MY', 'CN', 'HK', 'BN', 'MO', 'KH', 'KR', 'JP', 'KP', 'SG', 'CK', 'TL', 'RU', 'MN', 'AU', 'CX', 'MH', 'FM', 'PG', 'SB', 'TV', 'NR', 'VU', 'NC', 'NF', 'NZ', 'FJ', 'LY', 'CM', 'SN', 'CG', 'PT', 'LR', 'CI', 'GH', 'GQ', 'NG', 'BF', 'TG', 'GW', 'MR', 'BJ', 'GA', 'SL', 'ST', 'GI', 'GM', 'GN', 'TD', 'NE', 'ML', 'EH', 'TN', 'ES', 'MA', 'MT', 'DZ', 'FO', 'DK', 'IS', 'GB', 'CH', 'SE', 'NL', 'AT', 'BE', 'DE', 'LU', 'IE', 'MC', 'FR', 'AD', 'LI', 'JE', 'IM', 'GG', 'SK', 'CZ', 'NO', 'VA', 'SM', 'IT', 'SI', 'ME', 'HR', 'BA', 'AO', 'NA', 'SH', 'BV', 'BB', 'CV', 'GY', 'GF', 'SR', 'PM', 'GL', 'PY', 'UY', 'BR', 'FK', 'GS', 'JM', 'DO', 'CU', 'MQ', 'BS', 'BM', 'AI', 'TT', 'KN', 'DM', 'AG', 'LC', 'TC', 'AW', 'VG', 'VC', 'MS', 'MF', 'BL', 'GP', 'GD', 'KY', 'BZ', 'SV', 'GT', 'HN', 'NI', 'CR', 'VE', 'EC', 'CO', 'PA', 'HT', 'AR', 'CL', 'BO', 'PE', 'MX', 'PF', 'PN', 'KI', 'TK', 'TO', 'WF', 'WS', 'NU', 'MP', 'GU', 'PR', 'VI', 'UM', 'AS', 'CA', 'US', 'PS', 'RS', 'AQ', 'SX', 'CW', 'BQ', 'SS'); CREATE TYPE platform AS ENUM ('web','ios','android'); @@ -456,7 +456,7 @@ CREATE TABLE sessions project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, tracker_version text NOT NULL, start_ts bigint NOT NULL, - duration integer NOT NULL, + duration integer NULL, rev_id text DEFAULT NULL, platform platform NOT NULL DEFAULT 'web', is_snippet boolean NOT NULL DEFAULT FALSE, @@ -508,6 +508,7 @@ CREATE INDEX ON sessions (project_id, metadata_7); CREATE INDEX ON sessions (project_id, metadata_8); CREATE INDEX ON sessions (project_id, metadata_9); CREATE INDEX ON sessions (project_id, metadata_10); +-- CREATE INDEX ON sessions (rehydration_id); CREATE INDEX ON sessions (project_id, watchdogs_score DESC); CREATE INDEX platform_idx ON public.sessions (platform); @@ -558,6 +559,18 @@ CREATE TABLE user_favorite_sessions ); +-- --- assignments.sql --- + +create table assigned_sessions +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + issue_id text NOT NULL, + provider oauth_provider NOT NULL, + created_by integer NOT NULL, + created_at timestamp default timezone('utc'::text, now()) NOT NULL, + provider_data jsonb default '{}'::jsonb NOT NULL +); + -- --- events_common.sql --- CREATE SCHEMA events_common; @@ -613,7 +626,6 @@ CREATE INDEX requests_url_gin_idx2 ON events_common.requests USING GIN (RIGHT(ur gin_trgm_ops); -- --- events.sql --- - CREATE SCHEMA events; CREATE TABLE events.pages @@ -636,6 +648,7 @@ CREATE TABLE events.pages time_to_interactive integer DEFAULT NULL, response_time bigint DEFAULT NULL, response_end bigint DEFAULT NULL, + ttfb integer DEFAULT NULL, PRIMARY KEY (session_id, message_id) ); CREATE INDEX ON events.pages (session_id); @@ -655,6 +668,11 @@ CREATE INDEX pages_base_referrer_gin_idx2 ON events.pages USING GIN (RIGHT(base_ gin_trgm_ops); CREATE INDEX ON events.pages (response_time); CREATE INDEX ON events.pages (response_end); +CREATE INDEX pages_path_gin_idx ON events.pages USING GIN (path gin_trgm_ops); +CREATE INDEX pages_path_idx ON events.pages (path); +CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0; +CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0; +CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0; CREATE TABLE events.clicks @@ -721,6 +739,61 @@ CREATE INDEX ON events.state_actions (name); CREATE INDEX state_actions_name_gin_idx ON events.state_actions USING GIN (name gin_trgm_ops); CREATE INDEX ON events.state_actions (timestamp); +CREATE TYPE events.resource_type AS ENUM ('other', 'script', 'stylesheet', 'fetch', 'img', 'media'); +CREATE TYPE events.resource_method AS ENUM ('GET' , 'HEAD' , 'POST' , 'PUT' , 'DELETE' , 'CONNECT' , 'OPTIONS' , 'TRACE' , 'PATCH' ); +CREATE TABLE events.resources +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + message_id bigint NOT NULL, + timestamp bigint NOT NULL, + duration bigint NULL, + type events.resource_type NOT NULL, + url text NOT NULL, + url_host text NOT NULL, + url_hostpath text NOT NULL, + success boolean NOT NULL, + status smallint NULL, + method events.resource_method NULL, + ttfb bigint NULL, + header_size bigint NULL, + encoded_body_size integer NULL, + decoded_body_size integer NULL, + PRIMARY KEY (session_id, message_id) +); +CREATE INDEX ON events.resources (session_id); +CREATE INDEX ON events.resources (timestamp); +CREATE INDEX ON events.resources (success); +CREATE INDEX ON events.resources (status); +CREATE INDEX ON events.resources (type); +CREATE INDEX ON events.resources (duration) WHERE duration > 0; +CREATE INDEX ON events.resources (url_host); + +CREATE INDEX resources_url_gin_idx ON events.resources USING GIN (url gin_trgm_ops); +CREATE INDEX resources_url_idx ON events.resources (url); +CREATE INDEX resources_url_hostpath_gin_idx ON events.resources USING GIN (url_hostpath gin_trgm_ops); +CREATE INDEX resources_url_hostpath_idx ON events.resources (url_hostpath); + + + +CREATE TABLE events.performance +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + message_id bigint NOT NULL, + min_fps smallint NOT NULL, + avg_fps smallint NOT NULL, + max_fps smallint NOT NULL, + min_cpu smallint NOT NULL, + avg_cpu smallint NOT NULL, + max_cpu smallint NOT NULL, + min_total_js_heap_size bigint NOT NULL, + avg_total_js_heap_size bigint NOT NULL, + max_total_js_heap_size bigint NOT NULL, + min_used_js_heap_size bigint NOT NULL, + avg_used_js_heap_size bigint NOT NULL, + max_used_js_heap_size bigint NOT NULL, + PRIMARY KEY (session_id, message_id) +); CREATE OR REPLACE FUNCTION events.funnel(steps integer[], m integer) RETURNS boolean AS @@ -762,4 +835,4 @@ CREATE INDEX autocomplete_type_idx ON public.autocomplete (type); CREATE INDEX autocomplete_value_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops); -COMMIT; \ No newline at end of file +COMMIT; diff --git a/frontend/app/Router.js b/frontend/app/Router.js index b2024c7d4..2706ca2e4 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -21,6 +21,7 @@ import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails'; import APIClient from './api_client'; import * as routes from './routes'; +import { OB_DEFAULT_TAB } from 'App/routes'; import Signup from './components/Signup/Signup'; import { fetchTenants } from 'Duck/user'; @@ -48,6 +49,7 @@ const SIGNUP_PATH = routes.signup(); const FORGOT_PASSWORD = routes.forgotPassword(); const CLIENT_PATH = routes.client(); const ONBOARDING_PATH = routes.onboarding(); +const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); @withRouter @connect((state) => { @@ -67,6 +69,7 @@ const ONBOARDING_PATH = routes.onboarding(); organisation: state.getIn([ 'user', 'client', 'name' ]), tenantId: state.getIn([ 'user', 'client', 'tenantId' ]), tenants: state.getIn(['user', 'tenants']), + onboarding: state.getIn([ 'user', 'onboarding' ]) }; }, { fetchUserInfo, fetchTenants @@ -92,7 +95,7 @@ class Router extends React.Component { } render() { - const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, tenants } = this.props; + const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, tenants, onboarding } = this.props; const siteIdList = sites.map(({ id }) => id).toJS(); const hideHeader = location.pathname && location.pathname.includes('/session/'); @@ -121,6 +124,9 @@ class Router extends React.Component { } } /> + { onboarding && + + } { siteIdList.length === 0 && } diff --git a/frontend/app/assets/apple-touch-icon.png b/frontend/app/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2adaf4f0d8494a9a15b508b280111f338755cd5b GIT binary patch literal 6253 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 literal 0 HcmV?d00001 diff --git a/frontend/app/assets/favicon-32x32.png b/frontend/app/assets/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..980396723f37de2afc0502752524bf2625bc64cd GIT binary patch literal 1090 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 literal 0 HcmV?d00001 diff --git a/frontend/app/assets/favicon@1x.png b/frontend/app/assets/favicon@1x.png deleted file mode 100644 index 393d5d3cc6c5763dff0a4c964456b2f9fc13e3c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2127 zcmV-V2(b5wP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$`xk*GpR9FdJS8HsQRTf>}ckiX8 zR0?IL0yPL0H1e#&W2_dzVGQ_85Mwk(CX-0iI59eA@Ga=>-6JJ3S@%=+o zq?RgUp_a!GF?OJ&v=ye*N=r*=d++SE_c`Z&aMF9vd#|E%GT$X9gYmV ze>dWxQUkQj3P?lF0F9clyg>ph0RctY#%aT^Q(_6SB;|<-+0KGYW>!qk1*DXV>6s8w zqhrUWv9Rhaj4X+DQy?Vk=&3iZ2Boj$g=~)d-8}1<>l47H8kNeH4xn#s+R5%@GDBA_ zD#07iRA5*pM(@cX{O#4lSpQZFPGw0>3jg(5TZYN+Jm9hH%Blia*CK&rxd=eb*_A@Yp0R>l%X zS;x}VJ&0v1_oAt#*9PQ!(VQ{Z{$dp#|NUeX7e)DucO*i12%=ug=@2JXkwl_eh{Sng zYuOpv0S@?l=P^{>UXRCKX+lrmpw48{G45VE2|Hg{fEzCxr`{1*fe7a0wnX*$;l_0x zm9S4tCfuMO^@8p@2_k9A{D36i`uCbK`;I!iUDGBC+5Fj~3b5g>i?DfhH7YAgoErJW zQzD+2=7%W}i4uC+WK0w=ChCF8CX)o#;^w$U=8haci97$k8%rMDj@<`;_KPrgaw$H0 zWC7OQT8S|u(;+OTNM3=Uu;Q4_yaK`|WNt9+5=~WNvdV;z*>Ru~i&oX*-hb`FvF?6d z9mf%t&p!|KtFOdu3o4Lu$*4AEKlfRJnp1vU72wfRc4S7W5=G?_17{Vdu>6X0WHRPa zF3RGyx@OFK^i#aNwFyI6+gDOF3{PJ_8(Z&Ng6b({3ap8#vQv5sxxb=CfxPDg*S;g5 zk#HL-%7^2*+h^dLbqjIz+_RN1*Z$mp3J<)~h|AY(##c>81N95Xj>5aQU4ehzG#}@T zE($qjIY$!aZ0s3%a5D2bkkceIQkpAbeqz}Oymt3Id~km?rcaQ|%Bl9<(Jn0AumvmL ztV3(pi9qDKX%n&SuIq76^-Q@3+)b(>WeyA_j-eP*SrQ>2zGgBv@DfRs>I;5}FCO|W zp1tK)C@aZW2xZ-e`;Tr(evA44*o6NaITm)^Fm*~$Omj|-yF5n5 z#D{`)vXGR@e})2nP2a#EHq`CM{*KPPT4J+oWFh{v;37;bEA>m!a{RchA%-|9_o%!_ z1fT*W;*v1zkVZ~`O_|d}z{}{}#zXk?Cyi+9>CyePk;z0@aq(3AvAPn)1;c`=A5WaX zeKlX0N={+9vV;J^8F#W0Fy?{3>9{-AIj52{*Kp_<9{HdF4Xyvnhe#Z`{QU9w%i>?7 zVoXT@9vB+JD|_~0T|+&3b8=^>=Qhm?RTLn2{wQkS=RjnHNU&RuTXRPb)_k%Tn|2-4 zuo&?iCjO!rPh2?%i_f1BfZ5o3s0EL0+lB)@KbeEP;-g5urDg!6Ejb{bYZzpZWY|s= zUPP3(`KNBoeez>d%dIOaNa3D^GjYcy(@>D+HEHEg*Kw?_`x={@4`}#kSFi+@QMZ~; zZjJ07>Ub7rhM(%p+cykCoJU;h|;I_&b% zJs#Yj5PHT!?*kR<>#&<6yb;lRASeH;<;H33Rf!xP{Ah=~^Zl4!J{n&)|A?1&?nCd; zfb%r(aqwIcSOI-}eXZC6Bx!CoW3pIc+AqIup7^XGgvB!3E-Vk~ zYdtvv$jnh$=^Ou4Z-V=AWutvc&(LI}s3tBbidV`%Bb}PL?iguYc>}7%E~|MuoI+N+ zKHXwb+1a3r21*bXp`qmbIBOZ`j8M2TZe6?Va3(GPrDk#)MU8g#OplUmW1UD&yS9DS z%N%1a#t5KB=^5LOjGlR!<(Rd#(H=+9MrqAzU9sZu{{e)${x&=TH<$nb002ovPDHLk FV1h*j@O%IO diff --git a/frontend/app/assets/favicon@2x.png b/frontend/app/assets/favicon@2x.png deleted file mode 100644 index c99e774af9b12cefee4d8c90798de5906107d2ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5829 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 P00000NkvXXu0mjfqNy)p diff --git a/frontend/app/assets/favicon@3x.png b/frontend/app/assets/favicon@3x.png deleted file mode 100644 index 4d38be71c0f2d2689f32492eeac4045458f2fec0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10941 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 diff --git a/frontend/app/assets/favicon@4x.png b/frontend/app/assets/favicon@4x.png deleted file mode 100644 index 19f3a4256df203350c7e2c3f5fb8a6e48f8f7431..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16394 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 diff --git a/frontend/app/assets/favicon@6x.png b/frontend/app/assets/favicon@6x.png deleted file mode 100644 index dbf51e62c2e1aad3de79f59abfc87082f5186554..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32078 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

{coPvv>_ZRCR&PKtg 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 A&;S4c diff --git a/frontend/app/assets/index.html b/frontend/app/assets/index.html index 3147d2337..f90b87ff2 100644 --- a/frontend/app/assets/index.html +++ b/frontend/app/assets/index.html @@ -5,12 +5,9 @@ - - - - - - + + + diff --git a/frontend/app/components/BugFinder/CustomFilters/FilterItem.js b/frontend/app/components/BugFinder/CustomFilters/FilterItem.js index e929a53c4..8b60b601c 100644 --- a/frontend/app/components/BugFinder/CustomFilters/FilterItem.js +++ b/frontend/app/components/BugFinder/CustomFilters/FilterItem.js @@ -6,7 +6,7 @@ import cn from 'classnames'; const FilterItem = ({ className = '', icon, label, onClick }) => { return (

- + { icon && } { label }
); diff --git a/frontend/app/components/BugFinder/DateRange.js b/frontend/app/components/BugFinder/DateRange.js index 4f2ce8e22..60e98ffa1 100644 --- a/frontend/app/components/BugFinder/DateRange.js +++ b/frontend/app/components/BugFinder/DateRange.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { applyFilter, fetchList } from 'Duck/filters'; +import { applyFilter } from 'Duck/filters'; import { fetchList as fetchFunnelsList } from 'Duck/funnels'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; @@ -8,11 +8,10 @@ import DateRangeDropdown from 'Shared/DateRangeDropdown'; startDate: state.getIn([ 'filters', 'appliedFilter', 'startDate' ]), endDate: state.getIn([ 'filters', 'appliedFilter', 'endDate' ]), }), { - applyFilter, fetchList, fetchFunnelsList + applyFilter, fetchFunnelsList }) export default class DateRange extends React.PureComponent { onDateChange = (e) => { - this.props.fetchList(e.rangeValue) this.props.fetchFunnelsList(e.rangeValue) this.props.applyFilter(e) } diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index c29cdd6a5..af5adf937 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -3,14 +3,15 @@ import { connect } from 'react-redux'; import cn from 'classnames'; import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI' import stl from './sessionMenu.css'; -import { fetchList, fetchWatchdogStatus } from 'Duck/watchdogs'; +import { fetchWatchdogStatus } from 'Duck/watchdogs'; import { setActiveFlow, clearEvents } from 'Duck/filters'; import { setActiveTab } from 'Duck/sessions'; +import { issues_types } from 'Types/session/issue' function SessionsMenu(props) { const { activeFlow, activeTab, watchdogs = [], keyMap, wdTypeCount, - fetchList, fetchWatchdogStatus, toggleRehydratePanel } = props; + fetchWatchdogStatus, toggleRehydratePanel } = props; const onMenuItemClick = (filter) => { props.onMenuItemClick(filter) @@ -21,7 +22,6 @@ function SessionsMenu(props) { } useEffect(() => { - fetchList() fetchWatchdogStatus() }, []) @@ -62,7 +62,7 @@ function SessionsMenu(props) { /> - { watchdogs.filter(item => item.visible).map(item => ( + { issues_types.filter(item => item.visible).map(item => ( ({ - watchdogs: state.getIn(['watchdogs', 'list']).sortBy(i => i.order), activeTab: state.getIn([ 'sessions', 'activeTab' ]), keyMap: state.getIn([ 'sessions', 'keyMap' ]), wdTypeCount: state.getIn([ 'sessions', 'wdTypeCount' ]), activeFlow: state.getIn([ 'filters', 'activeFlow' ]), captureRate: state.getIn(['watchdogs', 'captureRate']), }), { - fetchList, fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab + fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab })(SessionsMenu); diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 754146a36..16586fd1d 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,20 +1,22 @@ import React from 'react' import { connect } from 'react-redux' -import { edit, save, init } from 'Duck/integrations/slack' +import { edit, save, init, update } from 'Duck/integrations/slack' import { Form, Input, Button, Message } from 'UI' import { confirm } from 'UI/Confirmation'; import { remove } from 'Duck/integrations/slack' class SlackAddForm extends React.PureComponent { - componentWillUnmount() { this.props.init({}); } save = () => { - this.props.save(this.props.instance).then(function() { - - }) + const instance = this.props.instance; + if(instance.exists()) { + this.props.update(this.props.instance) + } else { + this.props.save(this.props.instance) + } } remove = async (id) => { @@ -102,4 +104,4 @@ export default connect(state => ({ instance: state.getIn(['slack', 'instance']), saving: state.getIn(['slack', 'saveRequest', 'loading']), errors: state.getIn([ 'slack', 'saveRequest', 'errors' ]), -}), { edit, save, init, remove })(SlackAddForm) \ No newline at end of file +}), { edit, save, init, remove, update })(SlackAddForm) \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index 88529ce58..e854dfce2 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,7 +1,8 @@ import React from 'react' import { connect } from 'react-redux' -import { TextEllipsis, NoContent } from 'UI'; +import { NoContent } from 'UI'; import { remove, edit } from 'Duck/integrations/slack' +import DocLink from 'Shared/DocLink/DocLink'; function SlackChannelList(props) { const { list } = props; @@ -14,7 +15,12 @@ function SlackChannelList(props) { return (
+
Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.
+ +
+ } size="small" show={ list.size === 0 } > @@ -24,21 +30,12 @@ function SlackChannelList(props) { className="border-t px-5 py-2 flex items-center justify-between cursor-pointer" onClick={() => onEdit(c)} > -
+
{c.name}
- - {c.endpoint} -
- } - /> +
+ {c.endpoint} +
- {/*
- -
*/} ))} diff --git a/frontend/app/components/Client/ManageUsers/ManageUsers.js b/frontend/app/components/Client/ManageUsers/ManageUsers.js index c8d1c633d..9f0a4244d 100644 --- a/frontend/app/components/Client/ManageUsers/ManageUsers.js +++ b/frontend/app/components/Client/ManageUsers/ManageUsers.js @@ -7,6 +7,7 @@ import styles from './manageUsers.css'; import UserItem from './UserItem'; import { confirm } from 'UI/Confirmation'; import { toast } from 'react-toastify'; +import BannerMessage from 'Shared/BannerMessage'; const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; const LIMIT_WARNING = 'You have reached users limit.'; @@ -38,7 +39,7 @@ class ManageUsers extends React.PureComponent { } adminLabel = (user) => { - if (user.superAdmin) return 'Super Admin'; + if (user.superAdmin) return 'Owner'; return user.admin ? 'Admin' : ''; }; @@ -158,28 +159,37 @@ class ManageUsers extends React.PureComponent { onClose={ this.closeModal } />
-
- { !hideHeader &&

{ (isAdmin ? 'Manage ' : '') + 'Users' }

} - { hideHeader &&

{ `Team Size ${members.size}` }

} - - this.init() } - /> -
+
+
+ { !hideHeader &&

{ (isAdmin ? 'Manage ' : '') + 'Users' }

} + { hideHeader &&

{ `Team Size ${members.size}` }

} + + this.init() } + /> +
+ } + // disabled={ canAddUsers } + content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` } + size="tiny" + inverted + position="top left" + /> +
+
+ { !account.smtp && + + Inviting new users require email messaging. Please setup SMTP. + } - // disabled={ canAddUsers } - content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` } - size="tiny" - inverted - position="top left" - /> +
setTab(CLIENT_TABS.NOTIFICATIONS) } /> diff --git a/frontend/app/components/Client/ProfileSettings/OptOut.js b/frontend/app/components/Client/ProfileSettings/OptOut.js index dea675a60..6e4643d7b 100644 --- a/frontend/app/components/Client/ProfileSettings/OptOut.js +++ b/frontend/app/components/Client/ProfileSettings/OptOut.js @@ -6,7 +6,7 @@ import { updateClient } from 'Duck/user' function OptOut(props) { const { optOut } = props; const onChange = () => { - props.updateClient({ optOut: !optOut, name: 'OpenReplay' }) + props.updateClient({ optOut: !optOut }) } return (
diff --git a/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css b/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css index cf1b14578..dde6009e4 100644 --- a/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css +++ b/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css @@ -1,5 +1,5 @@ .bar { - height: 10px; + height: 5px; width: 100%; border-radius: 3px; display: flex; diff --git a/frontend/app/components/Errors/Error/ErrorInfo.js b/frontend/app/components/Errors/Error/ErrorInfo.js index c9681f25d..4726bc613 100644 --- a/frontend/app/components/Errors/Error/ErrorInfo.js +++ b/frontend/app/components/Errors/Error/ErrorInfo.js @@ -18,7 +18,7 @@ import SideSection from './SideSection'; export default class ErrorInfo extends React.PureComponent { ensureInstance() { const { errorId, loading, errorOnFetch } = this.props; - if (!loading && !errorOnFetch && + if (!loading && this.props.errorIdInStore !== errorId && errorId != null) { this.props.fetch(errorId); diff --git a/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js b/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js index 18702f5aa..0a8a997dd 100644 --- a/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js +++ b/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js @@ -34,9 +34,9 @@ const FunnelDetails = (props) => { useEffect(() => { if (funnels.size === 0) { - props.fetchList(); - props.fetchIssueTypes() + props.fetchList(); } + props.fetchIssueTypes() props.fetch(funnelId).then(() => { setMounted(true); diff --git a/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js b/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js index a747c9905..b7d140b1b 100644 --- a/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js +++ b/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js @@ -108,6 +108,7 @@ const FunnelHeader = (props) => { startDate={funnelFilters.startDate} endDate={funnelFilters.endDate} onDateChange={onDateChange} + customRangeRight />
diff --git a/frontend/app/components/Header/Discover/featureItem.css b/frontend/app/components/Header/Discover/featureItem.css index 0c0d54b9c..d434c8f91 100644 --- a/frontend/app/components/Header/Discover/featureItem.css +++ b/frontend/app/components/Header/Discover/featureItem.css @@ -1,5 +1,5 @@ .wrapper { - padding: 10px 0; + padding: 7px 0; } .checkbox { diff --git a/frontend/app/components/Header/OnboardingExplore/FeatureItem.js b/frontend/app/components/Header/OnboardingExplore/FeatureItem.js index bbc31b3ad..cbb0c3472 100644 --- a/frontend/app/components/Header/OnboardingExplore/FeatureItem.js +++ b/frontend/app/components/Header/OnboardingExplore/FeatureItem.js @@ -6,7 +6,7 @@ import stl from './featureItem.css'; const FeatureItem = ({ label, completed = false, subText, onClick }) => { return (
diff --git a/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js b/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js index a6893fc17..c6d7aa179 100644 --- a/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js +++ b/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js @@ -121,7 +121,7 @@ class OnboardingExplore extends React.PureComponent {
- Follow the steps below to complete this project setup and make the best out of OpenReplay. + Make the best out of OpenReplay by completing your project setup:
@@ -131,7 +131,7 @@ class OnboardingExplore extends React.PureComponent { key={ task.task } label={ task.task } completed={ task.done } - onClick={task.URL && (() => this.onClick(task)) } + onClick={() => this.onClick(task) } /> ))}
diff --git a/frontend/app/components/Header/OnboardingExplore/featureItem.css b/frontend/app/components/Header/OnboardingExplore/featureItem.css index e0b005408..b0fe2dbb9 100644 --- a/frontend/app/components/Header/OnboardingExplore/featureItem.css +++ b/frontend/app/components/Header/OnboardingExplore/featureItem.css @@ -1,5 +1,5 @@ .wrapper { - padding: 10px 0; + padding: 6px 0; display: flex; align-items: center; } diff --git a/frontend/app/components/Login/Login.js b/frontend/app/components/Login/Login.js index f79b2fc05..bd619bae5 100644 --- a/frontend/app/components/Login/Login.js +++ b/frontend/app/components/Login/Login.js @@ -63,7 +63,7 @@ export default class Login extends React.Component {

Login to OpenReplay

- { tenants.length === 0 &&
Don't have an account?Sign up
} + { tenants.length === 0 &&
Don't have an account? Sign up
}
{ window.ENV.CAPTCHA_ENABLED && ( diff --git a/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js b/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js index 0a048e3cb..d9d24c7df 100644 --- a/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js +++ b/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js @@ -5,6 +5,7 @@ import { Button } from 'UI' import { OB_TABS, onboarding as onboardingRoute } from 'App/routes' import * as routes from '../../../../routes' import { sessions } from 'App/routes'; +import { setOnboarding } from 'Duck/user'; const withSiteId = routes.withSiteId; const MENU_ITEMS = [OB_TABS.INSTALLING, OB_TABS.IDENTIFY_USERS, OB_TABS.MANAGE_USERS, OB_TABS.INTEGRATIONS] @@ -25,9 +26,14 @@ const OnboardingNavButton = (props) => { const tab = MENU_ITEMS[activeIndex+1] history.push(withSiteId(onboardingRoute(tab), siteId)); } else { - history.push(sessions()); + onDone() } } + + const onDone = () => { + props.setOnboarding(false); + history.push(sessions()); + } return ( <> @@ -35,7 +41,7 @@ const OnboardingNavButton = (props) => { primary size="small" plain - onClick={() => history.push(sessions())} + onClick={onDone} > {activeIndex === 0 ? 'Done. See Recorded Sessions' : 'Skip Optional Steps and See Recorded Sessions'} @@ -53,4 +59,4 @@ const OnboardingNavButton = (props) => { ) } -export default withRouter(OnboardingNavButton) \ No newline at end of file +export default withRouter(connect(null, { setOnboarding })(OnboardingNavButton)) \ No newline at end of file diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js index a093c4f94..755718ef0 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -28,9 +28,10 @@ const codeSnippet = ` r.setMetadata=function(k,v){r.push([4,k,v])}; r.event=function(k,p,i){r.push([5,k,p,i])}; r.issue=function(k,p){r.push([6,k,p])}; - r.isActive=r.active=function(){return false}; - r.getSessionToken=r.sessionID=function(){}; -})(0,PROJECT_HASH,"//${window.location.hostname}/static/openreplay.js",1,XXX); + r.isActive=function(){return false}; + r.getSessionToken=function(){}; + r.i="https://${window.location.hostname}/ingest"; +})(0, "PROJECT_KEY", "//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",1,XXX); `; diff --git a/frontend/app/components/Session_/Issues/IssueDetails.js b/frontend/app/components/Session_/Issues/IssueDetails.js index 111dd15fe..f91f0ad73 100644 --- a/frontend/app/components/Session_/Issues/IssueDetails.js +++ b/frontend/app/components/Session_/Issues/IssueDetails.js @@ -14,9 +14,9 @@ class IssueDetails extends React.PureComponent { write = (e, { name, value }) => this.setState({ [ name ]: value }); render() { - const { sessionId, issue, loading, users, issueTypeIcons, provider } = this.props; + const { sessionId, issue, loading, users, issueTypeIcons, issuesIntegration } = this.props; const activities = issue.activities; - + const provider = issuesIntegration.provider; const assignee = users.filter(({id}) => issue.assignee === id).first(); return ( @@ -53,5 +53,5 @@ export default connect(state => ({ users: state.getIn(['assignments', 'users']), loading: state.getIn(['assignments', 'fetchAssignment', 'loading']), issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']), - provider: state.getIn([ 'issues', 'list']).provider, + issuesIntegration: state.getIn([ 'issues', 'list']).first() || {}, }))(IssueDetails); diff --git a/frontend/app/components/Session_/Issues/IssueForm.js b/frontend/app/components/Session_/Issues/IssueForm.js index f305c3ee7..81dda0504 100644 --- a/frontend/app/components/Session_/Issues/IssueForm.js +++ b/frontend/app/components/Session_/Issues/IssueForm.js @@ -7,7 +7,8 @@ import { addActivity, init, edit, fetchAssignments, fetchMeta } from 'Duck/assig const SelectedValue = ({ icon, text }) => { return(
- + {/* */} + { icon } { text }
) @@ -37,7 +38,7 @@ class IssueForm extends React.PureComponent { addActivity(sessionId, instance).then(() => { const { errors } = this.props; - if (errors.length === 0) { + if (!errors || errors.length === 0) { this.props.init({projectId: instance.projectId}); this.props.fetchAssignments(sessionId); this.props.closeHandler(); @@ -52,8 +53,9 @@ class IssueForm extends React.PureComponent { const { creating, projects, users, issueTypes, instance, closeHandler, metaLoading } = this.props; const projectOptions = projects.map(({name, id}) => ({text: name, value: id })).toArray(); const userOptions = users.map(({name, id}) => ({text: name, value: id })).toArray(); - const issueTypeOptions = issueTypes.map(({name, id, iconUrl }) => { - return {text: name, value: id, iconUrl, icon: } + + const issueTypeOptions = issueTypes.map(({name, id, iconUrl, color }) => { + return {text: name, value: id, iconUrl, color } }).toArray(); const selectedIssueType = issueTypes.filter(issue => issue.id == instance.issueType).first(); @@ -80,6 +82,7 @@ class IssueForm extends React.PureComponent { { {/* */} {/* */}
- + { typeIcon } + {/* */} { issue.id } {/*
{ '@ 00:13 Secs'}
*/} { assignee && diff --git a/frontend/app/components/Session_/Issues/IssueListItem.js b/frontend/app/components/Session_/Issues/IssueListItem.js index 9145ffaa5..51b5bb25c 100644 --- a/frontend/app/components/Session_/Issues/IssueListItem.js +++ b/frontend/app/components/Session_/Issues/IssueListItem.js @@ -11,7 +11,8 @@ const IssueListItem = ({ issue, onClick, icon, user, active }) => { >
- + { icon } + {/* */} { issue.id }
diff --git a/frontend/app/components/Session_/Issues/Issues.js b/frontend/app/components/Session_/Issues/Issues.js index 66fc80bc8..bba3eaf40 100644 --- a/frontend/app/components/Session_/Issues/Issues.js +++ b/frontend/app/components/Session_/Issues/Issues.js @@ -21,7 +21,7 @@ import stl from './issues.css'; fetchIssueLoading: state.getIn(['assignments', 'fetchAssignment', 'loading']), fetchIssuesLoading: state.getIn(['assignments', 'fetchAssignments', 'loading']), projectsLoading: state.getIn(['assignments', 'fetchProjects', 'loading']), - provider: state.getIn([ 'issues', 'list']).provider, + issuesIntegration: state.getIn([ 'issues', 'list']).first() || {}, }), { fetchAssigment, fetchAssignments, fetchMeta, fetchProjects }) @withToggle('isModalDisplayed', 'toggleModal') class Issues extends React.Component { @@ -64,9 +64,10 @@ class Issues extends React.Component { render() { const { sessionId, activeIssue, isModalDisplayed, projectsLoading, - fetchIssueLoading, issues, metaLoading, fetchIssuesLoading, provider + fetchIssueLoading, issues, metaLoading, fetchIssuesLoading, issuesIntegration } = this.props; const { showModal } = this.state; + const provider = issuesIntegration.provider return (
diff --git a/frontend/app/components/Session_/Network/Network.js b/frontend/app/components/Session_/Network/Network.js index b3ea3dafb..4c30e6f32 100644 --- a/frontend/app/components/Session_/Network/Network.js +++ b/frontend/app/components/Session_/Network/Network.js @@ -158,30 +158,30 @@ export default class Network extends React.PureComponent { let filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ])); - const referenceLines = []; - if (domContentLoadedTime != null) { - referenceLines.push({ - time: domContentLoadedTime, - color: DOM_LOADED_TIME_COLOR, - }) - } - if (loadTime != null) { - referenceLines.push({ - time: loadTime, - color: LOAD_TIME_COLOR, - }) - } - - let tabs = TABS; - if (!fetchPresented) { - tabs = TABS.map(tab => tab.key === XHR - ? { - text: renderXHRText(), - key: XHR, - } - : tab - ); - } +// const referenceLines = []; +// if (domContentLoadedTime != null) { +// referenceLines.push({ +// time: domContentLoadedTime, +// color: DOM_LOADED_TIME_COLOR, +// }) +// } +// if (loadTime != null) { +// referenceLines.push({ +// time: loadTime, +// color: LOAD_TIME_COLOR, +// }) +// } +// +// let tabs = TABS; +// if (!fetchPresented) { +// tabs = TABS.map(tab => tab.key === XHR +// ? { +// text: renderXHRText(), +// key: XHR, +// } +// : tab +// ); +// } const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0); const transferredSize = filtered diff --git a/frontend/app/components/Session_/Network/NetworkContent.js b/frontend/app/components/Session_/Network/NetworkContent.js index 1f10eaffe..7eeebf538 100644 --- a/frontend/app/components/Session_/Network/NetworkContent.js +++ b/frontend/app/components/Session_/Network/NetworkContent.js @@ -168,13 +168,13 @@ export default class NetworkContent extends React.PureComponent { const referenceLines = []; if (domContentLoadedTime != null) { referenceLines.push({ - time: domContentLoadedTime, + time: domContentLoadedTime.time, color: DOM_LOADED_TIME_COLOR, }) } if (loadTime != null) { referenceLines.push({ - time: loadTime, + time: loadTime.time, color: LOAD_TIME_COLOR, }) } @@ -239,13 +239,13 @@ export default class NetworkContent extends React.PureComponent { /> diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index aeab1af64..3589be148 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -18,12 +18,14 @@ const getPointerIcon = (type) => { case 'log': return 'funnel/exclamation-circle'; case 'stack': - return 'funnel/file-exclamation'; + return 'funnel/patch-exclamation-fill'; case 'resource': return 'funnel/file-medical-alt'; case 'dead_click': return 'funnel/dizzy'; + case 'click_rage': + return 'funnel/dizzy'; case 'excessive_scrolling': return 'funnel/mouse'; case 'bad_request': @@ -61,6 +63,7 @@ const getPointerIcon = (type) => { fetchList: state.fetchList, })) @connect(state => ({ + issues: state.getIn([ 'sessions', 'current', 'issues' ]), showDevTools: state.getIn([ 'user', 'account', 'appearance', 'sessionsDevtools' ]), clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) && state.getIn([ 'sessions', 'current', 'clickRageTime' ]), @@ -95,6 +98,7 @@ export default class Timeline extends React.PureComponent { clickRageTime, stackList, fetchList, + issues } = this.props; const scale = 100 / endTime; @@ -124,6 +128,28 @@ export default class Timeline extends React.PureComponent { /> )) } + { + issues.map(iss => ( +
+ + { iss.name } +
+ } + /> +
+ )) + } { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => (
- { "Exception:" } + { "Exception" }
{ e.message }
@@ -278,7 +304,7 @@ export default class Timeline extends React.PureComponent { icon={getPointerIcon('log')} content={
- { "Console:" } + { "Console" }
{ l.value }
@@ -380,7 +406,7 @@ export default class Timeline extends React.PureComponent { icon={getPointerIcon('fetch')} content={
- { "Failed Fetch:" } + { "Failed Fetch" }
{ e.name }
@@ -421,7 +447,7 @@ export default class Timeline extends React.PureComponent { icon={getPointerIcon('stack')} content={
- { "Stack Event:" } + { "Stack Event" }
{ e.name }
diff --git a/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js b/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js index dc58d8b81..93a901f0a 100644 --- a/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js +++ b/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js @@ -46,7 +46,7 @@ export default class UserEvent extends React.PureComponent { case STACKDRIVER: return ; default: - return ; + return ; } } diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.js b/frontend/app/components/Signup/SignupForm/SignupForm.js index b5ea500f3..0a5de9507 100644 --- a/frontend/app/components/Signup/SignupForm/SignupForm.js +++ b/frontend/app/components/Signup/SignupForm/SignupForm.js @@ -137,7 +137,7 @@ export default class SignupForm extends React.Component {
-
By creating an account, you agree to our Terms of Service and Privacy Policy
+
By creating an account, you agree to our Terms of Service and Privacy Policy.
diff --git a/frontend/app/components/shared/BannerMessage/BannerMessage.js b/frontend/app/components/shared/BannerMessage/BannerMessage.js new file mode 100644 index 000000000..d1d66b991 --- /dev/null +++ b/frontend/app/components/shared/BannerMessage/BannerMessage.js @@ -0,0 +1,28 @@ +import React from 'react' +import { Icon } from 'UI' + +const BannerMessage= (props) => { + const { icon = 'info-circle', children } = props; + + return ( + <> +
+
+
+
+ +
+
+ {children} +
+
+
+
+ + ) +} + +export default BannerMessage; \ No newline at end of file diff --git a/frontend/app/components/shared/BannerMessage/index.js b/frontend/app/components/shared/BannerMessage/index.js new file mode 100644 index 000000000..4d6ad92b8 --- /dev/null +++ b/frontend/app/components/shared/BannerMessage/index.js @@ -0,0 +1 @@ +export { default } from './BannerMessage' \ No newline at end of file diff --git a/frontend/app/components/shared/DateRange.js b/frontend/app/components/shared/DateRange.js index 83feae0d8..7b627ab28 100644 --- a/frontend/app/components/shared/DateRange.js +++ b/frontend/app/components/shared/DateRange.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; function DateRange (props) { - const { startDate, endDate, rangeValue, className, onDateChange } = props; + const { startDate, endDate, rangeValue, className, onDateChange, customRangeRight=false } = props; return ( ); } diff --git a/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js b/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js index 4165506d4..f29d47745 100644 --- a/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js +++ b/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js @@ -66,7 +66,7 @@ export default class DateRangeDropdown extends React.PureComponent { } render() { - const { button = false, className, direction = 'right', customHidden=false, show30Minutes=false } = this.props; + const { customRangeRight, button = false, className, direction = 'right', customHidden=false, show30Minutes=false } = this.props; const { showDateRangePopup, value, range } = this.state; let options = getDateRangeOptions(range); @@ -108,7 +108,7 @@ export default class DateRangeDropdown extends React.PureComponent { { showDateRangePopup && -
+
{ @@ -9,7 +9,10 @@ export default function DocLink({ className = '', url, label }) { return (
) diff --git a/frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js b/frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js new file mode 100644 index 000000000..c308f33bf --- /dev/null +++ b/frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js @@ -0,0 +1,26 @@ +import React from 'react' +import { connect } from 'react-redux' +import { IconButton } from 'UI' +import { CLIENT_TABS, client as clientRoute } from 'App/routes'; +import { withRouter } from 'react-router-dom'; + +function IntegrateSlackButton({ history, tenantId }) { + const gotoPreferencesIntegrations = () => { + history.push(clientRoute(CLIENT_TABS.INTEGRATIONS)); + } + + return ( +
+ +
+ ) +} + +export default withRouter(connect(state => ({ + tenantId: state.getIn([ 'user', 'client', 'tenantId' ]), +}))(IntegrateSlackButton)) diff --git a/frontend/app/components/shared/IntegrateSlackButton/index.js b/frontend/app/components/shared/IntegrateSlackButton/index.js new file mode 100644 index 000000000..f2f8f2e16 --- /dev/null +++ b/frontend/app/components/shared/IntegrateSlackButton/index.js @@ -0,0 +1 @@ +export { default } from './IntegrateSlackButton' \ No newline at end of file diff --git a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js index a3011db44..cee55088b 100644 --- a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js +++ b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js @@ -23,7 +23,7 @@ const NoSessionsMessage= (props) => {
- It takes a few minutes for first recordings to appear. All set but they are still not showing up? Check our troubleshooting section. + It takes a few minutes for first recordings to appear. All set but they are still not showing up? Check our troubleshooting section.
diff --git a/frontend/app/components/shared/SharePopup/SharePopup.js b/frontend/app/components/shared/SharePopup/SharePopup.js index 845229034..347a95733 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.js +++ b/frontend/app/components/shared/SharePopup/SharePopup.js @@ -4,6 +4,7 @@ import withRequest from 'HOCs/withRequest'; import { Popup, Dropdown, Icon, IconButton } from 'UI'; import { pause } from 'Player'; import styles from './sharePopup.css'; +import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton'; @connect(state => ({ channels: state.getIn([ 'slack', 'list' ]), @@ -18,7 +19,7 @@ export default class SharePopup extends React.PureComponent { state = { comment: '', isOpen: false, - channelId: this.props.channels.getIn([ 0, 'id' ]), + channelId: this.props.channels.getIn([ 0, 'webhookId' ]), } editMessage = e => this.setState({ comment: e.target.value }) @@ -45,10 +46,10 @@ export default class SharePopup extends React.PureComponent { changeChannel = (e, { value }) => this.setState({ channelId: value }) render() { - const { trigger, loading, channels, tenantId } = this.props; + const { trigger, loading, channels } = this.props; const { comment, isOpen, channelId } = this.state; - const options = channels.map(({ id, name }) => ({ value: id, text: name })).toJS(); + const options = channels.map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS(); return ( { options.length === 0 ?
- - - +
:
diff --git a/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js index bacb71bfe..350378cf4 100644 --- a/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -27,9 +27,10 @@ const codeSnippet = ` r.setMetadata=function(k,v){r.push([4,k,v])}; r.event=function(k,p,i){r.push([5,k,p,i])}; r.issue=function(k,p){r.push([6,k,p])}; - r.isActive=r.active=function(){return false}; - r.getSessionToken=r.sessionID=function(){}; -})(0,PROJECT_HASH,"//${window.location.hostname}/static/openreplay.js",1,XXX); + r.isActive=function(){return false}; + r.getSessionToken=function(){}; + r.i="https://${window.location.hostname}/ingest"; +})(0, "PROJECT_KEY", "//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",1,XXX); `; diff --git a/frontend/app/components/ui/ErrorDetails/ErrorDetails.js b/frontend/app/components/ui/ErrorDetails/ErrorDetails.js index 68f48cb82..2a6afdd1e 100644 --- a/frontend/app/components/ui/ErrorDetails/ErrorDetails.js +++ b/frontend/app/components/ui/ErrorDetails/ErrorDetails.js @@ -4,7 +4,7 @@ import cn from 'classnames'; import { IconButton, Icon } from 'UI'; import { connect } from 'react-redux'; -const docLink = 'https://docs.openreplay.com/plugins/sourcemaps'; +const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps'; function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }) { const [showRaw, setShowRaw] = useState(false) diff --git a/frontend/app/duck/assignments.js b/frontend/app/duck/assignments.js index c6c7d3cda..b48e2ff45 100644 --- a/frontend/app/duck/assignments.js +++ b/frontend/app/duck/assignments.js @@ -5,6 +5,7 @@ import withRequestState, { RequestTypes } from './requestStateCreator'; import { createListUpdater, createItemInListUpdater } from './funcTools/tools'; import { editType, initType } from './funcTools/crud/types'; import { createInit, createEdit } from './funcTools/crud'; +import IssuesType from 'Types/issue/issuesType' const idKey = 'id'; const name = 'assignment'; @@ -41,17 +42,20 @@ const reducer = (state = initialState, action = {}) => { return state.mergeIn([ 'instance' ], action.instance); case FETCH_PROJECTS.SUCCESS: return state.set('projects', List(action.data)).set('projectsFetched', true); - case FETCH_ASSIGNMENTS.SUCCESS: - return state.set('list', List(action.data).map(Assignment)); + case FETCH_ASSIGNMENTS.SUCCESS: + return state.set('list', List(action.data.issues).map(Assignment)); case FETCH_ASSIGNMENT.SUCCESS: return state.set('activeIssue', Assignment({ ...action.data, users})); case FETCH_META.SUCCESS: - issueTypes = action.data.issueTypes; + issueTypes = List(action.data.issueTypes).map(IssuesType); var issueTypeIcons = {} - for (var i =0; i < issueTypes.length; i++) { - issueTypeIcons[issueTypes[i].id] = issueTypes[i].iconUrl - } - return state.set('issueTypes', List(issueTypes)) + // for (var i =0; i < issueTypes.length; i++) { + // issueTypeIcons[issueTypes[i].id] = issueTypes[i].iconUrl + // } + issueTypes.forEach(iss => { + issueTypeIcons[iss.id] = iss.iconUrl + }) + return state.set('issueTypes', issueTypes) .set('users', List(action.data.users)) .set('issueTypeIcons', issueTypeIcons) case ADD_ACTIVITY.SUCCESS: diff --git a/frontend/app/duck/integrations/slack.js b/frontend/app/duck/integrations/slack.js index 1d59bc16b..e4c2803ff 100644 --- a/frontend/app/duck/integrations/slack.js +++ b/frontend/app/duck/integrations/slack.js @@ -4,6 +4,7 @@ import Config from 'Types/integrations/slackConfig'; import { createItemInListUpdater } from '../funcTools/tools'; const SAVE = new RequestTypes('slack/SAVE'); +const UPDATE = new RequestTypes('slack/UPDATE'); const REMOVE = new RequestTypes('slack/REMOVE'); const FETCH_LIST = new RequestTypes('slack/FETCH_LIST'); const EDIT = 'slack/EDIT'; @@ -20,6 +21,7 @@ const reducer = (state = initialState, action = {}) => { switch (action.type) { case FETCH_LIST.SUCCESS: return state.set('list', List(action.data).map(Config)); + case UPDATE.SUCCESS: case SAVE.SUCCESS: const config = Config(action.data); return state @@ -57,6 +59,13 @@ export function save(instance) { }; } +export function update(instance) { + return { + types: UPDATE.toArray(), + call: client => client.put(`/integrations/slack/${instance.webhookId}`, instance.toData()), + }; +} + export function edit(instance) { return { type: EDIT, diff --git a/frontend/app/duck/user.js b/frontend/app/duck/user.js index f7ed2b1a2..deb41e715 100644 --- a/frontend/app/duck/user.js +++ b/frontend/app/duck/user.js @@ -19,6 +19,7 @@ const PUT_CLIENT = new RequestTypes('user/PUT_CLIENT'); const PUSH_NEW_SITE = 'user/PUSH_NEW_SITE'; const SET_SITE_ID = 'user/SET_SITE_ID'; +const SET_ONBOARDING = 'user/SET_ONBOARDING'; const SITE_ID_STORAGE_KEY = "__$user-siteId$__"; const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY); @@ -29,7 +30,8 @@ const initialState = Map({ siteId: null, passwordRequestError: false, passwordErrors: List(), - tenants: [] + tenants: [], + onboarding: false }); const setClient = (state, data) => { @@ -47,17 +49,21 @@ const setClient = (state, data) => { const reducer = (state = initialState, action = {}) => { switch (action.type) { - case SIGNUP.SUCCESS: + case UPDATE_PASSWORD.SUCCESS: case LOGIN.SUCCESS: return setClient( state.set('account', Account(action.data.user)), action.data.client, ); + case SIGNUP.SUCCESS: + return setClient( + state.set('account', Account(action.data.user)), + action.data.client, + ).set('onboarding', true); case REQUEST_RESET_PASSWORD.SUCCESS: break; case UPDATE_APPEARANCE.REQUEST: //TODO: failure handling return state.mergeIn([ 'account', 'appearance' ], action.appearance) - case UPDATE_PASSWORD.SUCCESS: case UPDATE_ACCOUNT.SUCCESS: case FETCH_ACCOUNT.SUCCESS: return state.set('account', Account(action.data)).set('passwordErrors', List()); @@ -77,6 +83,8 @@ const reducer = (state = initialState, action = {}) => { case PUSH_NEW_SITE: return state.updateIn([ 'client', 'sites' ], list => list.push(action.newSite)); + case SET_ONBOARDING: + return state.set('onboarding', action.state) } return state; }; @@ -187,3 +195,11 @@ export function pushNewSite(newSite) { newSite, }; } + +export function setOnboarding(state = false) { + return { + type: SET_ONBOARDING, + state + }; +} + diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.js b/frontend/app/player/MessageDistributor/MessageDistributor.js index fa3641b7a..c21d54ccb 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.js +++ b/frontend/app/player/MessageDistributor/MessageDistributor.js @@ -10,7 +10,7 @@ import ReduxAction from 'Types/session/reduxAction'; import { update } from '../store'; import { - init as initLists, + init as initListsDepr, append as listAppend, setStartTime as setListsStartTime } from '../lists'; @@ -43,6 +43,14 @@ export const INITIAL_STATE = { skipIntervals: [], } +function initLists() { + const lists = {}; + for (var i = 0; i < LIST_NAMES.length; i++) { + lists[ LIST_NAMES[i] ] = new ListWalker(); + } + return lists; +} + import type { Message, @@ -78,16 +86,7 @@ export default class MessageDistributor extends StatedScreen { #scrollManager: ListWalker = new ListWalker(); #decoder = new Decoder(); - #lists = { - redux: new ListWalker(), - mobx: new ListWalker(), - vuex: new ListWalker(), - ngrx: new ListWalker(), - graphql: new ListWalker(), - exceptions: new ListWalker(), - profiles: new ListWalker(), - longtasks: new ListWalker(), - } + #lists = initLists(); #activirtManager: ActivityManager; @@ -106,7 +105,7 @@ export default class MessageDistributor extends StatedScreen { /* == REFACTOR_ME == */ const eventList = sess.events.toJSON(); - initLists({ + initListsDepr({ event: eventList, stack: sess.stackEvents.toJSON(), resource: sess.resources.toJSON(), @@ -236,10 +235,16 @@ export default class MessageDistributor extends StatedScreen { const llEvent = this.#locationEventManager.moveToLast(t, index); if (!!llEvent) { if (llEvent.domContentLoadedTime != null) { - stateToUpdate.domContentLoadedTime = llEvent.domContentLoadedTime + this.#navigationStartOffset; + stateToUpdate.domContentLoadedTime = { + time: llEvent.domContentLoadedTime + this.#navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & setLocation: add navigationStart to db) + value: llEvent.domContentLoadedTime, + } } if (llEvent.loadTime != null) { - stateToUpdate.loadTime = llEvent.domContentLoadedTime + this.#navigationStartOffset + stateToUpdate.loadTime = { + time: llEvent.loadTime + this.#navigationStartOffset, + value: llEvent.loadTime, + } } if (llEvent.domBuildingTime != null) { stateToUpdate.domBuildingTime = llEvent.domBuildingTime; diff --git a/frontend/app/svg/icons/funnel/cpu.svg b/frontend/app/svg/icons/funnel/cpu.svg new file mode 100644 index 000000000..f4922a33f --- /dev/null +++ b/frontend/app/svg/icons/funnel/cpu.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/dizzy.svg b/frontend/app/svg/icons/funnel/dizzy.svg new file mode 100644 index 000000000..4f026cd64 --- /dev/null +++ b/frontend/app/svg/icons/funnel/dizzy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/emoji-angry.svg b/frontend/app/svg/icons/funnel/emoji-angry.svg new file mode 100644 index 000000000..e9c147cb9 --- /dev/null +++ b/frontend/app/svg/icons/funnel/emoji-angry.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/file-earmark-break.svg b/frontend/app/svg/icons/funnel/file-earmark-break.svg new file mode 100644 index 000000000..244e6b211 --- /dev/null +++ b/frontend/app/svg/icons/funnel/file-earmark-break.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/image.svg b/frontend/app/svg/icons/funnel/image.svg new file mode 100644 index 000000000..36bd4649f --- /dev/null +++ b/frontend/app/svg/icons/funnel/image.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/sd-card.svg b/frontend/app/svg/icons/funnel/sd-card.svg index 8d4991cb1..4e55e699b 100644 --- a/frontend/app/svg/icons/funnel/sd-card.svg +++ b/frontend/app/svg/icons/funnel/sd-card.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/frontend/app/types/account/account.js b/frontend/app/types/account/account.js index 42b0e9ac3..fa672dfcc 100644 --- a/frontend/app/types/account/account.js +++ b/frontend/app/types/account/account.js @@ -10,6 +10,7 @@ export default Member.extend({ banner: undefined, email: '', verifiedEmail: undefined, + smtp: false, license: '', expirationDate: undefined, }, { diff --git a/frontend/app/types/integrations/issueTracker.js b/frontend/app/types/integrations/issueTracker.js index ef53b01f1..c03ed1005 100644 --- a/frontend/app/types/integrations/issueTracker.js +++ b/frontend/app/types/integrations/issueTracker.js @@ -7,7 +7,6 @@ export const ACCESS_KEY_ID_LENGTH = 20; export default Record({ username: undefined, token: undefined, - provider: undefined, url: undefined, provider: 'jira' }, { diff --git a/frontend/app/types/issue/issuesType.js b/frontend/app/types/issue/issuesType.js index 1f0679d5a..c40864bea 100644 --- a/frontend/app/types/issue/issuesType.js +++ b/frontend/app/types/issue/issuesType.js @@ -2,7 +2,16 @@ import Record from 'Types/Record'; export default Record({ id: undefined, + color: undefined, + description: '', name: undefined, iconUrl: undefined }, { + fromJS: ({ iconUrl, color, ...issueType }) => ({ + ...issueType, + color, + iconUrl: iconUrl ? + : +
, + }), }) diff --git a/frontend/app/types/session/issue.js b/frontend/app/types/session/issue.js new file mode 100644 index 000000000..87878b6bb --- /dev/null +++ b/frontend/app/types/session/issue.js @@ -0,0 +1,43 @@ +import Record from 'Types/Record'; +import { List } from 'immutable'; +import Watchdog from 'Types/watchdog' + +export const issues_types = List([ + { 'type': 'js_exception', 'visible': true, 'order': 0, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' }, + { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, + { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' }, + { 'type': 'click_rage', 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/dizzy' }, + { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/emoji-angry' }, + { 'type': 'memory', 'visible': true, 'order': 5, 'name': 'High Memory', 'icon': 'funnel/sd-card' }, + { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' }, + { 'type': 'crash', 'visible': true, 'order': 7, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' }, + { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' } +]).map(Watchdog) + +export const issues_types_map = {} +issues_types.forEach(i => { + issues_types_map[i.type] = { type: i.type, visible: i.visible, order: i.order, name: i.name, } +}); + +export default Record({ + issueId: undefined, + name: '', + visible: true, + sessionId: undefined, + time: undefined, + seqIndex: undefined, + payload: {}, + projectId: undefined, + type: '', + contextString: '', + context: '', + icon: 'info' +}, { + idKey: 'issueId', + fromJS: ({ type, ...rest }) => ({ + ...rest, + type, + icon: issues_types_map[type].icon, + name: issues_types_map[type].name, + }), +}); diff --git a/frontend/app/types/session/session.js b/frontend/app/types/session/session.js index daa2f4ae0..132afcc7d 100644 --- a/frontend/app/types/session/session.js +++ b/frontend/app/types/session/session.js @@ -7,7 +7,7 @@ import StackEvent from './stackEvent'; import Resource from './resource'; import CustomField from './customField'; import SessionError from './error'; - +import Issue from './issue'; const SOURCE_JS = 'js_exception'; @@ -66,6 +66,7 @@ export default Record({ errorsCount: 0, watchdogs: [], issueTypes: [], + issues: [], userDeviceHeapSize: 0, userDeviceMemorySize: 0, errors: List(), @@ -80,6 +81,7 @@ export default Record({ projectId, errors, stackEvents = [], + issues = [], ...session }) => { const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration); @@ -109,6 +111,10 @@ export default Record({ .map(se => StackEvent({ ...se, time: se.timestamp - startedAt })); const exceptions = List(errors) .map(SessionError) + + const issuesList = List(issues) + .map(e => Issue({ ...e, time: e.timestamp - startedAt })) + return { ...session, isIOS: session.platform === "ios", @@ -128,6 +134,7 @@ export default Record({ userNumericHash: hashString(session.userId || session.userAnonymousId || session.userUuid || ""), userDisplayName: session.userId || session.userAnonymousId || 'Anonymous User', firstResourceTime, + issues: issuesList, }; }, idKey: "sessionId", diff --git a/frontend/app/types/watchdog.js b/frontend/app/types/watchdog.js index 76d874c64..fdc30dfb2 100644 --- a/frontend/app/types/watchdog.js +++ b/frontend/app/types/watchdog.js @@ -22,7 +22,6 @@ const WATCHDOG_TYPES = [ ] export const names = { - // 'all' : { label: 'All', icon: 'all' }, 'js_exception' : { label: 'JS Exceptions', icon: 'funnel/exclamation-circle' }, 'bad_request': { label: 'Bad Request', icon: 'funnel/patch-exclamation-fill' }, 'missing_resource': { label: 'Missing Resources', icon: 'funnel/image-fill' }, @@ -33,13 +32,6 @@ export const names = { 'cpu': { label: 'CPU', icon: 'funnel/hdd-fill' }, 'dead_click': { label: 'Dead Click', icon: 'funnel/emoji-dizzy-fill' }, 'custom': { label: 'Custom', icon: 'funnel/exclamation-circle-fill' }, - - // 'errors' : { label: 'Errors', icon: 'console/error' }, - // 'missing_image': { label: 'Missing Images', icon: 'image' }, - // 'slow_session': { label: 'Slow Sessions', icon: 'turtle' }, - // 'high_engagement': { label: 'High Engagements', icon: 'high-engagement' }, - // 'performance_issues': { label: 'Mem/CPU Issues', icon: 'tachometer-slowest' }, - // 'default': { label: 'Default', icon: 'window-alt' }, } const CONJUGATED_ISSUE_TYPES = { @@ -93,8 +85,6 @@ export default Record({ } }, fromJS: (item) => ({ - ...item, - name: item.name, - icon: names[item.type] ? names[item.type].icon : 'turtle' + ...item }), }); diff --git a/frontend/env.js b/frontend/env.js index fdc173aeb..7c8c52d2b 100644 --- a/frontend/env.js +++ b/frontend/env.js @@ -13,7 +13,7 @@ const oss = { ORIGIN: () => 'window.location.origin', API_EDP: () => 'window.location.origin + "/api"', ASSETS_HOST: () => 'window.location.origin + "/assets"', - VERSION: '1.0.1', + VERSION: '1.0.0', SOURCEMAP: true, MINIO_ENDPOINT: process.env.MINIO_ENDPOINT, MINIO_PORT: process.env.MINIO_PORT, diff --git a/scripts/helm/README.md b/scripts/helm/README.md index 1a79331a0..4d5c7e54e 100644 --- a/scripts/helm/README.md +++ b/scripts/helm/README.md @@ -1,48 +1,36 @@ -## Helm charts for installing openreplay components. +## Helm charts for installing OpenReplay components Installation components are separated by namepaces. **Namespace:** -- **app:** Core openreplay application related components. - - alert - - auth - - cache +- **app:** Core OpenReplay application related components. + - alerts + - assets - chalice - - clickhouse - ender - - events - - failover - - filesink - - filestorage + - sink + - storage - http - integrations - - ios-proxy - - metadata - - negative - - pg-stateless - - pg - - preprocessing - - redis - - ws + - db - **db:** Contains following databases and backend components. - - kafka + - kafka (ee) - redis - postgresql - - clickhouse + - clickhouse (ee) - minio - - sqs - nfs-server -- **longhorn:** On-Prem storage solution for kubernetes PVs. +- **longhorn:** Storage solution for kubernetes PVs. - **nginx-ingress:** Nginx ingress for internet traffic to enter the kubernetes cluster. **Scripts:** - **install.sh** - Installs openreplay in a single node machine, for trial runs / demo. + Installs OpenReplay in a single node machine, for trial runs / demo. This script is a wrapper around the `install.sh` with [k3s](https://k3s.io/) as kubernetes distro. @@ -50,8 +38,8 @@ Installation components are separated by namepaces. - **kube-install.sh:** - Installs openreplay on any given kubernetes cluster. Has 3 configuration types - - small (4cores 8G RAM) + Installs OpenReplay on any given kubernetes cluster. Has 3 configuration types: + - small (2cores 8G RAM) - medium (4cores 16G RAM) - recommened (8cores 32G RAM) diff --git a/scripts/helm/app/README.md b/scripts/helm/app/README.md index e5faed535..a5b73f915 100644 --- a/scripts/helm/app/README.md +++ b/scripts/helm/app/README.md @@ -1,13 +1,14 @@ -## Core Openreplay application configuration folder +## Core OpenReplay application configuration folder - This folder contains configuration for core openreplay apps. All applications share common helm chart named *openreplay* which can be overridden by `.yaml` file. + This folder contains configuration for core OpenReplay apps. All applications share common helm chart named *openreplay* which can be overridden by `.yaml` file. **Below is a sample template.** ```yaml - namespace: app # In which namespace alert runs. + namespace: app # In which namespace alerts runs. image: - repository: 998611063711.dkr.ecr.eu-central-1.amazonaws.com/alert # Which image to use + repository: rg.fr-par.scw.cloud/foss # Which image to use + name: alerts pullPolicy: IfNotPresent tag: "latest" # Overrides the image tag whose default is the chart appVersion. @@ -30,7 +31,7 @@ # env vars for the application env: - ALERT_NOTIFICATION_STRING: https://parrot.openreplay.io/alerts/notifications + ALERT_NOTIFICATION_STRING: http://chalice-openreplay.app.svc.cluster.local:8000/alerts/notifications CLICKHOUSE_STRING: tcp://clickhouse.db.svc.cluster.local:9000/default POSTGRES_STRING: postgres://postgresql.db.svc.cluster.local:5432 ``` diff --git a/scripts/helm/app/alerts.yaml b/scripts/helm/app/alerts.yaml index 4fe30c3cc..4bb397526 100644 --- a/scripts/helm/app/alerts.yaml +++ b/scripts/helm/app/alerts.yaml @@ -1,7 +1,7 @@ namespace: app image: repository: rg.fr-par.scw.cloud/foss - name: alert + name: alerts pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "latest" @@ -22,6 +22,6 @@ resources: memory: 128Mi env: - ALERT_NOTIFICATION_STRING: https://parrot.asayer.io/alerts/notifications + ALERT_NOTIFICATION_STRING: http://chalice-openreplay.app.svc.cluster.local:8000/alerts/notifications CLICKHOUSE_STRING: tcp://clickhouse.db.svc.cluster.local:9000/default POSTGRES_STRING: postgres://postgres:asayerPostgres@postgresql.db.svc.cluster.local:5432 diff --git a/scripts/helm/app/assets.yaml b/scripts/helm/app/assets.yaml index 390fe4e07..c7b740e22 100644 --- a/scripts/helm/app/assets.yaml +++ b/scripts/helm/app/assets.yaml @@ -22,8 +22,8 @@ resources: memory: 128Mi env: - ASSETS_ORIGIN: /asayer-sessions-assets # TODO: full path (with the minio prefix) - S3_BUCKET_ASSETS: asayer-sessions-assets + ASSETS_ORIGIN: /sessions-assets # TODO: full path (with the minio prefix) + S3_BUCKET_ASSETS: sessions-assets AWS_ENDPOINT: http://minio.db.svc.cluster.local:9000 AWS_ACCESS_KEY_ID: "minios3AccessKeyS3cr3t" AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" diff --git a/scripts/helm/app/chalice.yaml b/scripts/helm/app/chalice.yaml index 9a02adfaa..879bb945f 100644 --- a/scripts/helm/app/chalice.yaml +++ b/scripts/helm/app/chalice.yaml @@ -40,9 +40,6 @@ env: sessions_region: us-east-1 put_S3_TTL: '20' sourcemaps_bucket: sourcemaps - sourcemaps_bucket_key: minios3AccessKeyS3cr3t - sourcemaps_bucket_secret: m1n10s3CretK3yPassw0rd - sourcemaps_bucket_region: us-east-1 js_cache_bucket: sessions-assets async_Token: '' EMAIL_HOST: '' @@ -56,7 +53,7 @@ env: EMAIL_FROM: OpenReplay SITE_URL: '' announcement_url: '' - jwt_secret: SET A RANDOM STRING HERE + jwt_secret: "SetARandomStringHere" jwt_algorithm: HS512 jwt_exp_delta_seconds: '2592000' # Override with your https://domain_name diff --git a/scripts/helm/app/http.yaml b/scripts/helm/app/http.yaml index d2788970c..82342c171 100644 --- a/scripts/helm/app/http.yaml +++ b/scripts/helm/app/http.yaml @@ -22,9 +22,9 @@ resources: memory: 128Mi env: - ASSETS_ORIGIN: /asayer-sessions-assets # TODO: full path (with the minio prefix) + ASSETS_ORIGIN: /sessions-assets # TODO: full path (with the minio prefix) TOKEN_SECRET: secret_token_string # TODO: generate on buld - S3_BUCKET_IMAGES_IOS: asayer-sessions-mobile-assets + S3_BUCKET_IMAGES_IOS: sessions-mobile-assets AWS_ACCESS_KEY_ID: "minios3AccessKeyS3cr3t" AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" AWS_REGION: us-east-1 diff --git a/scripts/helm/app/issues.md b/scripts/helm/app/issues.md deleted file mode 100644 index 06a6cb91f..000000000 --- a/scripts/helm/app/issues.md +++ /dev/null @@ -1,76 +0,0 @@ -i [X] alert: - - [X] postgresql app db not found - public.alerts relation doesn't exist -- [X] cache: - - [X] connecting kafka with ssl:// -- [X] events: - - [X] postgresql app db not found - ``` - ERROR: relation "integrations" does not exist (SQLSTATE 42P01) - ``` -- [X] failover: asayer no error logs - - [X] Redis error: NOAUTH Authentication required. - redis cluster should not have password - - [X] Redis has cluster support disabled -- [X] redis-asayer: - - [X] /root/workers/redis/main.go:29: Redis error: no pools available - - [X] /root/workers/pg/main.go:49: Redis error: no cluster slots assigned -- [X] ws-asayer: - - [X] Redis has cluster support disabled -- [X] ender: - - [X] /root/pkg/kafka/consumer.go:95: Consumer error: Subscribed topic not available: ^(raw)$: Broker: Unknown topic or partition - - [X] kafka ssl -- [X] preprocessor: - - [X] kafka ssl -- [X] clickhouse-asayer: - - [X] Table default.sessions doesn't exist. -- [ ] puppeteer: - - [ ] Image not found - ``` - repository 998611063711.dkr.ecr.eu-central-1.amazonaws.com/puppeteer-jasmine not found: name unknown: The repository with name 'puppeteer-jasmine' does not exist in the registry with id '998611063711 - Back-off pulling image "998611063711.dkr.ecr.eu-central-1.amazonaws.com/puppeteer-jasmine:latest" - ``` -- [o] negative: - - [X] Clickhouse prepare error: code: 60, message: Table default.negatives_buffer doesn't exist. - - [ ] kafka ssl issue -- [o] metadata: - - [X] code: 60, message: Table default.sessions_metadata doesn't exist. - - [ ] /root/workers/metadata/main.go:96: Consumer Commit error: Local: No offset stored -- [ ] http: - - [ ] /root/pkg/env/worker_id.go:8: Get : unsupported protocol scheme "" -- [o] chalice: - - [X] No code to start - - [X] first install deps - - [X] then install chalice - - [X] sqs without creds - - [ ] do we need dead-runs as aws put failed in deadruns Q - - [ ] do we have to limit for parallel runs / the retries ? - -## Talk with Mehdi and Sacha -- [X] Do we need new app or old -- [X] in new we don't need redis. so what should we do ? - -# 3 new workers - -This is not in prod -kafka-staging take the new by compare with prod - -1. ender sasha -2. pg_stateless sasha -3. http sasha -4. changed preprocessing: david -5. ios proxy: taha - -Application loadbalancer - -domain: ingest.asayer.io - -ingress with ssl termination. - ios proxy ( in ecs ) - oauth - ws - api - http ( sasha ) - -ws lb with ssl: - ingress diff --git a/scripts/helm/app/openreplay/templates/deployment.yaml b/scripts/helm/app/openreplay/templates/deployment.yaml index 12e90714a..a2259a852 100644 --- a/scripts/helm/app/openreplay/templates/deployment.yaml +++ b/scripts/helm/app/openreplay/templates/deployment.yaml @@ -14,8 +14,9 @@ spec: {{- include "openreplay.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + openreplayRolloutID: {{ randAlphaNum 5 | quote }} # Restart nginx after every deployment + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: diff --git a/scripts/helm/app/openreplay/values.yaml b/scripts/helm/app/openreplay/values.yaml index e93d4d44d..498f88801 100644 --- a/scripts/helm/app/openreplay/values.yaml +++ b/scripts/helm/app/openreplay/values.yaml @@ -1,4 +1,4 @@ -# Default values for openreplay. +# Default values for OpenReplay. # This is a YAML-formatted file. # Declare variables to be passed into your templates. diff --git a/scripts/helm/app/storage.yaml b/scripts/helm/app/storage.yaml index 18890847a..aebc2f3e8 100644 --- a/scripts/helm/app/storage.yaml +++ b/scripts/helm/app/storage.yaml @@ -34,10 +34,10 @@ env: AWS_ENDPOINT: http://minio.db.svc.cluster.local:9000 AWS_ACCESS_KEY_ID: "minios3AccessKeyS3cr3t" AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" - AWS_REGION_WEB: eu-central-1 - AWS_REGION_IOS: eu-central-1 - S3_BUCKET_WEB: asayer-mobs - S3_BUCKET_IOS: asayer-mobs + AWS_REGION_WEB: us-east-1 + AWS_REGION_IOS: us-east-1 + S3_BUCKET_WEB: mobs + S3_BUCKET_IOS: mobs # REDIS_STRING: redis-master.db.svc.cluster.local:6379 KAFKA_SERVERS: kafka.db.svc.cluster.local:9092 diff --git a/scripts/helm/db/init_dbs/postgresql/init_schema.sql b/scripts/helm/db/init_dbs/postgresql/init_schema.sql index ed1449309..83afd3ba0 100644 --- a/scripts/helm/db/init_dbs/postgresql/init_schema.sql +++ b/scripts/helm/db/init_dbs/postgresql/init_schema.sql @@ -1,8 +1,9 @@ BEGIN; +-- --- public.sql --- CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS pgcrypto; - +-- --- accounts.sql --- CREATE OR REPLACE FUNCTION generate_api_key(length integer) RETURNS text AS $$ @@ -33,7 +34,7 @@ CREATE TABLE public.tenants created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), edition varchar(3) NOT NULL, version_number text NOT NULL, - licence text NULL, + license text NULL, opt_out bool NOT NULL DEFAULT FALSE, t_projects integer NOT NULL DEFAULT 1, t_sessions bigint NOT NULL DEFAULT 0, @@ -128,7 +129,6 @@ CREATE TABLE basic_authentication token_requested_at timestamp without time zone NULL DEFAULT NULL, changed_at timestamp, UNIQUE (user_id) - -- CHECK ((token IS NULL and token_requested_at IS NULL) or (token IS NOT NULL and token_requested_at IS NOT NULL)) ); CREATE TYPE oauth_provider AS ENUM ('jira', 'github'); @@ -138,32 +138,32 @@ CREATE TABLE oauth_authentication provider oauth_provider NOT NULL, provider_user_id text NOT NULL, token text NOT NULL, - UNIQUE (provider, provider_user_id) + UNIQUE (user_id, provider) ); - +-- --- projects.sql --- CREATE TABLE projects ( project_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - project_key varchar(20) NOT NULL UNIQUE DEFAULT generate_api_key(20), + project_key varchar(20) NOT NULL UNIQUE DEFAULT generate_api_key(20), name text NOT NULL, active boolean NOT NULL, - sample_rate smallint NOT NULL DEFAULT 100 CHECK (sample_rate >= 0 AND sample_rate <= 100), - created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), - deleted_at timestamp without time zone NULL DEFAULT NULL, - max_session_duration integer NOT NULL DEFAULT 7200000, - metadata_1 text DEFAULT NULL, - metadata_2 text DEFAULT NULL, - metadata_3 text DEFAULT NULL, - metadata_4 text DEFAULT NULL, - metadata_5 text DEFAULT NULL, - metadata_6 text DEFAULT NULL, - metadata_7 text DEFAULT NULL, - metadata_8 text DEFAULT NULL, - metadata_9 text DEFAULT NULL, - metadata_10 text DEFAULT NULL, - gdpr jsonb NOT NULL DEFAULT '{ + sample_rate smallint NOT NULL DEFAULT 100 CHECK (sample_rate >= 0 AND sample_rate <= 100), + created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), + deleted_at timestamp without time zone NULL DEFAULT NULL, + max_session_duration integer NOT NULL DEFAULT 7200000, + metadata_1 text DEFAULT NULL, + metadata_2 text DEFAULT NULL, + metadata_3 text DEFAULT NULL, + metadata_4 text DEFAULT NULL, + metadata_5 text DEFAULT NULL, + metadata_6 text DEFAULT NULL, + metadata_7 text DEFAULT NULL, + metadata_8 text DEFAULT NULL, + metadata_9 text DEFAULT NULL, + metadata_10 text DEFAULT NULL, + gdpr jsonb NOT NULL DEFAULT '{ "maskEmails": true, "sampleRate": 33, "maskNumbers": false, @@ -185,6 +185,70 @@ CREATE TRIGGER on_insert_or_update FOR EACH ROW EXECUTE PROCEDURE notify_project(); +-- --- alerts.sql --- + +CREATE TYPE alert_detection_method AS ENUM ('threshold', 'change'); + +CREATE TABLE alerts +( + alert_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + name text NOT NULL, + description text NULL DEFAULT NULL, + active boolean NOT NULL DEFAULT TRUE, + detection_method alert_detection_method NOT NULL, + query jsonb NOT NULL, + deleted_at timestamp NULL DEFAULT NULL, + created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), + options jsonb NOT NULL DEFAULT '{ + "renotifyInterval": 1440 + }'::jsonb +); + + +CREATE OR REPLACE FUNCTION notify_alert() RETURNS trigger AS +$$ +DECLARE + clone jsonb; +BEGIN + clone = to_jsonb(NEW); + clone = jsonb_set(clone, '{created_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.created_at) * 1000 AS BIGINT))); + IF NEW.deleted_at NOTNULL THEN + clone = jsonb_set(clone, '{deleted_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.deleted_at) * 1000 AS BIGINT))); + END IF; + PERFORM pg_notify('alert', clone::text); + RETURN NEW; +END ; +$$ LANGUAGE plpgsql; + + +CREATE TRIGGER on_insert_or_update_or_delete + AFTER INSERT OR UPDATE OR DELETE + ON alerts + FOR EACH ROW +EXECUTE PROCEDURE notify_alert(); + +-- --- webhooks.sql --- + +create type webhook_type as enum ('webhook', 'slack', 'email'); + +create table webhooks +( + webhook_id integer generated by default as identity + constraint webhooks_pkey + primary key, + endpoint text not null, + created_at timestamp default timezone('utc'::text, now()) not null, + deleted_at timestamp, + auth_header text, + type webhook_type not null, + index integer default 0 not null, + name varchar(100) +); + + +-- --- notifications.sql --- + CREATE TABLE notifications ( notification_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -209,6 +273,23 @@ CREATE TABLE user_viewed_notifications constraint user_viewed_notifications_pkey primary key (user_id, notification_id) ); +-- --- funnels.sql --- + +CREATE TABLE funnels +( + funnel_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + name text not null, + filter jsonb not null, + created_at timestamp default timezone('utc'::text, now()) not null, + deleted_at timestamp, + is_public boolean NOT NULL DEFAULT False +); + +CREATE INDEX ON public.funnels (user_id, is_public); + +-- --- announcements.sql --- create type announcement_type as enum ('notification', 'alert'); @@ -226,92 +307,7 @@ create table announcements type announcement_type default 'notification'::announcement_type not null ); -CREATE TYPE error_source AS ENUM ('js_exception', 'bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic'); -CREATE TYPE error_status AS ENUM ('unresolved', 'resolved', 'ignored'); -CREATE TABLE errors -( - error_id text NOT NULL PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - source error_source NOT NULL, - name text DEFAULT NULL, - message text NOT NULL, - payload jsonb NOT NULL, - status error_status NOT NULL DEFAULT 'unresolved', - parent_error_id text DEFAULT NULL REFERENCES errors (error_id) ON DELETE SET NULL, - stacktrace jsonb, --to save the stacktrace and not query S3 another time - stacktrace_parsed_at timestamp -); -CREATE INDEX ON errors (project_id, source); -CREATE INDEX errors_message_gin_idx ON public.errors USING GIN (message gin_trgm_ops); -CREATE INDEX errors_name_gin_idx ON public.errors USING GIN (name gin_trgm_ops); -CREATE INDEX errors_project_id_idx ON public.errors (project_id); -CREATE INDEX errors_project_id_status_idx ON public.errors (project_id, status); - -CREATE TABLE user_favorite_errors -( - user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, - error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, - PRIMARY KEY (user_id, error_id) -); - -CREATE TABLE user_viewed_errors -( - user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, - error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, - PRIMARY KEY (user_id, error_id) -); -CREATE INDEX user_viewed_errors_user_id_idx ON public.user_viewed_errors (user_id); -CREATE INDEX user_viewed_errors_error_id_idx ON public.user_viewed_errors (error_id); - - -CREATE TYPE issue_type AS ENUM ( - 'click_rage', - 'dead_click', - 'excessive_scrolling', - 'bad_request', - 'missing_resource', - 'memory', - 'cpu', - 'slow_resource', - 'slow_page_load', - 'crash', - 'ml_cpu', - 'ml_memory', - 'ml_dead_click', - 'ml_click_rage', - 'ml_mouse_thrashing', - 'ml_excessive_scrolling', - 'ml_slow_resources', - 'custom', - 'js_exception' - ); - -CREATE TABLE issues -( - issue_id text NOT NULL PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - type issue_type NOT NULL, - context_string text NOT NULL, - context jsonb DEFAULT NULL -); -CREATE INDEX ON issues (issue_id, type); -CREATE INDEX issues_context_string_gin_idx ON public.issues USING GIN (context_string gin_trgm_ops); - -create type webhook_type as enum ('webhook', 'slack', 'email'); - -create table webhooks -( - webhook_id integer generated by default as identity - constraint webhooks_pkey - primary key, - endpoint text not null, - created_at timestamp default timezone('utc'::text, now()) not null, - deleted_at timestamp, - auth_header text, - type webhook_type not null, - index integer default 0 not null, - name varchar(100) -); +-- --- integrations.sql --- CREATE TYPE integration_provider AS ENUM ('bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic', 'elasticsearch'); --, 'jira', 'github'); CREATE TABLE integrations @@ -355,74 +351,82 @@ create table jira_cloud url text ); -CREATE TABLE funnels +-- --- issues.sql --- + +CREATE TYPE issue_type AS ENUM ( + 'click_rage', + 'dead_click', + 'excessive_scrolling', + 'bad_request', + 'missing_resource', + 'memory', + 'cpu', + 'slow_resource', + 'slow_page_load', + 'crash', + 'ml_cpu', + 'ml_memory', + 'ml_dead_click', + 'ml_click_rage', + 'ml_mouse_thrashing', + 'ml_excessive_scrolling', + 'ml_slow_resources', + 'custom', + 'js_exception' + ); + +CREATE TABLE issues ( - funnel_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, - name text not null, - filter jsonb not null, - created_at timestamp default timezone('utc'::text, now()) not null, - deleted_at timestamp, - is_public boolean NOT NULL DEFAULT False + issue_id text NOT NULL PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + type issue_type NOT NULL, + context_string text NOT NULL, + context jsonb DEFAULT NULL +); +CREATE INDEX ON issues (issue_id, type); +CREATE INDEX issues_context_string_gin_idx ON public.issues USING GIN (context_string gin_trgm_ops); + +-- --- errors.sql --- + +CREATE TYPE error_source AS ENUM ('js_exception', 'bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic'); +CREATE TYPE error_status AS ENUM ('unresolved', 'resolved', 'ignored'); +CREATE TABLE errors +( + error_id text NOT NULL PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + source error_source NOT NULL, + name text DEFAULT NULL, + message text NOT NULL, + payload jsonb NOT NULL, + status error_status NOT NULL DEFAULT 'unresolved', + parent_error_id text DEFAULT NULL REFERENCES errors (error_id) ON DELETE SET NULL, + stacktrace jsonb, --to save the stacktrace and not query S3 another time + stacktrace_parsed_at timestamp +); +CREATE INDEX ON errors (project_id, source); +CREATE INDEX errors_message_gin_idx ON public.errors USING GIN (message gin_trgm_ops); +CREATE INDEX errors_name_gin_idx ON public.errors USING GIN (name gin_trgm_ops); +CREATE INDEX errors_project_id_idx ON public.errors (project_id); +CREATE INDEX errors_project_id_status_idx ON public.errors (project_id, status); + +CREATE TABLE user_favorite_errors +( + user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, error_id) ); -CREATE INDEX ON public.funnels (user_id, is_public); - -CREATE TYPE alert_detection_method AS ENUM ('threshold', 'change'); - -CREATE TABLE alerts +CREATE TABLE user_viewed_errors ( - alert_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - name text NOT NULL, - description text NULL DEFAULT NULL, - active boolean NOT NULL DEFAULT TRUE, - detection_method alert_detection_method NOT NULL, - query jsonb NOT NULL, - deleted_at timestamp NULL DEFAULT NULL, - created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), - options jsonb NOT NULL DEFAULT '{ - "renotifyInterval": 1440 - }'::jsonb + user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, error_id) ); +CREATE INDEX user_viewed_errors_user_id_idx ON public.user_viewed_errors (user_id); +CREATE INDEX user_viewed_errors_error_id_idx ON public.user_viewed_errors (error_id); -CREATE OR REPLACE FUNCTION notify_alert() RETURNS trigger AS -$$ -DECLARE - clone jsonb; -BEGIN - clone = to_jsonb(NEW); - clone = jsonb_set(clone, '{created_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.created_at) * 1000 AS BIGINT))); - IF NEW.deleted_at NOTNULL THEN - clone = jsonb_set(clone, '{deleted_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.deleted_at) * 1000 AS BIGINT))); - END IF; - PERFORM pg_notify('alert', clone::text); - RETURN NEW; -END ; -$$ LANGUAGE plpgsql; - - -CREATE TRIGGER on_insert_or_update_or_delete - AFTER INSERT OR UPDATE OR DELETE - ON alerts - FOR EACH ROW -EXECUTE PROCEDURE notify_alert(); - -CREATE TABLE autocomplete -( - value text NOT NULL, - type text NOT NULL, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE -); - -CREATE unique index autocomplete_unique ON autocomplete (project_id, value, type); -CREATE index autocomplete_project_id_idx ON autocomplete (project_id); -CREATE INDEX autocomplete_type_idx ON public.autocomplete (type); -CREATE INDEX autocomplete_value_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops); - - +-- --- sessions.sql --- CREATE TYPE device_type AS ENUM ('desktop', 'tablet', 'mobile', 'other'); CREATE TYPE country AS ENUM ('UN', 'RW', 'SO', 'YE', 'IQ', 'SA', 'IR', 'CY', 'TZ', 'SY', 'AM', 'KE', 'CD', 'DJ', 'UG', 'CF', 'SC', 'JO', 'LB', 'KW', 'OM', 'QA', 'BH', 'AE', 'IL', 'TR', 'ET', 'ER', 'EG', 'SD', 'GR', 'BI', 'EE', 'LV', 'AZ', 'LT', 'SJ', 'GE', 'MD', 'BY', 'FI', 'AX', 'UA', 'MK', 'HU', 'BG', 'AL', 'PL', 'RO', 'XK', 'ZW', 'ZM', 'KM', 'MW', 'LS', 'BW', 'MU', 'SZ', 'RE', 'ZA', 'YT', 'MZ', 'MG', 'AF', 'PK', 'BD', 'TM', 'TJ', 'LK', 'BT', 'IN', 'MV', 'IO', 'NP', 'MM', 'UZ', 'KZ', 'KG', 'TF', 'HM', 'CC', 'PW', 'VN', 'TH', 'ID', 'LA', 'TW', 'PH', 'MY', 'CN', 'HK', 'BN', 'MO', 'KH', 'KR', 'JP', 'KP', 'SG', 'CK', 'TL', 'RU', 'MN', 'AU', 'CX', 'MH', 'FM', 'PG', 'SB', 'TV', 'NR', 'VU', 'NC', 'NF', 'NZ', 'FJ', 'LY', 'CM', 'SN', 'CG', 'PT', 'LR', 'CI', 'GH', 'GQ', 'NG', 'BF', 'TG', 'GW', 'MR', 'BJ', 'GA', 'SL', 'ST', 'GI', 'GM', 'GN', 'TD', 'NE', 'ML', 'EH', 'TN', 'ES', 'MA', 'MT', 'DZ', 'FO', 'DK', 'IS', 'GB', 'CH', 'SE', 'NL', 'AT', 'BE', 'DE', 'LU', 'IE', 'MC', 'FR', 'AD', 'LI', 'JE', 'IM', 'GG', 'SK', 'CZ', 'NO', 'VA', 'SM', 'IT', 'SI', 'ME', 'HR', 'BA', 'AO', 'NA', 'SH', 'BV', 'BB', 'CV', 'GY', 'GF', 'SR', 'PM', 'GL', 'PY', 'UY', 'BR', 'FK', 'GS', 'JM', 'DO', 'CU', 'MQ', 'BS', 'BM', 'AI', 'TT', 'KN', 'DM', 'AG', 'LC', 'TC', 'AW', 'VG', 'VC', 'MS', 'MF', 'BL', 'GP', 'GD', 'KY', 'BZ', 'SV', 'GT', 'HN', 'NI', 'CR', 'VE', 'EC', 'CO', 'PA', 'HT', 'AR', 'CL', 'BO', 'PE', 'MX', 'PF', 'PN', 'KI', 'TK', 'TO', 'WF', 'WS', 'NU', 'MP', 'GU', 'PR', 'VI', 'UM', 'AS', 'CA', 'US', 'PS', 'RS', 'AQ', 'SX', 'CW', 'BQ', 'SS'); CREATE TYPE platform AS ENUM ('web','ios','android'); @@ -433,7 +437,7 @@ CREATE TABLE sessions project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, tracker_version text NOT NULL, start_ts bigint NOT NULL, - duration integer DEFAULT NULL, + duration integer NULL, rev_id text DEFAULT NULL, platform platform NOT NULL DEFAULT 'web', is_snippet boolean NOT NULL DEFAULT FALSE, @@ -507,6 +511,8 @@ CREATE INDEX sessions_user_anonymous_id_gin_idx ON public.sessions USING GIN (us CREATE INDEX sessions_user_country_gin_idx ON public.sessions (project_id, user_country); CREATE INDEX ON sessions (project_id, user_country); CREATE INDEX ON sessions (project_id, user_browser); +CREATE INDEX sessions_start_ts_idx ON public.sessions (start_ts) WHERE duration > 0; +CREATE INDEX sessions_project_id_idx ON public.sessions (project_id) WHERE duration > 0; ALTER TABLE public.sessions ADD CONSTRAINT web_browser_constraint CHECK ( (sessions.platform = 'web' AND sessions.user_browser NOTNULL) OR @@ -536,6 +542,73 @@ CREATE TABLE user_favorite_sessions ); +-- --- assignments.sql --- + +create table assigned_sessions +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + issue_id text NOT NULL, + provider oauth_provider NOT NULL, + created_by integer NOT NULL, + created_at timestamp default timezone('utc'::text, now()) NOT NULL, + provider_data jsonb default '{}'::jsonb NOT NULL +); + +-- --- events_common.sql --- + +CREATE SCHEMA events_common; + +CREATE TYPE events_common.custom_level AS ENUM ('info','error'); + +CREATE TABLE events_common.customs +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + seq_index integer NOT NULL, + name text NOT NULL, + payload jsonb NOT NULL, + level events_common.custom_level NOT NULL DEFAULT 'info', + PRIMARY KEY (session_id, timestamp, seq_index) +); +CREATE INDEX ON events_common.customs (name); +CREATE INDEX customs_name_gin_idx ON events_common.customs USING GIN (name gin_trgm_ops); +CREATE INDEX ON events_common.customs (timestamp); + + +CREATE TABLE events_common.issues +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + seq_index integer NOT NULL, + issue_id text NOT NULL REFERENCES issues (issue_id) ON DELETE CASCADE, + payload jsonb DEFAULT NULL, + PRIMARY KEY (session_id, timestamp, seq_index) +); + + +CREATE TABLE events_common.requests +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + seq_index integer NOT NULL, + url text NOT NULL, + duration integer NOT NULL, + success boolean NOT NULL, + PRIMARY KEY (session_id, timestamp, seq_index) +); +CREATE INDEX ON events_common.requests (url); +CREATE INDEX ON events_common.requests (duration); +CREATE INDEX requests_url_gin_idx ON events_common.requests USING GIN (url gin_trgm_ops); +CREATE INDEX ON events_common.requests (timestamp); +CREATE INDEX requests_url_gin_idx2 ON events_common.requests USING GIN (RIGHT(url, length(url) - (CASE + WHEN url LIKE 'http://%' + THEN 7 + WHEN url LIKE 'https://%' + THEN 8 + ELSE 0 END)) + gin_trgm_ops); + +-- --- events.sql --- CREATE SCHEMA events; CREATE TABLE events.pages @@ -558,6 +631,7 @@ CREATE TABLE events.pages time_to_interactive integer DEFAULT NULL, response_time bigint DEFAULT NULL, response_end bigint DEFAULT NULL, + ttfb integer DEFAULT NULL, PRIMARY KEY (session_id, message_id) ); CREATE INDEX ON events.pages (session_id); @@ -577,6 +651,11 @@ CREATE INDEX pages_base_referrer_gin_idx2 ON events.pages USING GIN (RIGHT(base_ gin_trgm_ops); CREATE INDEX ON events.pages (response_time); CREATE INDEX ON events.pages (response_end); +CREATE INDEX pages_path_gin_idx ON events.pages USING GIN (path gin_trgm_ops); +CREATE INDEX pages_path_idx ON events.pages (path); +CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0; +CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0; +CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0; CREATE TABLE events.clicks @@ -643,85 +722,6 @@ CREATE INDEX ON events.state_actions (name); CREATE INDEX state_actions_name_gin_idx ON events.state_actions USING GIN (name gin_trgm_ops); CREATE INDEX ON events.state_actions (timestamp); - - -CREATE OR REPLACE FUNCTION events.funnel(steps integer[], m integer) RETURNS boolean AS -$$ -DECLARE - step integer; - c integer := 0; -BEGIN - FOREACH step IN ARRAY steps - LOOP - IF step + c = 0 THEN - IF c = 0 THEN - RETURN false; - END IF; - c := 0; - CONTINUE; - END IF; - IF c + 1 = step THEN - c := step; - END IF; - END LOOP; - RETURN c = m; -END; -$$ LANGUAGE plpgsql IMMUTABLE; - - - -CREATE SCHEMA events_common; - -CREATE TYPE events_common.custom_level AS ENUM ('info','error'); - -CREATE TABLE events_common.customs -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - seq_index integer NOT NULL, - name text NOT NULL, - payload jsonb NOT NULL, - level events_common.custom_level NOT NULL DEFAULT 'info', - PRIMARY KEY (session_id, timestamp, seq_index) -); -CREATE INDEX ON events_common.customs (name); -CREATE INDEX customs_name_gin_idx ON events_common.customs USING GIN (name gin_trgm_ops); -CREATE INDEX ON events_common.customs (timestamp); - - -CREATE TABLE events_common.issues -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - seq_index integer NOT NULL, - issue_id text NOT NULL REFERENCES issues (issue_id) ON DELETE CASCADE, - payload jsonb DEFAULT NULL, - PRIMARY KEY (session_id, timestamp, seq_index) -); - - -CREATE TABLE events_common.requests -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - seq_index integer NOT NULL, - url text NOT NULL, - duration integer NOT NULL, - success boolean NOT NULL, - PRIMARY KEY (session_id, timestamp, seq_index) -); -CREATE INDEX ON events_common.requests (url); -CREATE INDEX ON events_common.requests (duration); -CREATE INDEX requests_url_gin_idx ON events_common.requests USING GIN (url gin_trgm_ops); -CREATE INDEX ON events_common.requests (timestamp); -CREATE INDEX requests_url_gin_idx2 ON events_common.requests USING GIN (RIGHT(url, length(url) - (CASE - WHEN url LIKE 'http://%' - THEN 7 - WHEN url LIKE 'https://%' - THEN 8 - ELSE 0 END)) - gin_trgm_ops); - CREATE TYPE events.resource_type AS ENUM ('other', 'script', 'stylesheet', 'fetch', 'img', 'media'); CREATE TYPE events.resource_method AS ENUM ('GET' , 'HEAD' , 'POST' , 'PUT' , 'DELETE' , 'CONNECT' , 'OPTIONS' , 'TRACE' , 'PATCH' ); CREATE TABLE events.resources @@ -779,16 +779,42 @@ CREATE TABLE events.performance ); -ALTER TABLE events.pages - ADD COLUMN ttfb integer DEFAULT NULL; -CREATE INDEX pages_path_gin_idx ON events.pages USING GIN (path gin_trgm_ops); -CREATE INDEX pages_path_idx ON events.pages (path); -CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0; -CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0; -CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0; - -CREATE INDEX sessions_start_ts_idx ON public.sessions (start_ts) WHERE duration > 0; -CREATE INDEX sessions_project_id_idx ON public.sessions (project_id) WHERE duration > 0; +CREATE OR REPLACE FUNCTION events.funnel(steps integer[], m integer) RETURNS boolean AS +$$ +DECLARE + step integer; + c integer := 0; +BEGIN + FOREACH step IN ARRAY steps + LOOP + IF step + c = 0 THEN + IF c = 0 THEN + RETURN false; + END IF; + c := 0; + CONTINUE; + END IF; + IF c + 1 = step THEN + c := step; + END IF; + END LOOP; + RETURN c = m; +END; +$$ LANGUAGE plpgsql IMMUTABLE; -COMMIT; \ No newline at end of file +-- --- autocomplete.sql --- + +CREATE TABLE autocomplete +( + value text NOT NULL, + type text NOT NULL, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE +); + +CREATE unique index autocomplete_unique ON autocomplete (project_id, value, type); +CREATE index autocomplete_project_id_idx ON autocomplete (project_id); +CREATE INDEX autocomplete_type_idx ON public.autocomplete (type); +CREATE INDEX autocomplete_value_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops); + +COMMIT; diff --git a/scripts/helm/db/sqs/.helmignore b/scripts/helm/db/sqs/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/scripts/helm/db/sqs/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/scripts/helm/db/sqs/Chart.yaml b/scripts/helm/db/sqs/Chart.yaml deleted file mode 100644 index df40d044a..000000000 --- a/scripts/helm/db/sqs/Chart.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v2 -name: sqs -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 1.16.0 diff --git a/scripts/helm/db/sqs/templates/NOTES.txt b/scripts/helm/db/sqs/templates/NOTES.txt deleted file mode 100644 index 1933314a0..000000000 --- a/scripts/helm/db/sqs/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sqs.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sqs.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sqs.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sqs.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:9325 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 9325:$CONTAINER_PORT -{{- end }} diff --git a/scripts/helm/db/sqs/templates/_helpers.tpl b/scripts/helm/db/sqs/templates/_helpers.tpl deleted file mode 100644 index 518fd7cc2..000000000 --- a/scripts/helm/db/sqs/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "sqs.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "sqs.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "sqs.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "sqs.labels" -}} -helm.sh/chart: {{ include "sqs.chart" . }} -{{ include "sqs.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "sqs.selectorLabels" -}} -app.kubernetes.io/name: {{ include "sqs.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "sqs.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "sqs.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/scripts/helm/db/sqs/templates/configmap.yaml b/scripts/helm/db/sqs/templates/configmap.yaml deleted file mode 100644 index aa6c9f956..000000000 --- a/scripts/helm/db/sqs/templates/configmap.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "sqs.fullname" . }} - labels: - {{- include "sqs.labels" . | nindent 4 }} -data: - elasticmq.conf: |- - include classpath("application.conf") - akka.http.server.request-timeout = 40 s - - node-address { - protocol = http - host = "*" - port = 9324 - context-path = "" - } - - rest-sqs { - enabled = true - bind-port = 9324 - bind-hostname = "0.0.0.0" - // Possible values: relaxed, strict - sqs-limits = strict - } -{{if .Values.queueConfig }} -{{ .Values.queueConfig | trim | nindent 4 }} -{{ end }} diff --git a/scripts/helm/db/sqs/templates/deployment.yaml b/scripts/helm/db/sqs/templates/deployment.yaml deleted file mode 100644 index 62712031f..000000000 --- a/scripts/helm/db/sqs/templates/deployment.yaml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "sqs.fullname" . }} - labels: - {{- include "sqs.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "sqs.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "sqs.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "sqs.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: 9325 - protocol: TCP - - name: sqs - containerPort: 9324 - protocol: TCP - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumeMounts: - - name: elasticmq - mountPath: /opt/elasticmq.conf - subPath: elasticmq.conf - volumes: - - name: elasticmq - configMap: - name: {{ include "sqs.fullname" . }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/scripts/helm/db/sqs/templates/hpa.yaml b/scripts/helm/db/sqs/templates/hpa.yaml deleted file mode 100644 index db0747bcf..000000000 --- a/scripts/helm/db/sqs/templates/hpa.yaml +++ /dev/null @@ -1,28 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2beta1 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "sqs.fullname" . }} - labels: - {{- include "sqs.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "sqs.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/scripts/helm/db/sqs/templates/ingress.yaml b/scripts/helm/db/sqs/templates/ingress.yaml deleted file mode 100644 index b2dc375fb..000000000 --- a/scripts/helm/db/sqs/templates/ingress.yaml +++ /dev/null @@ -1,41 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "sqs.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "sqs.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ . }} - backend: - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} diff --git a/scripts/helm/db/sqs/templates/service.yaml b/scripts/helm/db/sqs/templates/service.yaml deleted file mode 100644 index fa6b14238..000000000 --- a/scripts/helm/db/sqs/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "sqs.fullname" . }} - labels: - {{- include "sqs.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.http.port }} - targetPort: http - protocol: TCP - name: http - - port: {{ .Values.service.sqs.port }} - targetPort: sqs - protocol: TCP - name: sqs - selector: - {{- include "sqs.selectorLabels" . | nindent 4 }} diff --git a/scripts/helm/db/sqs/templates/serviceaccount.yaml b/scripts/helm/db/sqs/templates/serviceaccount.yaml deleted file mode 100644 index a2989f188..000000000 --- a/scripts/helm/db/sqs/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "sqs.serviceAccountName" . }} - labels: - {{- include "sqs.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/scripts/helm/db/sqs/values.yaml b/scripts/helm/db/sqs/values.yaml deleted file mode 100644 index 5634a5494..000000000 --- a/scripts/helm/db/sqs/values.yaml +++ /dev/null @@ -1,111 +0,0 @@ -# Default values for sqs. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: roribio16/alpine-sqs - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "latest" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - sqs: - port: 9324 - http: - port: 9325 - -ingress: - enabled: false - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: [] - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - limits: - cpu: 1 - memory: 1Gi - requests: - cpu: 100m - memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -# Creating the initial queue -# Ref: https://github.com/softwaremill/elasticmq#automatically-creating-queues-on-startup -queueConfig: |- - queues { - scheduled-runs { - defaultVisibilityTimeout = 10 seconds - delay = 5 seconds - receiveMessageWait = 0 seconds - deadLettersQueue { - name = "dead-runs" - maxReceiveCount = 1000 - } - } - ondemand-runs { - defaultVisibilityTimeout = 10 seconds - delay = 5 seconds - receiveMessageWait = 0 seconds - deadLettersQueue { - name = "dead-runs" - maxReceiveCount = 1000 - } - } - dead-runs { - defaultVisibilityTimeout = 10 seconds - delay = 5 seconds - receiveMessageWait = 0 seconds - } - } diff --git a/scripts/helm/install.sh b/scripts/helm/install.sh index fc50c519b..a3dfcc4c2 100755 --- a/scripts/helm/install.sh +++ b/scripts/helm/install.sh @@ -2,6 +2,22 @@ set -o errtrace +# Check for a valid domain_name +domain_name=`grep domain_name vars.yaml | grep -v "example" | cut -d " " -f2 | cut -d '"' -f2` +# Ref: https://stackoverflow.com/questions/15268987/bash-based-regex-domain-name-validation +[[ $(echo $domain_name | grep -P '(?=^.{5,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)') ]] || { + echo "OpenReplay Needs a valid domain name for captured sessions to replay. For example, openreplay.mycompany.com" + echo "Please enter your domain name" + read domain_name + [[ -z domain_name ]] && { + echo "OpenReplay won't work without domain name. Exiting..." + exit 1 + } || { + sed -i "s#domain_name.*#domain_name: \"${domain_name}\" #g" vars.yaml + } +} + + # Installing k3s curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik" sh - mkdir ~/.kube diff --git a/scripts/helm/kube-install.sh b/scripts/helm/kube-install.sh index e3905c2d4..5483c4420 100755 --- a/scripts/helm/kube-install.sh +++ b/scripts/helm/kube-install.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -xo errtrace +set -o errtrace # color schemes # Ansi color code variables @@ -22,7 +22,7 @@ echo -e ${reset} ## installing kubectl which kubectl &> /dev/null || { - echo "kubectl not installed. installing..." + echo "kubectl not installed. Installing it..." sudo curl -SsL https://dl.k8s.io/release/v1.20.0/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl ; sudo chmod +x /usr/local/bin/kubectl } @@ -34,7 +34,7 @@ which stern &> /dev/null || { ## installing k9s which k9s &> /dev/null || { - echo "k9s not installed. installing..." + echo "k9s not installed. Installing it..." sudo curl -SsL https://github.com/derailed/k9s/releases/download/v0.24.2/k9s_Linux_x86_64.tar.gz -o /tmp/k9s.tar.gz cd /tmp tar -xf k9s.tar.gz @@ -44,13 +44,13 @@ which k9s &> /dev/null || { } which ansible &> /dev/null || { - echo "ansible not installed. Installing..." + echo "ansible not installed. Installing it..." which pip || (sudo apt update && sudo apt install python3-pip -y) sudo pip3 install ansible==2.10.0 } which docker &> /dev/null || { - echo "docker is not installed. Installing..." + echo "docker is not installed. Installing it..." user=`whoami` sudo apt install docker.io -y sudo usermod -aG docker $user @@ -59,7 +59,7 @@ which docker &> /dev/null || { ## installing helm which helm &> /dev/null if [[ $? -ne 0 ]]; then - echo "helm not installed. installing..." + echo "helm not installed. Installing it..." curl -ssl https://get.helm.sh/helm-v3.4.2-linux-amd64.tar.gz -o /tmp/helm.tar.gz tar -xf /tmp/helm.tar.gz chmod +x linux-amd64/helm @@ -77,30 +77,31 @@ fi # make all stderr red color()(set -o pipefail;"$@" 2>&1>&3|sed $'s,.*,\e[31m&\e[m,'>&2)3>&1 -usage() -{ +usage() { echo -e ${bold}${yellow} ''' -This script will install and configure openreplay apps and databases on the kubernetes cluster, +This script will install and configure OpenReplay apps and databases on the kubernetes cluster, which is accesd with the ${HOME}/.kube/config or $KUBECONFIG env variable. ''' -cat << EOF -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -█░▄▄▀█░▄▄█░▄▄▀█░██░█░▄▄█░▄▄▀██ -█░▀▀░█▄▄▀█░▀▀░█░▀▀░█░▄▄█░▀▀▄██ -█▄██▄█▄▄▄█▄██▄█▀▀▀▄█▄▄▄█▄█▄▄██ -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +cat <<"EOF" + ___ ____ _ + / _ \ _ __ ___ _ __ | _ \ ___ _ __ | | __ _ _ _ +| | | | '_ \ / _ \ '_ \| |_) / _ \ '_ \| |/ _` | | | | +| |_| | |_) | __/ | | | _ < __/ |_) | | (_| | |_| | + \___/| .__/ \___|_| |_|_| \_\___| .__/|_|\__,_|\__, | + |_| |_| |___/ + EOF echo -e "${green}Usage: openreplay-cli [ -h | --help ] [ -v | --verbose ] [ -a | --app APP_NAME ] to install/reinstall specific application [ -t | --type small|medium|ideal ]" echo -e "${reset}${blue}type defines the resource limits applied for the installation: - small: 4core 8G machine + small: 2core 8G machine medium: 4core 16G machine ideal: 8core 32G machine apps can specifically be installed/reinstalled: - alerts assets auth chalice ender http integrations ios-proxy metadata negative pg-stateless pg preprocessing redis sink storage frontend + alerts assets chalice ender http integrations ios-proxy pg redis sink storage frontend ${reset}" echo type value: $installation_type exit 0 @@ -122,8 +123,10 @@ type() { function app(){ case $1 in nginx) - [[ NGINX_REDIRECT_HTTPS -eq 0 ]] && { - sed -i "/return 301/d" nginx-ingress/nginx-ingress/templates/configmap.yaml + # Resetting the redirection rule + sed -i 's/.* return 301 .*/ # return 301 https:\/\/$host$request_uri;/g' nginx-ingress/nginx-ingress/templates/configmap.yaml + [[ NGINX_REDIRECT_HTTPS -eq 1 ]] && { + sed -i "s/# return 301/return 301/g" nginx-ingress/nginx-ingress/templates/configmap.yaml } ansible-playbook -c local setup.yaml -e @vars.yaml -e scale=$installation_type --tags nginx -v exit 0 @@ -170,3 +173,7 @@ done { ansible-playbook -c local setup.yaml -e @vars.yaml -e scale=$installation_type --skip-tags pre-check -v } || exit $? + + + + diff --git a/scripts/helm/nginx-ingress/nginx-ingress/README.md b/scripts/helm/nginx-ingress/nginx-ingress/README.md index a61fe2bc6..76c878b5a 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/README.md +++ b/scripts/helm/nginx-ingress/nginx-ingress/README.md @@ -1,13 +1,13 @@ ## Description -This is the frontend of the openreplay web app to internet. +This is the frontend of the OpenReplay web app (internet). -## Path information +## Endpoints -/ws -> websocket -/streaming -> ios-proxy -/api -> chalice -/http -> http -/ -> frontend (in minio) -/assets -> asayer-sessions-assets bucket in minio -/s3 -> minio api endpoint +- /streaming -> ios-proxy +- /api -> chalice +- /http -> http +- / -> frontend (in minio) +- /assets -> sessions-assets bucket in minio +- /minio -> minio api endpoint +- /ingest -> events ingestor diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml index d47e47255..d02cc26b1 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml @@ -20,6 +20,8 @@ data: proxy_set_header Connection ""; chunked_transfer_encoding off; + client_max_body_size 50M; + proxy_pass http://minio.db.svc.cluster.local:9000; } @@ -35,6 +37,9 @@ data: proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; + proxy_set_header X-Forwarded-For $real_ip; + proxy_set_header X-Forwarded-Host $real_ip; + proxy_set_header X-Real-IP $real_ip; proxy_set_header Host $host; proxy_pass http://http-openreplay.app.svc.cluster.local; } @@ -102,6 +107,13 @@ data: ; sites.conf: |- + # Need real ip address for flags in replay. + # Some LBs will forward real ips as x-forwarded-for + # So making that as priority + map $http_x_forwarded_for $real_ip { + ~^(\d+\.\d+\.\d+\.\d+) $1; + default $remote_addr; + } map $http_upgrade $connection_upgrade { default upgrade; '' close; @@ -110,8 +122,8 @@ data: listen 80 default_server; listen [::]:80 default_server; # server_name _; - return 301 https://$host$request_uri; - # include /etc/nginx/conf.d/location.list; + # return 301 https://$host$request_uri; + include /etc/nginx/conf.d/location.list; } server { listen 443 ssl; diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml index c9d9d78e5..9cc018dc1 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml @@ -13,8 +13,9 @@ spec: {{- include "nginx.selectorLabels" . | nindent 6 }} template: metadata: - {{- with .Values.podAnnotations }} annotations: + nginxRolloutID: {{ randAlphaNum 5 | quote }} # Restart nginx after every deployment + {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml index 38dc08846..38912bf78 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml @@ -6,6 +6,8 @@ metadata: {{- include "nginx.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} + # Make sure to get client ip + externalTrafficPolicy: Local ports: {{- range .Values.service.ports }} - port: {{ .port }} diff --git a/scripts/helm/openreplay-cli b/scripts/helm/openreplay-cli index 05a0b55ed..19392c1ce 100755 --- a/scripts/helm/openreplay-cli +++ b/scripts/helm/openreplay-cli @@ -1,6 +1,6 @@ #!/bin/bash -## This script is a helper for openreplay management +## This script is a helper for managing your OpenReplay instance set -eE -o pipefail # same as: `set -o errexit -o errtrace` # Trapping the error @@ -37,6 +37,16 @@ CWD=$pwd usage() { clear +cat <<"EOF" + ___ ____ _ + / _ \ _ __ ___ _ __ | _ \ ___ _ __ | | __ _ _ _ +| | | | '_ \ / _ \ '_ \| |_) / _ \ '_ \| |/ _` | | | | +| |_| | |_) | __/ | | | _ < __/ |_) | | (_| | |_| | + \___/| .__/ \___|_| |_|_| \_\___| .__/|_|\__,_|\__, | + |_| |_| |___/ + +EOF + echo -e "${green}Usage: openreplay-cli [ -h | --help ] [ -d | --status ] [ -v | --verbose ] @@ -48,7 +58,7 @@ clear echo -e "${reset}${blue}services: ${services[*]}${reset}" exit 0 } -services=( alert auth cache chalice clickhouse ender events failover filesink filestorage http integrations ios-proxy metadata negative pg-stateless pg preprocessing redis ws ) +services=( alerts assets chalice clickhouse ender sink storage http integrations ios-proxy db pg redis ) check() { if ! command -v kubectl &> /dev/null @@ -72,14 +82,14 @@ stop() { start() { if [[ $1 == "all" ]]; then - cd ./helm/app + cd ./app for apps in $(ls *.yaml);do app=$(echo $apps | cut -d '.' -f1) helm upgrade --install -n app $app openreplay -f $app.yaml done cd $CWD fi - helm upgrade --install -n app $1 ./helm/app/openreplay -f ./helm/app/openreplay/$1.yaml + helm upgrade --install -n app $1 ./app/openreplay -f ./app/$1.yaml } @@ -95,7 +105,7 @@ install() { } upgrade() { - sed -i "s/tag:.*/ tag: 'latest'/g" helm/app/$1.yaml + sed -i "s/tag:.*/ tag: 'latest'/g" ./app/$1.yaml } logs() { @@ -109,7 +119,7 @@ status() { [[ $# -eq 0 ]] && usage && exit 1 -PARSED_ARGUMENTS=$(color getopt -a -n openreplay-cli -o vhds:S:l:r:i: --long verbose,help,status,start:,stop:,restart:,install: -- "$@") +PARSED_ARGUMENTS=$(color getopt -a -n openreplay-cli -o vhds:S:l:r:i: --long verbose,help,status,start:,stop:,logs:,restart:,install: -- "$@") VALID_ARGUMENTS=$? if [[ "$VALID_ARGUMENTS" != "0" ]]; then usage diff --git a/scripts/helm/roles/openreplay/defaults/main.yml b/scripts/helm/roles/openreplay/defaults/main.yml index 4927d5350..f7948e53c 100644 --- a/scripts/helm/roles/openreplay/defaults/main.yml +++ b/scripts/helm/roles/openreplay/defaults/main.yml @@ -6,5 +6,4 @@ db_list: - "nfs-server-provisioner" - "postgresql" - "redis" - - "sqs" enterprise_edition: false diff --git a/scripts/helm/roles/openreplay/tasks/install-apps.yaml b/scripts/helm/roles/openreplay/tasks/install-apps.yaml index f442065ac..3e511ac19 100644 --- a/scripts/helm/roles/openreplay/tasks/install-apps.yaml +++ b/scripts/helm/roles/openreplay/tasks/install-apps.yaml @@ -9,7 +9,7 @@ executable: /bin/bash when: app_name|length > 0 tags: app -- name: Installing openreplay core applications +- name: Installing OpenReplay core applications shell: | override='' [[ -f /tmp/'{{ item|basename }}' ]] && override='-f /tmp/{{ item|basename }}' || true diff --git a/scripts/helm/roles/openreplay/tasks/main.yml b/scripts/helm/roles/openreplay/tasks/main.yml index ab4a7cd60..66d31cf4a 100644 --- a/scripts/helm/roles/openreplay/tasks/main.yml +++ b/scripts/helm/roles/openreplay/tasks/main.yml @@ -14,11 +14,11 @@ shell: | kubectl delete -n app secret aws-registry || true kubectl create secret -n app docker-registry aws-registry \ - --docker-server="{{ ecr_docker_registry_server }}" \ - --docker-username="{{ ecr_docker_username }}" \ - --docker-password="{{ ecr_docker_password }}" \ + --docker-server="{{ docker_registry_url }}" \ + --docker-username="{{ docker_registry_username }}" \ + --docker-password="{{ docker_registry_password }}" \ --docker-email=no@email.local - when: ecr_docker_username|length != 0 and ecr_docker_password|length != 0 + when: docker_registry_username|length != 0 and docker_registry_password|length != 0 # Creating helm override files. - name: Creating override files diff --git a/scripts/helm/roles/openreplay/tasks/pre-check.yaml b/scripts/helm/roles/openreplay/tasks/pre-check.yaml index 4363b9c34..60801192f 100644 --- a/scripts/helm/roles/openreplay/tasks/pre-check.yaml +++ b/scripts/helm/roles/openreplay/tasks/pre-check.yaml @@ -4,11 +4,11 @@ block: - name: Checking mandatory variables fail: - msg: "Didn't find openreplay docker credentials." - when: kubeconfig_path|length == 0 or ecr_docker_registry_server|length == 0 + msg: "Didn't find OpenReplay docker credentials." + when: kubeconfig_path|length == 0 or docker_registry_url|length == 0 - name: Generaing minio access key block: - - name: Generaing minio access key + - name: Generating minio access key set_fact: minio_access_key_generated: "{{ lookup('password', '/dev/null length=30 chars=ascii_letters') }}" - name: Updating vars.yaml @@ -16,13 +16,13 @@ regexp: '^minio_access_key' line: 'minio_access_key: "{{ minio_access_key_generated }}"' path: vars.yaml - - name: Generaing minio access key + - name: Generating minio access key set_fact: minio_access_key: "{{ minio_access_key_generated }}" when: minio_access_key|length == 0 - - name: Generaing minio secret key + - name: Generating minio secret key block: - - name: Generaing minio access key + - name: Generating minio access key set_fact: minio_secret_key_generated: "{{ lookup('password', '/dev/null length=30 chars=ascii_letters') }}" - name: Updating vars.yaml @@ -30,19 +30,33 @@ regexp: '^minio_secret_key' line: 'minio_secret_key: "{{minio_secret_key_generated}}"' path: vars.yaml - - name: Generaing minio secret key + - name: Generating minio secret key set_fact: minio_access_key: "{{ minio_secret_key_generated }}" when: minio_secret_key|length == 0 + - name: Generating jwt secret key + block: + - name: Generating jwt access key + set_fact: + jwt_secret_key_generated: "{{ lookup('password', '/dev/null length=30 chars=ascii_letters') }}" + - name: Updating vars.yaml + lineinfile: + regexp: '^jwt_secret_key' + line: 'jwt_secret_key: "{{jwt_secret_key_generated}}"' + path: vars.yaml + - name: Generating jwt secret key + set_fact: + jwt_access_key: "{{ jwt_secret_key_generated }}" + when: jwt_secret_key|length == 0 rescue: - name: Caught error debug: msg: - - Below variabls are mandatory. Please make sure it's updated in vars.yaml + - Below variables are mandatory. Please make sure it is updated in vars.yaml - kubeconfig_path - - ecr_docker_username - - ecr_docker_password - - ecr_docker_registry_server + - docker_registry_username + - docker_registry_password + - docker_registry_url failed_when: true tags: pre-check - name: Creating Nginx SSL certificate diff --git a/scripts/helm/roles/openreplay/templates/alert.yaml b/scripts/helm/roles/openreplay/templates/alert.yaml index 0200a406a..1ba439a63 100644 --- a/scripts/helm/roles/openreplay/templates/alert.yaml +++ b/scripts/helm/roles/openreplay/templates/alert.yaml @@ -4,9 +4,9 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/assets.yaml b/scripts/helm/roles/openreplay/templates/assets.yaml index d7be9fa9d..1f21147bb 100644 --- a/scripts/helm/roles/openreplay/templates/assets.yaml +++ b/scripts/helm/roles/openreplay/templates/assets.yaml @@ -7,6 +7,6 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/chalice.yaml b/scripts/helm/roles/openreplay/templates/chalice.yaml index 90b6de579..28325eb64 100644 --- a/scripts/helm/roles/openreplay/templates/chalice.yaml +++ b/scripts/helm/roles/openreplay/templates/chalice.yaml @@ -4,7 +4,7 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} env: @@ -13,3 +13,4 @@ env: sourcemaps_bucket_key: "{{ minio_access_key }}" sourcemaps_bucket_secret: "{{ minio_secret_key }}" S3_HOST: "https://{{ domain_name }}" + jwt_secret: "{{ jwt_secret_key }}" diff --git a/scripts/helm/roles/openreplay/templates/db.yaml b/scripts/helm/roles/openreplay/templates/db.yaml index 0200a406a..1ba439a63 100644 --- a/scripts/helm/roles/openreplay/templates/db.yaml +++ b/scripts/helm/roles/openreplay/templates/db.yaml @@ -4,9 +4,9 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/ender.yaml b/scripts/helm/roles/openreplay/templates/ender.yaml index 560483e94..2d51506ea 100644 --- a/scripts/helm/roles/openreplay/templates/ender.yaml +++ b/scripts/helm/roles/openreplay/templates/ender.yaml @@ -4,6 +4,6 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/http.yaml b/scripts/helm/roles/openreplay/templates/http.yaml index d7be9fa9d..1f21147bb 100644 --- a/scripts/helm/roles/openreplay/templates/http.yaml +++ b/scripts/helm/roles/openreplay/templates/http.yaml @@ -7,6 +7,6 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/integrations.yaml b/scripts/helm/roles/openreplay/templates/integrations.yaml index 560483e94..2d51506ea 100644 --- a/scripts/helm/roles/openreplay/templates/integrations.yaml +++ b/scripts/helm/roles/openreplay/templates/integrations.yaml @@ -4,6 +4,6 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/sink.yaml b/scripts/helm/roles/openreplay/templates/sink.yaml index 560483e94..2d51506ea 100644 --- a/scripts/helm/roles/openreplay/templates/sink.yaml +++ b/scripts/helm/roles/openreplay/templates/sink.yaml @@ -4,6 +4,6 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/storage.yaml b/scripts/helm/roles/openreplay/templates/storage.yaml index d7be9fa9d..1f21147bb 100644 --- a/scripts/helm/roles/openreplay/templates/storage.yaml +++ b/scripts/helm/roles/openreplay/templates/storage.yaml @@ -7,6 +7,6 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" -{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} +{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/vars.yaml b/scripts/helm/vars.yaml index 1bdd7a6bf..bdade016c 100644 --- a/scripts/helm/vars.yaml +++ b/scripts/helm/vars.yaml @@ -7,22 +7,22 @@ # Give absolute file path. # Use following command to get the full file path # `readlink -f ` -kubeconfig_path: "" +kubeconfig_path: /home/rajeshr/.kube/config ################### ## Optional Fields. ################### # If you've private registry, please update the details here. -ecr_docker_username: "" -ecr_docker_password: "" -ecr_docker_registry_server: "rg.fr-par.scw.cloud/foss" -image_tag: v1.0.0 +docker_registry_username: "" +docker_registry_password: "" +docker_registry_url: "rg.fr-par.scw.cloud/foss" +image_tag: "latest" # This is an optional field. If you want to use proper ssl, then it's mandatory -# Using which domain name, you'll be accessing openreplay -# for exmample: domain_name: "openreplay.mycorp.org" -domain_name: "" +# Using which domain name, you'll be accessing OpenReplay +# for example: domain_name: "test.com" +domain_name: "" # Nginx ssl certificates. # in cert format @@ -39,16 +39,21 @@ domain_name: "" nginx_ssl_cert_file_path: "" nginx_ssl_key_file_path: "" +# This key is used to create password for chalice api requests. +# Create a strong password. +# By default, a default key will be generated and will update the value here. +jwt_secret_key: "" + # Enable monitoring # If set, monitoring stack will be installed # including, prometheus, grafana and other core components, -# to scrape the metrics. But this will cost, additional resources(cpu and memory). +# to scrape the metrics. But this will cost, additional resources (cpu and memory). # Monitoring won't be installed on base installation. enable_monitoring: "false" # Random password for minio, # If not defined, will generate at runtime. -# Use following command to generate passwordwill give +# Use following command to generate password # `openssl rand -base64 30` minio_access_key: "" minio_secret_key: "" diff --git a/sourcemap-uploader/cli.js b/sourcemap-uploader/cli.js index f7b124117..c644369f3 100755 --- a/sourcemap-uploader/cli.js +++ b/sourcemap-uploader/cli.js @@ -18,10 +18,13 @@ parser.addArgument(['-p', '-i', '--project-key'], { // -i is depricated help: 'Project Key', required: true, }); - parser.addArgument(['-s', '--server'], { help: 'OpenReplay API server URL for upload', }); +parser.addArgument(['-l', '--log'], { + help: 'Log requests information', + action: 'storeTrue', +}); const subparsers = parser.addSubparsers({ title: 'commands', @@ -50,7 +53,9 @@ dir.addArgument(['-u', '--js-dir-url'], { // TODO: exclude in dir -const { command, api_key, project_key, server, ...args } = parser.parseArgs(); +const { command, api_key, project_key, server, log, ...args } = parser.parseArgs(); + +global.LOG = !!log; try { global.SERVER = new URL(server || "https://api.openreplay.com"); diff --git a/sourcemap-uploader/lib/readDir.js b/sourcemap-uploader/lib/readDir.js index 56a51a72b..501a2949f 100644 --- a/sourcemap-uploader/lib/readDir.js +++ b/sourcemap-uploader/lib/readDir.js @@ -3,7 +3,9 @@ const readFile = require('./readFile'); module.exports = (sourcemap_dir_path, js_dir_url) => { sourcemap_dir_path = (sourcemap_dir_path + '/').replace(/\/+/g, '/'); - js_dir_url = (js_dir_url + '/').replace(/\/+/g, '/'); + if (js_dir_url[ js_dir_url.length - 1 ] !== '/') { // replace will break schema + js_dir_url += '/'; + } return glob(sourcemap_dir_path + '**/*.map').then(sourcemap_file_paths => Promise.all( sourcemap_file_paths.map(sourcemap_file_path => diff --git a/sourcemap-uploader/lib/uploadSourcemaps.js b/sourcemap-uploader/lib/uploadSourcemaps.js index a39ce5e4d..f0c3171fd 100644 --- a/sourcemap-uploader/lib/uploadSourcemaps.js +++ b/sourcemap-uploader/lib/uploadSourcemaps.js @@ -7,14 +7,21 @@ const getUploadURLs = (api_key, project_key, js_file_urls) => } const pathPrefix = (global.SERVER.pathname + "/").replace(/\/+/g, '/'); + const options = { + method: 'PUT', + hostname: global.SERVER.host, + path: pathPrefix + `${project_key}/sourcemaps/`, + headers: { Authorization: api_key, 'Content-Type': 'application/json' }, + } + if (global.LOG) { + console.log("Request: ", options, "\nFiles: ", js_file_urls); + } const req = https.request( - { - method: 'PUT', - hostname: global.SERVER.host, - path: pathPrefix + `${project_key}/sourcemaps/`, - headers: { Authorization: api_key, 'Content-Type': 'application/json' }, - }, + options, res => { + if (global.LOG) { + console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage); + } if (res.statusCode === 403) { reject("Authorisation rejected. Please, check your API_KEY and/or PROJECT_KEY.") return @@ -24,7 +31,12 @@ const getUploadURLs = (api_key, project_key, js_file_urls) => } let data = ''; res.on('data', s => (data += s)); - res.on('end', () => resolve(JSON.parse(data).data)); + res.on('end', () => { + if (global.LOG) { + console.log("Server Response: ", data) + } + resolve(JSON.parse(data).data) + }); }, ); req.on('error', reject); @@ -46,8 +58,12 @@ const uploadSourcemap = (upload_url, body) => }, res => { if (res.statusCode !== 200) { + if (global.LOG) { + console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage); + } + reject("Unable to upload. Please, contact OpenReplay support."); - return; + return; // TODO: report per-file errors. } resolve(); //res.on('end', resolve); diff --git a/sourcemap-uploader/package.json b/sourcemap-uploader/package.json index 2ffe6f5b7..8f0070408 100644 --- a/sourcemap-uploader/package.json +++ b/sourcemap-uploader/package.json @@ -1,6 +1,6 @@ { "name": "@openreplay/sourcemap-uploader", - "version": "3.0.1", + "version": "3.0.2", "description": "NPM module to upload your JS sourcemaps files to OpenReplay", "bin": "cli.js", "main": "index.js", diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 1ea5ac2bb..00bf42046 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "3.0.2", + "version": "3.0.3", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 91dc20322..3fe006ce8 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -174,7 +174,24 @@ export default class App { _start(reset: boolean): void { // TODO: return a promise instead of onStart handling if (!this.isActive) { this.isActive = true; + if (!this.worker) { + throw new Error("Stranger things: no worker found"); + } + + let pageNo: number = 0; + const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key); + if (pageNoStr != null) { + pageNo = parseInt(pageNoStr); + pageNo++; + } + sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); const startTimestamp = timestamp(); + + this.worker.postMessage({ ingestPoint: this.options.ingestPoint, pageNo, startTimestamp }); // brings delay of 10th ms? + this.observer.observe(); + this.startCallbacks.forEach((cb) => cb()); + this.ticker.start(); + window.fetch(this.options.ingestPoint + '/v1/web/start', { method: 'POST', headers: { @@ -196,7 +213,7 @@ export default class App { .then(r => { if (r.status === 200) { return r.json() - } else { // TODO: handle canceling + } else { // TODO: handle canceling && 403 throw new Error("Server error"); } }) @@ -206,26 +223,14 @@ export default class App { typeof userUUID !== 'string') { throw new Error("Incorrect server responce"); } - if (!this.worker) { - throw new Error("Stranger things: worker is not started"); - } sessionStorage.setItem(this.options.session_token_key, token); localStorage.setItem(this.options.local_uuid_key, userUUID); - - let pageNo: number = 0; - const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key); - if (pageNoStr != null) { - pageNo = parseInt(pageNoStr); - pageNo++; + if (!this.worker) { + throw new Error("Stranger things: no worker found after start request"); } - sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); - - this.worker.postMessage({ ingestPoint: this.options.ingestPoint, token, pageNo, startTimestamp }); - this.observer.observe(); - this.startCallbacks.forEach((cb) => cb()); - this.ticker.start(); - log("OpenReplay tracking started."); + this.worker.postMessage({ token }); + log("OpenReplay tracking started."); if (typeof this.options.onStart === 'function') { this.options.onStart({ sessionToken: token, userUUID, sessionID: token /* back compat (depricated) */ }); } @@ -254,7 +259,7 @@ export default class App { if (this.isActive) { try { if (this.worker) { - this.worker.postMessage(null); + this.worker.postMessage("stop"); } this.observer.disconnect(); this.nodes.clear(); diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index def27c55a..d6c8481df 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -85,6 +85,8 @@ export default class API { ? null : new App(options.projectKey, options.sessionToken, options); if (this.app !== null) { + Viewport(this.app); + CSSRules(this.app); Connection(this.app); Console(this.app, options); Exception(this.app, options); @@ -94,9 +96,7 @@ export default class API { Timing(this.app, options); Performance(this.app); Scroll(this.app); - Viewport(this.app); Longtasks(this.app); - CSSRules(this.app); (window as any).__OPENREPLAY__ = (window as any).__OPENREPLAY__ || this; } else { console.log("OpenReplay: broeser doesn't support API required for tracking.") diff --git a/tracker/tracker/src/webworker/index.ts b/tracker/tracker/src/webworker/index.ts index 2e9d3b0e0..f61b7bca5 100644 --- a/tracker/tracker/src/webworker/index.ts +++ b/tracker/tracker/src/webworker/index.ts @@ -1,4 +1,4 @@ -import { classes, BatchMeta, Timestamp, SetPageVisibility } from '../messages'; +import { classes, BatchMeta, Timestamp, SetPageVisibility, CreateDocument } from '../messages'; import Message from '../messages/message'; import Writer from '../messages/writer'; @@ -12,8 +12,11 @@ let ingestPoint: string = ""; let token: string = ""; let pageNo: number = 0; let timestamp: number = 0; +let timeAdjustment: number = 0; let nextIndex: number = 0; -let isEmpty: boolean = true; +// TODO: clear logic: isEmpty here means presense of BatchMeta but absence of other messages +// BatchWriter should be abstracted +let isEmpty: boolean = true; function writeBatchMeta(): boolean { // TODO: move to encoder return new BatchMeta(pageNo, nextIndex, timestamp).encode(writer) @@ -67,7 +70,7 @@ function sendBatch(batch: Uint8Array):void { } function send(): void { - if (isEmpty || ingestPoint === "") { + if (isEmpty || token === "" || ingestPoint === "") { return; } const batch = writer.flush(); @@ -82,29 +85,45 @@ function send(): void { } function reset() { + ingestPoint = "" + token = "" clearInterval(sendIntervalID); writer.reset(); } let restartTimeoutID: ReturnType; +function hasTimestamp(msg: any): msg is { timestamp: number } { + return typeof msg === 'object' && typeof msg.timestamp === 'number'; +} + self.onmessage = ({ data }: MessageEvent) => { if (data === null) { send(); return; } - if (!Array.isArray(data)) { + if (data === "stop") { + send(); reset(); - ingestPoint = data.ingestPoint; - token = data.token; - pageNo = data.pageNo; - timestamp = data.startTimestamp; - writeBatchMeta(); - sendIntervalID = setInterval(send, SEND_INTERVAL); + return; + } + if (!Array.isArray(data)) { + ingestPoint = data.ingestPoint || ingestPoint; + token = data.token || token; + pageNo = data.pageNo || pageNo; + timestamp = data.startTimestamp || timestamp; + timeAdjustment = data.timeAdjustment || timeAdjustment; + if (writer.isEmpty()) { + writeBatchMeta(); + } + if (sendIntervalID == null) { + sendIntervalID = setInterval(send, SEND_INTERVAL); + } return; } data.forEach((data: any) => { const message: Message = new (classes.get(data._id))(); + Object.assign(message, data); if (message instanceof Timestamp) { timestamp = (message).timestamp; @@ -116,7 +135,6 @@ self.onmessage = ({ data }: MessageEvent) => { } } - Object.assign(message, data); writer.checkpoint(); nextIndex++; if (message.encode(writer)) { From c8e50cc9247e9b4d69529e917cbcf938ebe7b312 Mon Sep 17 00:00:00 2001 From: Mehdi Osman Date: Fri, 21 May 2021 19:49:42 +0200 Subject: [PATCH 2/9] feat: add lib/pq to third-party Co-Authored-By: Mehdi Osman --- third-party.md | 1 + 1 file changed, 1 insertion(+) diff --git a/third-party.md b/third-party.md index 42268ef8a..ac55546fd 100644 --- a/third-party.md +++ b/third-party.md @@ -83,3 +83,4 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan | serverless | MIT | JavaScript | | schedule | MIT | Python | | croniter | MIT | Python | +| lib/pq | MIT | Go | From 328f0a7a56962f5792de99fdbc31a9285840fb24 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Sat, 22 May 2021 00:26:14 +0530 Subject: [PATCH 3/9] Revert "Bug fixes and features. (#7)" (#8) This reverts commit 2e86b6eb6afddcae2cbb8a1e23dbd42deef83a0d. --- .github/workflows/api.yaml | 6 +- api/.chalice/config.json | 10 +- api/.gitignore | 2 +- api/Dockerfile | 10 +- api/app.py | 36 +- api/chalicelib/blueprints/bp_core.py | 2 +- api/chalicelib/blueprints/bp_core_dynamic.py | 31 +- api/chalicelib/core/collaboration_slack.py | 17 +- api/chalicelib/core/dashboard.py | 4 + api/chalicelib/core/events.py | 2 +- .../core/integration_jira_cloud_issue.py | 2 +- api/chalicelib/core/sessions.py | 9 +- api/chalicelib/core/sessions_assignments.py | 1 + api/chalicelib/core/sessions_mobs.py | 10 +- api/chalicelib/core/sourcemaps.py | 5 - api/chalicelib/core/sourcemaps_parser.py | 9 +- api/chalicelib/core/telemetry.py | 2 +- api/chalicelib/core/tenants.py | 2 +- api/chalicelib/core/webhook.py | 10 +- api/chalicelib/utils/jira_client.py | 3 +- api/chalicelib/utils/pg_client.py | 25 +- api/chalicelib/utils/s3.py | 27 +- api/entrypoint.sh | 6 - api/requirements.txt | 3 + api/sourcemaps_reader/handler.js | 111 --- api/sourcemaps_reader/server.js | 38 - backend/pkg/db/postgres/messages_web.go | 8 +- backend/services/db/messages.go | 1 - backend/services/ender/builder/builder.go | 25 +- .../ender/builder/inputEventBuilder.go | 8 +- .../integrations/integration/sentry.go | 2 +- backend/services/integrations/main.go | 14 +- ee/api/.chalice/config.json | 10 +- ee/api/.gitignore | 4 +- ee/api/app.py | 37 +- ee/api/chalicelib/blueprints/bp_core.py | 2 +- .../chalicelib/blueprints/bp_core_dynamic.py | 28 +- ee/api/chalicelib/core/collaboration_slack.py | 17 +- ee/api/chalicelib/core/events.py | 2 +- .../core/integration_jira_cloud_issue.py | 2 +- ee/api/chalicelib/core/sessions.py | 9 +- .../chalicelib/core/sessions_assignments.py | 1 + ee/api/chalicelib/core/sessions_mobs.py | 6 +- ee/api/chalicelib/core/sourcemaps.py | 5 - ee/api/chalicelib/core/sourcemaps_parser.py | 9 +- ee/api/chalicelib/ee/webhook.py | 12 +- ee/api/chalicelib/utils/jira_client.py | 3 +- ee/api/chalicelib/utils/pg_client.py | 26 +- ee/api/chalicelib/utils/s3.py | 59 +- ee/api/requirements.txt | 3 + ee/api/sourcemaps_reader/handler.js | 111 --- ee/api/sourcemaps_reader/server.js | 38 - ee/connectors/bigquery_utils/create_table.py | 357 --------- ee/connectors/db/api.py | 129 --- ee/connectors/db/loaders/__init__.py | 0 ee/connectors/db/loaders/bigquery_loader.py | 34 - ee/connectors/db/loaders/clickhouse_loader.py | 4 - ee/connectors/db/loaders/postgres_loader.py | 3 - ee/connectors/db/loaders/redshift_loader.py | 19 - ee/connectors/db/loaders/snowflake_loader.py | 5 - ee/connectors/db/models.py | 389 --------- ee/connectors/db/tables.py | 61 -- ee/connectors/db/utils.py | 368 --------- ee/connectors/db/writer.py | 63 -- ee/connectors/handler.py | 647 --------------- ee/connectors/main.py | 121 --- ee/connectors/msgcodec/codec.py | 670 ---------------- ee/connectors/msgcodec/messages.py | 752 ------------------ ee/connectors/requirements.txt | 43 - ee/connectors/sql/clickhouse_events.sql | 56 -- .../sql/clickhouse_events_buffer.sql | 52 -- ee/connectors/sql/clickhouse_sessions.sql | 52 -- .../sql/clickhouse_sessions_buffer.sql | 50 -- ee/connectors/sql/postgres_events.sql | 52 -- ee/connectors/sql/postgres_sessions.sql | 50 -- ee/connectors/sql/redshift_events.sql | 52 -- ee/connectors/sql/redshift_sessions.sql | 50 -- ee/connectors/sql/snowflake_events.sql | 52 -- ee/connectors/sql/snowflake_sessions.sql | 50 -- ee/connectors/utils/bigquery.env.example | 7 - .../bigquery_service_account.json.example | 12 - ee/connectors/utils/clickhouse.env.example | 7 - ee/connectors/utils/pg.env.example | 10 - ee/connectors/utils/redshift.env.example | 15 - ee/connectors/utils/snowflake.env.example | 11 - .../db/init_dbs/postgresql/init_schema.sql | 215 ++--- frontend/app/Router.js | 8 +- frontend/app/assets/apple-touch-icon.png | Bin 6253 -> 0 bytes frontend/app/assets/favicon-16x16.png | Bin 799 -> 0 bytes frontend/app/assets/favicon-32x32.png | Bin 1090 -> 0 bytes frontend/app/assets/favicon.ico | Bin 15086 -> 0 bytes frontend/app/assets/favicon@1x.png | Bin 0 -> 2127 bytes frontend/app/assets/favicon@2x.png | Bin 0 -> 5829 bytes frontend/app/assets/favicon@3x.png | Bin 0 -> 10941 bytes frontend/app/assets/favicon@4x.png | Bin 0 -> 16394 bytes frontend/app/assets/favicon@5x.png | Bin 0 -> 24304 bytes frontend/app/assets/favicon@6x.png | Bin 0 -> 32078 bytes frontend/app/assets/index.html | 9 +- .../BugFinder/CustomFilters/FilterItem.js | 2 +- .../app/components/BugFinder/DateRange.js | 5 +- .../BugFinder/SessionsMenu/SessionsMenu.js | 11 +- .../Integrations/SlackAddForm/SlackAddForm.js | 14 +- .../SlackChannelList/SlackChannelList.js | 27 +- .../Client/ManageUsers/ManageUsers.js | 54 +- .../Client/PreferencesMenu/PreferencesMenu.js | 2 +- .../Client/ProfileSettings/OptOut.js | 2 +- .../Widgets/SessionsPerBrowser/Bar.css | 2 +- .../app/components/Errors/Error/ErrorInfo.js | 2 +- .../Funnels/FunnelDetails/FunnelDetails.js | 4 +- .../Funnels/FunnelHeader/FunnelHeader.js | 1 - .../Header/Discover/featureItem.css | 2 +- .../Header/OnboardingExplore/FeatureItem.js | 2 +- .../OnboardingExplore/OnboardingExplore.js | 4 +- .../Header/OnboardingExplore/featureItem.css | 2 +- frontend/app/components/Login/Login.js | 2 +- .../OnboardingNavButton.js | 12 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 7 +- .../Session_/Issues/IssueDetails.js | 6 +- .../components/Session_/Issues/IssueForm.js | 11 +- .../components/Session_/Issues/IssueHeader.js | 3 +- .../Session_/Issues/IssueListItem.js | 3 +- .../app/components/Session_/Issues/Issues.js | 5 +- .../components/Session_/Network/Network.js | 48 +- .../Session_/Network/NetworkContent.js | 8 +- .../Session_/Player/Controls/Timeline.js | 36 +- .../StackEvents/UserEvent/UserEvent.js | 2 +- .../Signup/SignupForm/SignupForm.js | 2 +- .../shared/BannerMessage/BannerMessage.js | 28 - .../components/shared/BannerMessage/index.js | 1 - frontend/app/components/shared/DateRange.js | 3 +- .../DateRangeDropdown/DateRangeDropdown.js | 4 +- .../DateRangeDropdown/dateRangeDropdown.css | 4 - .../app/components/shared/DocLink/DocLink.js | 7 +- .../IntegrateSlackButton.js | 26 - .../shared/IntegrateSlackButton/index.js | 1 - .../NoSessionsMessage/NoSessionsMessage.js | 2 +- .../shared/SharePopup/SharePopup.js | 14 +- .../ProjectCodeSnippet/ProjectCodeSnippet.js | 7 +- .../ui/ErrorDetails/ErrorDetails.js | 2 +- frontend/app/duck/assignments.js | 18 +- frontend/app/duck/integrations/slack.js | 9 - frontend/app/duck/user.js | 22 +- .../MessageDistributor/MessageDistributor.js | 33 +- frontend/app/svg/icons/funnel/cpu.svg | 3 - frontend/app/svg/icons/funnel/dizzy.svg | 1 - frontend/app/svg/icons/funnel/emoji-angry.svg | 4 - .../svg/icons/funnel/file-earmark-break.svg | 3 - frontend/app/svg/icons/funnel/image.svg | 4 - frontend/app/svg/icons/funnel/sd-card.svg | 5 +- frontend/app/types/account/account.js | 1 - .../app/types/integrations/issueTracker.js | 1 + frontend/app/types/issue/issuesType.js | 9 - frontend/app/types/session/issue.js | 43 - frontend/app/types/session/session.js | 9 +- frontend/app/types/watchdog.js | 12 +- frontend/env.js | 2 +- scripts/helm/README.md | 38 +- scripts/helm/app/README.md | 11 +- scripts/helm/app/alerts.yaml | 4 +- scripts/helm/app/assets.yaml | 4 +- scripts/helm/app/chalice.yaml | 5 +- scripts/helm/app/http.yaml | 4 +- scripts/helm/app/issues.md | 76 ++ .../app/openreplay/templates/deployment.yaml | 3 +- scripts/helm/app/openreplay/values.yaml | 2 +- scripts/helm/app/storage.yaml | 8 +- .../db/init_dbs/postgresql/init_schema.sql | 544 ++++++------- scripts/helm/db/sqs/.helmignore | 23 + scripts/helm/db/sqs/Chart.yaml | 23 + scripts/helm/db/sqs/templates/NOTES.txt | 22 + scripts/helm/db/sqs/templates/_helpers.tpl | 62 ++ scripts/helm/db/sqs/templates/configmap.yaml | 28 + scripts/helm/db/sqs/templates/deployment.yaml | 64 ++ scripts/helm/db/sqs/templates/hpa.yaml | 28 + scripts/helm/db/sqs/templates/ingress.yaml | 41 + scripts/helm/db/sqs/templates/service.yaml | 19 + .../helm/db/sqs/templates/serviceaccount.yaml | 12 + scripts/helm/db/sqs/values.yaml | 111 +++ scripts/helm/install.sh | 16 - scripts/helm/kube-install.sh | 45 +- .../nginx-ingress/nginx-ingress/README.md | 18 +- .../nginx-ingress/templates/configmap.yaml | 16 +- .../nginx-ingress/templates/deployment.yaml | 3 +- .../nginx-ingress/templates/service.yaml | 2 - scripts/helm/openreplay-cli | 22 +- .../helm/roles/openreplay/defaults/main.yml | 1 + .../roles/openreplay/tasks/install-apps.yaml | 2 +- scripts/helm/roles/openreplay/tasks/main.yml | 8 +- .../roles/openreplay/tasks/pre-check.yaml | 36 +- .../roles/openreplay/templates/alert.yaml | 4 +- .../roles/openreplay/templates/assets.yaml | 2 +- .../roles/openreplay/templates/chalice.yaml | 3 +- .../helm/roles/openreplay/templates/db.yaml | 4 +- .../roles/openreplay/templates/ender.yaml | 2 +- .../helm/roles/openreplay/templates/http.yaml | 2 +- .../openreplay/templates/integrations.yaml | 2 +- .../helm/roles/openreplay/templates/sink.yaml | 2 +- .../roles/openreplay/templates/storage.yaml | 2 +- scripts/helm/vars.yaml | 25 +- sourcemap-uploader/cli.js | 9 +- sourcemap-uploader/lib/readDir.js | 4 +- sourcemap-uploader/lib/uploadSourcemaps.js | 32 +- sourcemap-uploader/package.json | 2 +- tracker/tracker/package.json | 2 +- tracker/tracker/src/main/app/index.ts | 43 +- tracker/tracker/src/main/index.ts | 4 +- tracker/tracker/src/webworker/index.ts | 40 +- 207 files changed, 1432 insertions(+), 5912 deletions(-) delete mode 100755 api/entrypoint.sh delete mode 100644 api/sourcemaps_reader/handler.js delete mode 100644 api/sourcemaps_reader/server.js delete mode 100644 ee/api/sourcemaps_reader/handler.js delete mode 100644 ee/api/sourcemaps_reader/server.js delete mode 100644 ee/connectors/bigquery_utils/create_table.py delete mode 100644 ee/connectors/db/api.py delete mode 100644 ee/connectors/db/loaders/__init__.py delete mode 100644 ee/connectors/db/loaders/bigquery_loader.py delete mode 100644 ee/connectors/db/loaders/clickhouse_loader.py delete mode 100644 ee/connectors/db/loaders/postgres_loader.py delete mode 100644 ee/connectors/db/loaders/redshift_loader.py delete mode 100644 ee/connectors/db/loaders/snowflake_loader.py delete mode 100644 ee/connectors/db/models.py delete mode 100644 ee/connectors/db/tables.py delete mode 100644 ee/connectors/db/utils.py delete mode 100644 ee/connectors/db/writer.py delete mode 100644 ee/connectors/handler.py delete mode 100644 ee/connectors/main.py delete mode 100644 ee/connectors/msgcodec/codec.py delete mode 100644 ee/connectors/msgcodec/messages.py delete mode 100644 ee/connectors/requirements.txt delete mode 100644 ee/connectors/sql/clickhouse_events.sql delete mode 100644 ee/connectors/sql/clickhouse_events_buffer.sql delete mode 100644 ee/connectors/sql/clickhouse_sessions.sql delete mode 100644 ee/connectors/sql/clickhouse_sessions_buffer.sql delete mode 100644 ee/connectors/sql/postgres_events.sql delete mode 100644 ee/connectors/sql/postgres_sessions.sql delete mode 100644 ee/connectors/sql/redshift_events.sql delete mode 100644 ee/connectors/sql/redshift_sessions.sql delete mode 100644 ee/connectors/sql/snowflake_events.sql delete mode 100644 ee/connectors/sql/snowflake_sessions.sql delete mode 100644 ee/connectors/utils/bigquery.env.example delete mode 100644 ee/connectors/utils/bigquery_service_account.json.example delete mode 100644 ee/connectors/utils/clickhouse.env.example delete mode 100644 ee/connectors/utils/pg.env.example delete mode 100644 ee/connectors/utils/redshift.env.example delete mode 100644 ee/connectors/utils/snowflake.env.example delete mode 100644 frontend/app/assets/apple-touch-icon.png delete mode 100644 frontend/app/assets/favicon-16x16.png delete mode 100644 frontend/app/assets/favicon-32x32.png delete mode 100644 frontend/app/assets/favicon.ico create mode 100644 frontend/app/assets/favicon@1x.png create mode 100644 frontend/app/assets/favicon@2x.png create mode 100644 frontend/app/assets/favicon@3x.png create mode 100644 frontend/app/assets/favicon@4x.png create mode 100644 frontend/app/assets/favicon@5x.png create mode 100644 frontend/app/assets/favicon@6x.png delete mode 100644 frontend/app/components/shared/BannerMessage/BannerMessage.js delete mode 100644 frontend/app/components/shared/BannerMessage/index.js delete mode 100644 frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js delete mode 100644 frontend/app/components/shared/IntegrateSlackButton/index.js delete mode 100644 frontend/app/svg/icons/funnel/cpu.svg delete mode 100644 frontend/app/svg/icons/funnel/dizzy.svg delete mode 100644 frontend/app/svg/icons/funnel/emoji-angry.svg delete mode 100644 frontend/app/svg/icons/funnel/file-earmark-break.svg delete mode 100644 frontend/app/svg/icons/funnel/image.svg delete mode 100644 frontend/app/types/session/issue.js create mode 100644 scripts/helm/app/issues.md create mode 100644 scripts/helm/db/sqs/.helmignore create mode 100644 scripts/helm/db/sqs/Chart.yaml create mode 100644 scripts/helm/db/sqs/templates/NOTES.txt create mode 100644 scripts/helm/db/sqs/templates/_helpers.tpl create mode 100644 scripts/helm/db/sqs/templates/configmap.yaml create mode 100644 scripts/helm/db/sqs/templates/deployment.yaml create mode 100644 scripts/helm/db/sqs/templates/hpa.yaml create mode 100644 scripts/helm/db/sqs/templates/ingress.yaml create mode 100644 scripts/helm/db/sqs/templates/service.yaml create mode 100644 scripts/helm/db/sqs/templates/serviceaccount.yaml create mode 100644 scripts/helm/db/sqs/values.yaml diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index c247b2a68..435d07126 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -39,11 +39,13 @@ jobs: ENVIRONMENT: staging run: | cd api - PUSH_IMAGE=1 bash build.sh + bash build.sh + [[ -z "${DOCKER_REPO}" ]] || { + docker push ${DOCKER_REPO}/chalice:"${IMAGE_TAG}" + } - name: Deploy to kubernetes run: | cd scripts/helm/ - 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/tag:.*/tag: \"$IMAGE_TAG\"/g" app/chalice.yaml bash kube-install.sh --app chalice diff --git a/api/.chalice/config.json b/api/.chalice/config.json index 8385a17e7..8f2874beb 100644 --- a/api/.chalice/config.json +++ b/api/.chalice/config.json @@ -28,12 +28,14 @@ "assign_link": "http://127.0.0.1:8000/async/email_assignment", "captcha_server": "", "captcha_key": "", - "sessions_bucket": "mobs", + "sessions_bucket": "asayer-mobs", "sessions_region": "us-east-1", "put_S3_TTL": "20", - "sourcemaps_reader": "http://127.0.0.1:3000/", - "sourcemaps_bucket": "sourcemaps", - "js_cache_bucket": "sessions-assets", + "sourcemaps_bucket": "asayer-sourcemaps", + "sourcemaps_bucket_key": "", + "sourcemaps_bucket_secret": "", + "sourcemaps_bucket_region": "us-east-1", + "js_cache_bucket": "asayer-sessions-assets", "async_Token": "", "EMAIL_HOST": "", "EMAIL_PORT": "587", diff --git a/api/.gitignore b/api/.gitignore index dd32b5d3f..d9688e343 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -170,7 +170,7 @@ logs*.txt *.csv *.p +*.js SUBNETS.json ./chalicelib/.configs -README/* \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 84d1b88f5..0ca8c1edf 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -4,14 +4,6 @@ WORKDIR /work COPY . . 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 sourcemaps_reader && \ - npm install # Add Tini # Startup daemon @@ -21,4 +13,4 @@ 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 +CMD python env_handler.py && chalice local --no-autoreload --host 0.0.0.0 --stage ${ENTERPRISE_BUILD} \ No newline at end of file diff --git a/api/app.py b/api/app.py index 2c4465189..469d8a42f 100644 --- a/api/app.py +++ b/api/app.py @@ -23,13 +23,13 @@ import traceback old_tb = traceback.print_exception old_f = sys.stdout old_e = sys.stderr -OR_SESSION_TOKEN = None +ASAYER_SESSION_ID = None 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}") + if ASAYER_SESSION_ID is not None and x != '\n' and not helper.is_local(): + old_f.write(f"[asayer_session_id={ASAYER_SESSION_ID}] {x}") else: old_f.write(x) @@ -38,8 +38,9 @@ class F: 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)) + if ASAYER_SESSION_ID is not None and not helper.is_local(): + # bugsnag.notify(Exception(str(value)), meta_data={"special_info": {"asayerSessionId": ASAYER_SESSION_ID}}) + value = type(value)(f"[asayer_session_id={ASAYER_SESSION_ID}] " + str(value)) old_tb(etype, value, tb, limit, file, chain) @@ -54,11 +55,11 @@ sys.stderr = F() _overrides.chalice_app(app) - +# v0905 @app.middleware('http') -def or_middleware(event, get_response): - global OR_SESSION_TOKEN - OR_SESSION_TOKEN = app.current_request.headers.get('vnd.openreplay.com.sid', +def asayer_middleware(event, get_response): + global ASAYER_SESSION_ID + ASAYER_SESSION_ID = 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!!") @@ -70,24 +71,19 @@ def or_middleware(event, get_response): import time now = int(time.time() * 1000) response = get_response(event) - if response.status_code == 500 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(): + print("middleware exception handling") + print(e) + pg_client.close() + if helper.allow_sentry() and ASAYER_SESSION_ID 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_tag('openReplaySessionToken', ASAYER_SESSION_ID) 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) + raise e pg_client.close() return response diff --git a/api/chalicelib/blueprints/bp_core.py b/api/chalicelib/blueprints/bp_core.py index bd42b2254..3b2910606 100644 --- a/api/chalicelib/blueprints/bp_core.py +++ b/api/chalicelib/blueprints/bp_core.py @@ -881,5 +881,5 @@ def all_issue_types(context): @app.route('/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) @app.route('/{projectId}/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) -def removed_endpoints(projectId=None, context=None): +def removed_endpoints(context): return Response(body={"errors": ["Endpoint no longer available"]}, status_code=410) diff --git a/api/chalicelib/blueprints/bp_core_dynamic.py b/api/chalicelib/blueprints/bp_core_dynamic.py index 1768896f9..4ec5278d7 100644 --- a/api/chalicelib/blueprints/bp_core_dynamic.py +++ b/api/chalicelib/blueprints/bp_core_dynamic.py @@ -35,7 +35,7 @@ def login(): 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 + for_plugin= False ) if r is None: return { @@ -73,12 +73,10 @@ def get_account(context): "projects": -1, "metadata": metadata.get_remaining_metadata_with_count(context['tenantId']) }, - **license.get_status(context["tenantId"]), - "smtp": environ["EMAIL_HOST"] is not None and len(environ["EMAIL_HOST"]) > 0 + **license.get_status(context["tenantId"]) } } - @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, @@ -158,28 +156,12 @@ 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: + if Slack.add_integration(tenant_id=context["tenantId"], url=data["url"], name=data["name"]): + return {"data": {"status": "success"}} + else: return { - "errors": ["We couldn't send you a test message on your Slack channel. Please verify your webhook url."] + "errors": ["failed URL verification, if you received a message on slack, please notify our dev-team"] } - 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): @@ -404,7 +386,6 @@ def search_sessions_by_metadata(context): m_key=key, project_id=project_id)} - @app.route('/plans', methods=['GET']) def get_current_plan(context): return { diff --git a/api/chalicelib/core/collaboration_slack.py b/api/chalicelib/core/collaboration_slack.py index b3da03a37..5fc80511c 100644 --- a/api/chalicelib/core/collaboration_slack.py +++ b/api/chalicelib/core/collaboration_slack.py @@ -6,18 +6,19 @@ from chalicelib.core import webhook class Slack: @classmethod - def add_channel(cls, tenant_id, **args): + def add_integration(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 + if cls.__say_hello(url): + webhook.add(tenant_id=tenant_id, + endpoint=url, + webhook_type="slack", + name=name) + return True + return False @classmethod - def say_hello(cls, url): + def __say_hello(cls, url): r = requests.post( url=url, json={ diff --git a/api/chalicelib/core/dashboard.py b/api/chalicelib/core/dashboard.py index f306a51b4..a778dcdfc 100644 --- a/api/chalicelib/core/dashboard.py +++ b/api/chalicelib/core/dashboard.py @@ -146,6 +146,7 @@ def get_processed_sessions(project_id, startTimestamp=TimeUTC.now(delta_days=-1) ORDER BY generated_timestamp;""" params = {"step_size": step_size, "project_id": project_id, "startTimestamp": startTimestamp, "endTimestamp": endTimestamp, **__get_constraint_values(args)} + print(cur.mogrify(pg_query, params)) cur.execute(cur.mogrify(pg_query, params)) rows = cur.fetchall() results = { @@ -639,6 +640,9 @@ def search(text, resource_type, project_id, performance=False, pages_only=False, FROM events.pages INNER JOIN public.sessions USING (session_id) WHERE {" AND ".join(pg_sub_query)} LIMIT 10;""" + print(cur.mogrify(pg_query, {"project_id": project_id, + "value": helper.string_to_sql_like(text), + "platform_0": platform})) cur.execute(cur.mogrify(pg_query, {"project_id": project_id, "value": helper.string_to_sql_like(text), "platform_0": platform})) diff --git a/api/chalicelib/core/events.py b/api/chalicelib/core/events.py index 69213a079..65ade49ed 100644 --- a/api/chalicelib/core/events.py +++ b/api/chalicelib/core/events.py @@ -365,7 +365,7 @@ def __get_merged_queries(queries, value, project_id): def __get_autocomplete_table(value, project_id): with pg_client.PostgresClient() as cur: cur.execute(cur.mogrify("""SELECT DISTINCT ON(value,type) project_id, value, type - FROM (SELECT project_id, type, value + FROM (SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY type ORDER BY value) AS Row_ID FROM public.autocomplete diff --git a/api/chalicelib/core/integration_jira_cloud_issue.py b/api/chalicelib/core/integration_jira_cloud_issue.py index bb847007a..00fac2fcb 100644 --- a/api/chalicelib/core/integration_jira_cloud_issue.py +++ b/api/chalicelib/core/integration_jira_cloud_issue.py @@ -34,7 +34,7 @@ class JIRACloudIntegrationIssue(BaseIntegrationIssue): if len(projects_map[integration_project_id]) > 0: jql += f" AND ID IN ({','.join(projects_map[integration_project_id])})" issues = self._client.get_issues(jql, offset=0) - results += issues + results += [issues] return {"issues": results} def get(self, integration_project_id, assignment_id): diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index fa127b04a..439bca0fd 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -1,6 +1,6 @@ from chalicelib.utils import pg_client, helper from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, \ - sessions_mobs, issues + sessions_mobs from chalicelib.utils import dev from chalicelib.core import projects, errors @@ -25,7 +25,7 @@ SESSION_PROJECTION_COLS = """s.project_id, s.user_anonymous_id, s.platform, s.issue_score, - to_jsonb(s.issue_types) AS issue_types, + s.issue_types::text[] AS issue_types, favorite_sessions.session_id NOTNULL AS favorite, COALESCE((SELECT TRUE FROM public.user_viewed_sessions AS fs @@ -84,6 +84,7 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_ios(sessionId=session_id) + data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data["socket"] = socket_ios.start_replay(project_id=project_id, session_id=session_id, device=data["userDevice"], os_version=data["userOsVersion"], @@ -100,11 +101,9 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events.get_customs_by_sessionId2_pg(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_web(sessionId=session_id) + data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data['resources'] = resources.get_by_session_id(session_id=session_id) - data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) - data['issues'] = issues.get_by_session_id(session_id=session_id) - return data return None diff --git a/api/chalicelib/core/sessions_assignments.py b/api/chalicelib/core/sessions_assignments.py index 3e0929dad..2b9c28d8f 100644 --- a/api/chalicelib/core/sessions_assignments.py +++ b/api/chalicelib/core/sessions_assignments.py @@ -119,6 +119,7 @@ def get_by_session(tenant_id, user_id, project_id, session_id): continue r = integration.issue_handler.get_by_ids(saved_issues=issues[tool]) + print(r) for i in r["issues"]: i["provider"] = tool results += r["issues"] diff --git a/api/chalicelib/core/sessions_mobs.py b/api/chalicelib/core/sessions_mobs.py index ea020d412..75ac59307 100644 --- a/api/chalicelib/core/sessions_mobs.py +++ b/api/chalicelib/core/sessions_mobs.py @@ -1,10 +1,14 @@ from chalicelib.utils.helper import environ -from chalicelib.utils.s3 import client +import boto3 def get_web(sessionId): - return client.generate_presigned_url( + return boto3.client('s3', + endpoint_url=environ["S3_HOST"], + aws_access_key_id=environ["S3_KEY"], + aws_secret_access_key=environ["S3_SECRET"], + region_name=environ["sessions_region"]).generate_presigned_url( 'get_object', Params={ 'Bucket': environ["sessions_bucket"], @@ -15,7 +19,7 @@ def get_web(sessionId): def get_ios(sessionId): - return client.generate_presigned_url( + return boto3.client('s3', region_name=environ["ios_region"]).generate_presigned_url( 'get_object', Params={ 'Bucket': environ["ios_bucket"], diff --git a/api/chalicelib/core/sourcemaps.py b/api/chalicelib/core/sourcemaps.py index 01204847c..c198b859b 100644 --- a/api/chalicelib/core/sourcemaps.py +++ b/api/chalicelib/core/sourcemaps.py @@ -80,12 +80,7 @@ def get_traces_group(project_id, payload): payloads = {} all_exists = True for i, u in enumerate(frames): - print("===============================") - print(u["absPath"]) - print("converted to:") key = __get_key(project_id, u["absPath"]) # use filename instead? - print(key) - print("===============================") if key not in payloads: file_exists = s3.exists(environ['sourcemaps_bucket'], key) all_exists = all_exists and file_exists diff --git a/api/chalicelib/core/sourcemaps_parser.py b/api/chalicelib/core/sourcemaps_parser.py index b7c17f3d3..cb0463d55 100644 --- a/api/chalicelib/core/sourcemaps_parser.py +++ b/api/chalicelib/core/sourcemaps_parser.py @@ -8,9 +8,14 @@ def get_original_trace(key, positions): "key": key, "positions": positions, "padding": 5, - "bucket": environ['sourcemaps_bucket'] + "bucket": environ['sourcemaps_bucket'], + "bucket_config": { + "aws_access_key_id": environ["sourcemaps_bucket_key"], + "aws_secret_access_key": environ["sourcemaps_bucket_secret"], + "aws_region": environ["sourcemaps_bucket_region"] + } } - r = requests.post(environ["sourcemaps_reader"], json=payload) + r = requests.post(environ["sourcemaps"], json=payload) if r.status_code != 200: return {} diff --git a/api/chalicelib/core/telemetry.py b/api/chalicelib/core/telemetry.py index 48f403f57..362550553 100644 --- a/api/chalicelib/core/telemetry.py +++ b/api/chalicelib/core/telemetry.py @@ -30,7 +30,7 @@ def compute(): RETURNING *,(SELECT email FROM public.users WHERE role='owner' LIMIT 1);""" ) data = cur.fetchone() - requests.post('https://parrot.asayer.io/os/telemetry', json={"stats": [process_data(data)]}) + requests.post('https://parrot.asayer.io/os/telemetry', json=process_data(data)) def new_client(): diff --git a/api/chalicelib/core/tenants.py b/api/chalicelib/core/tenants.py index 4b439cfef..f047dcffa 100644 --- a/api/chalicelib/core/tenants.py +++ b/api/chalicelib/core/tenants.py @@ -10,7 +10,7 @@ def get_by_tenant_id(tenant_id): f"""SELECT tenant_id, name, - api_key, + api_key created_at, edition, version_number, diff --git a/api/chalicelib/core/webhook.py b/api/chalicelib/core/webhook.py index fff2d4e7e..99a3b0569 100644 --- a/api/chalicelib/core/webhook.py +++ b/api/chalicelib/core/webhook.py @@ -24,7 +24,7 @@ def get(tenant_id, webhook_id): cur.execute( cur.mogrify("""\ SELECT - webhook_id AS integration_id, webhook_id AS id, w.* + w.* FROM public.webhooks AS w where w.webhook_id =%(webhook_id)s AND deleted_at ISNULL;""", {"webhook_id": webhook_id}) @@ -40,7 +40,7 @@ def get_by_type(tenant_id, webhook_type): cur.execute( cur.mogrify("""\ SELECT - w.webhook_id AS integration_id, w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at + w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at FROM public.webhooks AS w WHERE w.type =%(type)s AND deleted_at ISNULL;""", {"type": webhook_type}) @@ -55,7 +55,7 @@ def get_by_tenant(tenant_id, replace_none=False): with pg_client.PostgresClient() as cur: cur.execute("""\ SELECT - webhook_id AS integration_id, webhook_id AS id, w.* + w.* FROM public.webhooks AS w WHERE deleted_at ISNULL;""" ) @@ -81,7 +81,7 @@ def update(tenant_id, webhook_id, changes, replace_none=False): UPDATE public.webhooks SET {','.join(sub_query)} WHERE webhook_id =%(id)s AND deleted_at ISNULL - RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", + RETURNING *;""", {"id": webhook_id, **changes}) ) w = helper.dict_to_camel_case(cur.fetchone()) @@ -98,7 +98,7 @@ def add(tenant_id, endpoint, auth_header=None, webhook_type='webhook', name="", query = cur.mogrify("""\ INSERT INTO public.webhooks(endpoint,auth_header,type,name) VALUES (%(endpoint)s, %(auth_header)s, %(type)s,%(name)s) - RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", + RETURNING *;""", {"endpoint": endpoint, "auth_header": auth_header, "type": webhook_type, "name": name}) cur.execute( diff --git a/api/chalicelib/utils/jira_client.py b/api/chalicelib/utils/jira_client.py index a7ab92932..6da501bbe 100644 --- a/api/chalicelib/utils/jira_client.py +++ b/api/chalicelib/utils/jira_client.py @@ -68,8 +68,7 @@ class JiraManager: # print(issue.raw) issue_dict_list.append(self.__parser_issue_info(issue, include_comments=False)) - # return {"total": issues.total, "issues": issue_dict_list} - return issue_dict_list + return {"total": issues.total, "issues": issue_dict_list} def get_issue(self, issue_id: str): try: diff --git a/api/chalicelib/utils/pg_client.py b/api/chalicelib/utils/pg_client.py index 89a9dc8fa..8d1e37d40 100644 --- a/api/chalicelib/utils/pg_client.py +++ b/api/chalicelib/utils/pg_client.py @@ -9,25 +9,9 @@ PG_CONFIG = {"host": environ["pg_host"], "port": int(environ["pg_port"])} from psycopg2 import pool -from threading import Semaphore - - -class ORThreadedConnectionPool(psycopg2.pool.ThreadedConnectionPool): - def __init__(self, minconn, maxconn, *args, **kwargs): - self._semaphore = Semaphore(maxconn) - super().__init__(minconn, maxconn, *args, **kwargs) - - def getconn(self, *args, **kwargs): - self._semaphore.acquire() - return super().getconn(*args, **kwargs) - - def putconn(self, *args, **kwargs): - super().putconn(*args, **kwargs) - self._semaphore.release() - try: - postgreSQL_pool = ORThreadedConnectionPool(20, 100, **PG_CONFIG) + postgreSQL_pool = psycopg2.pool.ThreadedConnectionPool(6, 20, **PG_CONFIG) if (postgreSQL_pool): print("Connection pool created successfully") except (Exception, psycopg2.DatabaseError) as error: @@ -35,6 +19,13 @@ except (Exception, psycopg2.DatabaseError) as error: raise error +# finally: +# # closing database connection. +# # use closeall method to close all the active connection if you want to turn of the application +# if (postgreSQL_pool): +# postgreSQL_pool.closeall +# print("PostgreSQL connection pool is closed") + class PostgresClient: connection = None cursor = None diff --git a/api/chalicelib/utils/s3.py b/api/chalicelib/utils/s3.py index 49b6cfc85..29a8d28bc 100644 --- a/api/chalicelib/utils/s3.py +++ b/api/chalicelib/utils/s3.py @@ -2,7 +2,7 @@ from botocore.exceptions import ClientError from chalicelib.utils.helper import environ import boto3 -import botocore + from botocore.client import Config client = boto3.client('s3', endpoint_url=environ["S3_HOST"], @@ -13,20 +13,14 @@ client = boto3.client('s3', endpoint_url=environ["S3_HOST"], def exists(bucket, key): - try: - boto3.resource('s3', endpoint_url=environ["S3_HOST"], - aws_access_key_id=environ["S3_KEY"], - aws_secret_access_key=environ["S3_SECRET"], - config=Config(signature_version='s3v4'), - region_name='us-east-1') \ - .Object(bucket, key).load() - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == "404": - return False - else: - # Something else has gone wrong. - raise - return True + response = client.list_objects_v2( + Bucket=bucket, + Prefix=key, + ) + for obj in response.get('Contents', []): + if obj['Key'] == key: + return True + return False def get_presigned_url_for_sharing(bucket, expires_in, key, check_exists=False): @@ -55,9 +49,6 @@ def get_presigned_url_for_upload(bucket, expires_in, key): def get_file(source_bucket, source_key): - print("******************************") - print(f"looking for: {source_key} in {source_bucket}") - print("******************************") try: result = client.get_object( Bucket=source_bucket, diff --git a/api/entrypoint.sh b/api/entrypoint.sh deleted file mode 100755 index 3c3d12fd5..000000000 --- a/api/entrypoint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -cd sourcemaps_reader -nohup node server.js &> /tmp/sourcemaps_reader.log & -cd .. -python env_handler.py -chalice local --no-autoreload --host 0.0.0.0 --stage ${ENTERPRISE_BUILD} diff --git a/api/requirements.txt b/api/requirements.txt index 671aa5da5..094d32758 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -5,6 +5,9 @@ pyjwt==1.7.1 psycopg2-binary==2.8.6 pytz==2020.1 sentry-sdk==0.19.1 +rollbar==0.15.1 +bugsnag==4.0.1 +kubernetes==12.0.0 elasticsearch==7.9.1 jira==2.0.0 schedule==1.1.0 diff --git a/api/sourcemaps_reader/handler.js b/api/sourcemaps_reader/handler.js deleted file mode 100644 index 117808cae..000000000 --- a/api/sourcemaps_reader/handler.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; -const sourceMap = require('source-map'); -const AWS = require('aws-sdk'); -const sourceMapVersion = require('./package.json').dependencies["source-map"]; -const URL = require('url'); -const getVersion = version => version.replace(/[\^\$\=\~]/, ""); - -module.exports.sourcemapReader = async event => { - sourceMap.SourceMapConsumer.initialize({ - "lib/mappings.wasm": `https://unpkg.com/source-map@${getVersion(sourceMapVersion)}/lib/mappings.wasm` - }); - let s3; - if (process.env.S3_HOST) { - s3 = new AWS.S3({ - endpoint: process.env.S3_HOST, - accessKeyId: process.env.S3_KEY, - secretAccessKey: process.env.S3_SECRET, - s3ForcePathStyle: true, // needed with minio? - signatureVersion: 'v4' - }); - } else { - s3 = new AWS.S3({ - 'AccessKeyID': process.env.aws_access_key_id, - 'SecretAccessKey': process.env.aws_secret_access_key, - 'Region': process.env.aws_region - }); - } - - var options = { - Bucket: event.bucket, - Key: event.key - }; - return new Promise(function (resolve, reject) { - s3.getObject(options, (err, data) => { - if (err) { - console.log("Get S3 object failed"); - console.log(err); - return reject(err); - } - const sourcemap = data.Body.toString(); - - return new sourceMap.SourceMapConsumer(sourcemap) - .then(consumer => { - let results = []; - for (let i = 0; i < event.positions.length; i++) { - let original = consumer.originalPositionFor({ - line: event.positions[i].line, - column: event.positions[i].column - }); - let url = URL.parse(""); - let preview = []; - if (original.source) { - preview = consumer.sourceContentFor(original.source, true); - if (preview !== null) { - preview = preview.split("\n") - .map((line, i) => [i + 1, line]); - if (event.padding) { - let start = original.line < event.padding ? 0 : original.line - event.padding; - preview = preview.slice(start, original.line + event.padding); - } - } else { - console.log("source not found, null preview for:"); - console.log(original.source); - preview = [] - } - url = URL.parse(original.source); - } else { - console.log("couldn't find original position of:"); - console.log({ - line: event.positions[i].line, - column: event.positions[i].column - }); - } - let result = { - "absPath": url.href, - "filename": url.pathname, - "lineNo": original.line, - "colNo": original.column, - "function": original.name, - "context": preview - }; - // console.log(result); - results.push(result); - } - - // Use this code if you don't use the http event with the LAMBDA-PROXY integration - return resolve(results); - }); - }); - }); -}; - - -// let v = { -// 'key': '1725/99f96f044fa7e941dbb15d7d68b20549', -// 'positions': [{'line': 1, 'column': 943}], -// 'padding': 5, -// 'bucket': 'asayer-sourcemaps' -// }; -// let v = { -// 'key': '1/65d8d3866bb8c92f3db612cb330f270c', -// 'positions': [{'line': 1, 'column': 0}], -// 'padding': 5, -// 'bucket': 'asayer-sourcemaps-staging' -// }; -// module.exports.sourcemapReader(v).then((r) => { -// // console.log(r); -// const fs = require('fs'); -// let data = JSON.stringify(r); -// fs.writeFileSync('results.json', data); -// }); \ No newline at end of file diff --git a/api/sourcemaps_reader/server.js b/api/sourcemaps_reader/server.js deleted file mode 100644 index 2a1c4dcf6..000000000 --- a/api/sourcemaps_reader/server.js +++ /dev/null @@ -1,38 +0,0 @@ -const http = require('http'); -const handler = require('./handler'); -const hostname = '127.0.0.1'; -const port = 3000; - -const server = http.createServer((req, res) => { - if (req.method === 'POST') { - let data = ''; - req.on('data', chunk => { - data += chunk; - }); - req.on('end', function () { - data = JSON.parse(data); - console.log("Starting parser for: " + data.key); - // process.env = {...process.env, ...data.bucket_config}; - handler.sourcemapReader(data) - .then((results) => { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(results)); - }) - .catch((e) => { - console.error("Something went wrong"); - console.error(e); - res.statusCode(500); - res.end(e); - }); - }) - } else { - res.statusCode = 405; - res.setHeader('Content-Type', 'text/plain'); - res.end('Method Not Allowed'); - } -}); - -server.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); -}); \ No newline at end of file diff --git a/backend/pkg/db/postgres/messages_web.go b/backend/pkg/db/postgres/messages_web.go index 25e044e68..9156ab78e 100644 --- a/backend/pkg/db/postgres/messages_web.go +++ b/backend/pkg/db/postgres/messages_web.go @@ -92,8 +92,8 @@ func (conn *Conn) InsertWebPageEvent(sessionID uint64, e *PageEvent) error { if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, "LOCATION", url.DiscardURLQuery(path)) - conn.insertAutocompleteValue(sessionID, "REFERRER", url.DiscardURLQuery(e.Referrer)) + conn.insertAutocompleteValue(sessionID, url.DiscardURLQuery(path), "LOCATION") + conn.insertAutocompleteValue(sessionID, url.DiscardURLQuery(e.Referrer), "REFERRER") return nil } @@ -123,7 +123,7 @@ func (conn *Conn) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error { if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, "CLICK", e.Label) + conn.insertAutocompleteValue(sessionID, e.Label, "CLICK") return nil } @@ -158,7 +158,7 @@ func (conn *Conn) InsertWebInputEvent(sessionID uint64, e *InputEvent) error { if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, "INPUT", e.Label) + conn.insertAutocompleteValue(sessionID, e.Label, "INPUT") return nil } diff --git a/backend/services/db/messages.go b/backend/services/db/messages.go index 511165c5f..6aa4ac076 100644 --- a/backend/services/db/messages.go +++ b/backend/services/db/messages.go @@ -16,7 +16,6 @@ func insertMessage(sessionID uint64, msg Message) error { // Web case *SessionStart: - log.Printf("Session Start: %v", sessionID) return pg.InsertWebSessionStart(sessionID, m) case *SessionEnd: return pg.InsertWebSessionEnd(sessionID, m) diff --git a/backend/services/ender/builder/builder.go b/backend/services/ender/builder/builder.go index 246b2f7e0..cccf96bcf 100644 --- a/backend/services/ender/builder/builder.go +++ b/backend/services/ender/builder/builder.go @@ -82,9 +82,6 @@ func (b *builder) iterateReadyMessage(iter func(msg Message)) { } func (b *builder) buildSessionEnd() { - if b.timestamp == 0 { - return - } sessionEnd := &SessionEnd{ Timestamp: b.timestamp, // + delay? } @@ -109,25 +106,16 @@ func (b *builder) buildInputEvent() { func (b *builder) handleMessage(message Message, messageID uint64) { timestamp := uint64(message.Meta().Timestamp) - if b.timestamp <= timestamp { // unnecessary. TODO: test and remove + if b.timestamp <= timestamp { b.timestamp = timestamp } - // Before the first timestamp. + // Start from the first timestamp. switch msg := message.(type) { case *SessionStart, *Metadata, *UserID, *UserAnonymousID: b.appendReadyMessage(msg) - case *RawErrorEvent: - b.appendReadyMessage(&ErrorEvent{ - MessageID: messageID, - Timestamp: msg.Timestamp, - Source: msg.Source, - Name: msg.Name, - Message: msg.Message, - Payload: msg.Payload, - }) } if b.timestamp == 0 { return @@ -189,6 +177,15 @@ func (b *builder) handleMessage(message Message, messageID uint64) { Timestamp: b.timestamp, }) } + case *RawErrorEvent: + b.appendReadyMessage(&ErrorEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Source: msg.Source, + Name: msg.Name, + Message: msg.Message, + Payload: msg.Payload, + }) case *JSException: b.appendReadyMessage(&ErrorEvent{ MessageID: messageID, diff --git a/backend/services/ender/builder/inputEventBuilder.go b/backend/services/ender/builder/inputEventBuilder.go index 98c7ebaf6..4938e47a9 100644 --- a/backend/services/ender/builder/inputEventBuilder.go +++ b/backend/services/ender/builder/inputEventBuilder.go @@ -69,10 +69,10 @@ func (b *inputEventBuilder) Build() *InputEvent { return nil } inputEvent := b.inputEvent - label, exists := b.inputLabels[b.inputID] - if !exists { - return nil - } + label := b.inputLabels[b.inputID] + // if !ok { + // return nil + // } inputEvent.Label = label b.inputEvent = nil diff --git a/backend/services/integrations/integration/sentry.go b/backend/services/integrations/integration/sentry.go index 0330430c3..39443f51a 100644 --- a/backend/services/integrations/integration/sentry.go +++ b/backend/services/integrations/integration/sentry.go @@ -111,7 +111,7 @@ PageLoop: c.errChan <- err continue } - if token == "" && sessionID == 0 { // We can't felter them on request + if sessionID == 0 { // We can't felter them on request continue } diff --git a/backend/services/integrations/main.go b/backend/services/integrations/main.go index e1ea58ebd..68b4ec5aa 100644 --- a/backend/services/integrations/main.go +++ b/backend/services/integrations/main.go @@ -19,7 +19,7 @@ import ( func main() { log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) - TOPIC_RAW := env.String("TOPIC_RAW") + TOPIC_TRIGGER := env.String("TOPIC_TRIGGER") POSTGRES_STRING := env.String("POSTGRES_STRING") pg := postgres.NewConn(POSTGRES_STRING) @@ -43,7 +43,6 @@ func main() { }) producer:= queue.NewProducer() - defer producer.Close(15000) listener, err := postgres.NewIntegrationsListener(POSTGRES_STRING) if err != nil { @@ -73,14 +72,13 @@ func main() { sessionID := event.SessionID if sessionID == 0 { sessData, err := tokenizer.Parse(event.Token) - if err != nil && err != token.EXPIRED { + if err != nil { log.Printf("Error on token parsing: %v; Token: %v", err, event.Token) continue } sessionID = sessData.ID } - // TODO: send to ready-events topic. Otherwise it have to go through the events worker. - producer.Produce(TOPIC_RAW, sessionID, messages.Encode(event.RawErrorEvent)) + producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(event.RawErrorEvent)) case err := <-manager.Errors: log.Printf("Integration error: %v\n", err) case i := <-manager.RequestDataUpdates: @@ -88,10 +86,10 @@ func main() { if err := pg.UpdateIntegrationRequestData(&i); err != nil { log.Printf("Postgres Update request_data error: %v\n", err) } - case err := <-listener.Errors: - log.Printf("Postgres listen error: %v\n", err) + //case err := <-listener.Errors: + //log.Printf("Postgres listen error: %v\n", err) case iPointer := <-listener.Integrations: - log.Printf("Integration update: %v\n", *iPointer) + // log.Printf("Integration update: %v\n", *iPointer) err := manager.Update(iPointer) if err != nil { log.Printf("Integration parse error: %v | Integration: %v\n", err, *iPointer) diff --git a/ee/api/.chalice/config.json b/ee/api/.chalice/config.json index 5cda73bd3..605e5b7c1 100644 --- a/ee/api/.chalice/config.json +++ b/ee/api/.chalice/config.json @@ -31,12 +31,14 @@ "assign_link": "http://127.0.0.1:8000/async/email_assignment", "captcha_server": "", "captcha_key": "", - "sessions_bucket": "mobs", + "sessions_bucket": "asayer-mobs", "sessions_region": "us-east-1", "put_S3_TTL": "20", - "sourcemaps_reader": "http://127.0.0.1:3000/", - "sourcemaps_bucket": "sourcemaps", - "js_cache_bucket": "sessions-assets", + "sourcemaps_bucket": "asayer-sourcemaps", + "sourcemaps_bucket_key": "", + "sourcemaps_bucket_secret": "", + "sourcemaps_bucket_region": "us-east-1", + "js_cache_bucket": "asayer-sessions-assets", "async_Token": "", "EMAIL_HOST": "", "EMAIL_PORT": "587", diff --git a/ee/api/.gitignore b/ee/api/.gitignore index 7e2873ee0..812abce9c 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -170,8 +170,8 @@ logs*.txt *.csv *.p +*.js SUBNETS.json chalicelib/.config -chalicelib/saas -README/* \ No newline at end of file +chalicelib/saas \ No newline at end of file diff --git a/ee/api/app.py b/ee/api/app.py index d604992a1..da75c1ac5 100644 --- a/ee/api/app.py +++ b/ee/api/app.py @@ -25,13 +25,13 @@ import traceback old_tb = traceback.print_exception old_f = sys.stdout old_e = sys.stderr -OR_SESSION_TOKEN = None +ASAYER_SESSION_ID = None 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}") + if ASAYER_SESSION_ID is not None and x != '\n' and not helper.is_local(): + old_f.write(f"[asayer_session_id={ASAYER_SESSION_ID}] {x}") else: old_f.write(x) @@ -40,8 +40,9 @@ class F: 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)) + if ASAYER_SESSION_ID is not None and not helper.is_local(): + # bugsnag.notify(Exception(str(value)), meta_data={"special_info": {"asayerSessionId": ASAYER_SESSION_ID}}) + value = type(value)(f"[asayer_session_id={ASAYER_SESSION_ID}] " + str(value)) old_tb(etype, value, tb, limit, file, chain) @@ -58,7 +59,7 @@ _overrides.chalice_app(app) @app.middleware('http') -def or_middleware(event, get_response): +def asayer_middleware(event, get_response): from chalicelib.ee import unlock if not unlock.is_valid(): return Response(body={"errors": ["expired license"]}, status_code=403) @@ -67,11 +68,12 @@ def or_middleware(event, get_response): if not projects.is_authorized(project_id=event.uri_params["projectId"], tenant_id=event.context["authorizer"]["tenantId"]): print("unauthorized project") + # return {"errors": ["unauthorized project"]} pg_client.close() return Response(body={"errors": ["unauthorized project"]}, status_code=401) - 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')) + global ASAYER_SESSION_ID + ASAYER_SESSION_ID = 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() @@ -82,24 +84,19 @@ def or_middleware(event, get_response): import time now = int(time.time() * 1000) response = get_response(event) - if response.status_code == 500 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(): + print("middleware exception handling") + print(e) + pg_client.close() + if helper.allow_sentry() and ASAYER_SESSION_ID 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_tag('openReplaySessionToken', ASAYER_SESSION_ID) 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) + raise e pg_client.close() return response diff --git a/ee/api/chalicelib/blueprints/bp_core.py b/ee/api/chalicelib/blueprints/bp_core.py index bd42b2254..3b2910606 100644 --- a/ee/api/chalicelib/blueprints/bp_core.py +++ b/ee/api/chalicelib/blueprints/bp_core.py @@ -881,5 +881,5 @@ def all_issue_types(context): @app.route('/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) @app.route('/{projectId}/flows', methods=['GET', 'PUT', 'POST', 'DELETE']) -def removed_endpoints(projectId=None, context=None): +def removed_endpoints(context): return Response(body={"errors": ["Endpoint no longer available"]}, status_code=410) diff --git a/ee/api/chalicelib/blueprints/bp_core_dynamic.py b/ee/api/chalicelib/blueprints/bp_core_dynamic.py index 6e45627df..505f10cb9 100644 --- a/ee/api/chalicelib/blueprints/bp_core_dynamic.py +++ b/ee/api/chalicelib/blueprints/bp_core_dynamic.py @@ -73,12 +73,10 @@ def get_account(context): "projects": -1, "metadata": metadata.get_remaining_metadata_with_count(context['tenantId']) }, - **license.get_status(context["tenantId"]), - "smtp": environ["EMAIL_HOST"] is not None and len(environ["EMAIL_HOST"]) > 0 + **license.get_status(context["tenantId"]) } } - @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, @@ -159,27 +157,12 @@ 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: + if Slack.add_integration(tenant_id=context["tenantId"], url=data["url"], name=data["name"]): + return {"data": {"status": "success"}} + else: return { - "errors": ["We couldn't send you a test message on your Slack channel. Please verify your webhook url."] + "errors": ["failed URL verification, if you received a message on slack, please notify our dev-team"] } - 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']) @@ -408,7 +391,6 @@ def search_sessions_by_metadata(context): m_key=key, project_id=project_id)} - @app.route('/plans', methods=['GET']) def get_current_plan(context): return { diff --git a/ee/api/chalicelib/core/collaboration_slack.py b/ee/api/chalicelib/core/collaboration_slack.py index b3da03a37..5fc80511c 100644 --- a/ee/api/chalicelib/core/collaboration_slack.py +++ b/ee/api/chalicelib/core/collaboration_slack.py @@ -6,18 +6,19 @@ from chalicelib.core import webhook class Slack: @classmethod - def add_channel(cls, tenant_id, **args): + def add_integration(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 + if cls.__say_hello(url): + webhook.add(tenant_id=tenant_id, + endpoint=url, + webhook_type="slack", + name=name) + return True + return False @classmethod - def say_hello(cls, url): + def __say_hello(cls, url): r = requests.post( url=url, json={ diff --git a/ee/api/chalicelib/core/events.py b/ee/api/chalicelib/core/events.py index 69213a079..65ade49ed 100644 --- a/ee/api/chalicelib/core/events.py +++ b/ee/api/chalicelib/core/events.py @@ -365,7 +365,7 @@ def __get_merged_queries(queries, value, project_id): def __get_autocomplete_table(value, project_id): with pg_client.PostgresClient() as cur: cur.execute(cur.mogrify("""SELECT DISTINCT ON(value,type) project_id, value, type - FROM (SELECT project_id, type, value + FROM (SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY type ORDER BY value) AS Row_ID FROM public.autocomplete diff --git a/ee/api/chalicelib/core/integration_jira_cloud_issue.py b/ee/api/chalicelib/core/integration_jira_cloud_issue.py index bb847007a..00fac2fcb 100644 --- a/ee/api/chalicelib/core/integration_jira_cloud_issue.py +++ b/ee/api/chalicelib/core/integration_jira_cloud_issue.py @@ -34,7 +34,7 @@ class JIRACloudIntegrationIssue(BaseIntegrationIssue): if len(projects_map[integration_project_id]) > 0: jql += f" AND ID IN ({','.join(projects_map[integration_project_id])})" issues = self._client.get_issues(jql, offset=0) - results += issues + results += [issues] return {"issues": results} def get(self, integration_project_id, assignment_id): diff --git a/ee/api/chalicelib/core/sessions.py b/ee/api/chalicelib/core/sessions.py index 56ba7c463..9d9ff204a 100644 --- a/ee/api/chalicelib/core/sessions.py +++ b/ee/api/chalicelib/core/sessions.py @@ -1,6 +1,6 @@ from chalicelib.utils import pg_client, helper from chalicelib.utils import dev -from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, sessions_mobs, issues +from chalicelib.core import events, sessions_metas, socket_ios, metadata, events_ios, sessions_mobs from chalicelib.ee import projects, errors @@ -24,7 +24,7 @@ SESSION_PROJECTION_COLS = """s.project_id, s.user_anonymous_id, s.platform, s.issue_score, - to_jsonb(s.issue_types) AS issue_types, + s.issue_types::text[] AS issue_types, favorite_sessions.session_id NOTNULL AS favorite, COALESCE((SELECT TRUE FROM public.user_viewed_sessions AS fs @@ -83,6 +83,7 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events_ios.get_customs_by_sessionId(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_ios(sessionId=session_id) + data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data["socket"] = socket_ios.start_replay(project_id=project_id, session_id=session_id, device=data["userDevice"], os_version=data["userOsVersion"], @@ -99,11 +100,9 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_ data['userEvents'] = events.get_customs_by_sessionId2_pg(project_id=project_id, session_id=session_id) data['mobsUrl'] = sessions_mobs.get_web(sessionId=session_id) + data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) data['resources'] = resources.get_by_session_id(session_id=session_id) - data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data) - data['issues'] = issues.get_by_session_id(session_id=session_id) - return data return None diff --git a/ee/api/chalicelib/core/sessions_assignments.py b/ee/api/chalicelib/core/sessions_assignments.py index 3e0929dad..2b9c28d8f 100644 --- a/ee/api/chalicelib/core/sessions_assignments.py +++ b/ee/api/chalicelib/core/sessions_assignments.py @@ -119,6 +119,7 @@ def get_by_session(tenant_id, user_id, project_id, session_id): continue r = integration.issue_handler.get_by_ids(saved_issues=issues[tool]) + print(r) for i in r["issues"]: i["provider"] = tool results += r["issues"] diff --git a/ee/api/chalicelib/core/sessions_mobs.py b/ee/api/chalicelib/core/sessions_mobs.py index 80fe59b28..b96662c67 100644 --- a/ee/api/chalicelib/core/sessions_mobs.py +++ b/ee/api/chalicelib/core/sessions_mobs.py @@ -1,11 +1,11 @@ from chalicelib.utils import helper from chalicelib.utils.helper import environ -from chalicelib.utils.s3 import client +import boto3 def get_web(sessionId): - return client.generate_presigned_url( + return boto3.client('s3', region_name=environ["sessions_region"]).generate_presigned_url( 'get_object', Params={ 'Bucket': environ["sessions_bucket"], @@ -16,7 +16,7 @@ def get_web(sessionId): def get_ios(sessionId): - return client.generate_presigned_url( + return boto3.client('s3', region_name=environ["ios_region"]).generate_presigned_url( 'get_object', Params={ 'Bucket': environ["ios_bucket"], diff --git a/ee/api/chalicelib/core/sourcemaps.py b/ee/api/chalicelib/core/sourcemaps.py index dbd7213ea..5f82a31e2 100644 --- a/ee/api/chalicelib/core/sourcemaps.py +++ b/ee/api/chalicelib/core/sourcemaps.py @@ -79,12 +79,7 @@ def get_traces_group(project_id, payload): payloads = {} all_exists = True for i, u in enumerate(frames): - print("===============================") - print(u["absPath"]) - print("converted to:") key = __get_key(project_id, u["absPath"]) # use filename instead? - print(key) - print("===============================") if key not in payloads: file_exists = s3.exists(environ['sourcemaps_bucket'], key) all_exists = all_exists and file_exists diff --git a/ee/api/chalicelib/core/sourcemaps_parser.py b/ee/api/chalicelib/core/sourcemaps_parser.py index b7c17f3d3..cb0463d55 100644 --- a/ee/api/chalicelib/core/sourcemaps_parser.py +++ b/ee/api/chalicelib/core/sourcemaps_parser.py @@ -8,9 +8,14 @@ def get_original_trace(key, positions): "key": key, "positions": positions, "padding": 5, - "bucket": environ['sourcemaps_bucket'] + "bucket": environ['sourcemaps_bucket'], + "bucket_config": { + "aws_access_key_id": environ["sourcemaps_bucket_key"], + "aws_secret_access_key": environ["sourcemaps_bucket_secret"], + "aws_region": environ["sourcemaps_bucket_region"] + } } - r = requests.post(environ["sourcemaps_reader"], json=payload) + r = requests.post(environ["sourcemaps"], json=payload) if r.status_code != 200: return {} diff --git a/ee/api/chalicelib/ee/webhook.py b/ee/api/chalicelib/ee/webhook.py index 20e873f5c..0a2406ab9 100644 --- a/ee/api/chalicelib/ee/webhook.py +++ b/ee/api/chalicelib/ee/webhook.py @@ -8,7 +8,7 @@ def get_by_id(webhook_id): cur.execute( cur.mogrify("""\ SELECT - webhook_id AS integration_id, webhook_id AS id, w.* + w.* FROM public.webhooks AS w where w.webhook_id =%(webhook_id)s AND deleted_at ISNULL;""", {"webhook_id": webhook_id}) @@ -24,7 +24,7 @@ def get(tenant_id, webhook_id): cur.execute( cur.mogrify("""\ SELECT - webhook_id AS integration_id, webhook_id AS id, w.* + w.* FROM public.webhooks AS w where w.webhook_id =%(webhook_id)s AND w.tenant_id =%(tenant_id)s AND deleted_at ISNULL;""", {"webhook_id": webhook_id, "tenant_id": tenant_id}) @@ -40,7 +40,7 @@ def get_by_type(tenant_id, webhook_type): cur.execute( cur.mogrify("""\ SELECT - w.webhook_id AS integration_id, w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at + w.webhook_id AS id,w.webhook_id,w.endpoint,w.auth_header,w.type,w.index,w.name,w.created_at FROM public.webhooks AS w where w.tenant_id =%(tenant_id)s @@ -59,7 +59,7 @@ def get_by_tenant(tenant_id, replace_none=False): cur.execute( cur.mogrify("""\ SELECT - webhook_id AS integration_id, webhook_id AS id,w.* + w.* FROM public.webhooks AS w where w.tenant_id =%(tenant_id)s @@ -88,7 +88,7 @@ def update(tenant_id, webhook_id, changes, replace_none=False): UPDATE public.webhooks SET {','.join(sub_query)} WHERE tenant_id =%(tenant_id)s AND webhook_id =%(id)s AND deleted_at ISNULL - RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", + RETURNING *;""", {"tenant_id": tenant_id, "id": webhook_id, **changes}) ) w = helper.dict_to_camel_case(cur.fetchone()) @@ -105,7 +105,7 @@ def add(tenant_id, endpoint, auth_header=None, webhook_type='webhook', name="", query = cur.mogrify("""\ INSERT INTO public.webhooks(tenant_id, endpoint,auth_header,type,name) VALUES (%(tenant_id)s, %(endpoint)s, %(auth_header)s, %(type)s,%(name)s) - RETURNING webhook_id AS integration_id, webhook_id AS id,*;""", + RETURNING *;""", {"tenant_id": tenant_id, "endpoint": endpoint, "auth_header": auth_header, "type": webhook_type, "name": name}) cur.execute( diff --git a/ee/api/chalicelib/utils/jira_client.py b/ee/api/chalicelib/utils/jira_client.py index a7ab92932..6da501bbe 100644 --- a/ee/api/chalicelib/utils/jira_client.py +++ b/ee/api/chalicelib/utils/jira_client.py @@ -68,8 +68,7 @@ class JiraManager: # print(issue.raw) issue_dict_list.append(self.__parser_issue_info(issue, include_comments=False)) - # return {"total": issues.total, "issues": issue_dict_list} - return issue_dict_list + return {"total": issues.total, "issues": issue_dict_list} def get_issue(self, issue_id: str): try: diff --git a/ee/api/chalicelib/utils/pg_client.py b/ee/api/chalicelib/utils/pg_client.py index 4df29be39..e95527d64 100644 --- a/ee/api/chalicelib/utils/pg_client.py +++ b/ee/api/chalicelib/utils/pg_client.py @@ -9,26 +9,11 @@ PG_CONFIG = {"host": environ["pg_host"], "port": int(environ["pg_port"])} # connexion pool for FOS & EE + from psycopg2 import pool -from threading import Semaphore - - -class ORThreadedConnectionPool(psycopg2.pool.ThreadedConnectionPool): - def __init__(self, minconn, maxconn, *args, **kwargs): - self._semaphore = Semaphore(maxconn) - super().__init__(minconn, maxconn, *args, **kwargs) - - def getconn(self, *args, **kwargs): - self._semaphore.acquire() - return super().getconn(*args, **kwargs) - - def putconn(self, *args, **kwargs): - super().putconn(*args, **kwargs) - self._semaphore.release() - try: - postgreSQL_pool = ORThreadedConnectionPool(20, 100, **PG_CONFIG) + postgreSQL_pool = psycopg2.pool.ThreadedConnectionPool(6, 20, **PG_CONFIG) if (postgreSQL_pool): print("Connection pool created successfully") except (Exception, psycopg2.DatabaseError) as error: @@ -36,6 +21,13 @@ except (Exception, psycopg2.DatabaseError) as error: raise error +# finally: +# # closing database connection. +# # use closeall method to close all the active connection if you want to turn of the application +# if (postgreSQL_pool): +# postgreSQL_pool.closeall +# print("PostgreSQL connection pool is closed") + class PostgresClient: connection = None cursor = None diff --git a/ee/api/chalicelib/utils/s3.py b/ee/api/chalicelib/utils/s3.py index c9516982f..29a8d28bc 100644 --- a/ee/api/chalicelib/utils/s3.py +++ b/ee/api/chalicelib/utils/s3.py @@ -3,7 +3,6 @@ from chalicelib.utils.helper import environ import boto3 -import botocore from botocore.client import Config client = boto3.client('s3', endpoint_url=environ["S3_HOST"], @@ -14,17 +13,51 @@ client = boto3.client('s3', endpoint_url=environ["S3_HOST"], def exists(bucket, key): + response = client.list_objects_v2( + Bucket=bucket, + Prefix=key, + ) + for obj in response.get('Contents', []): + if obj['Key'] == key: + return True + return False + + +def get_presigned_url_for_sharing(bucket, expires_in, key, check_exists=False): + if check_exists and not exists(bucket, key): + return None + + return client.generate_presigned_url( + 'get_object', + Params={ + 'Bucket': bucket, + 'Key': key + }, + ExpiresIn=expires_in + ) + + +def get_presigned_url_for_upload(bucket, expires_in, key): + return client.generate_presigned_url( + 'put_object', + Params={ + 'Bucket': bucket, + 'Key': key + }, + ExpiresIn=expires_in + ) + + +def get_file(source_bucket, source_key): try: - boto3.resource('s3', endpoint_url=environ["S3_HOST"], - aws_access_key_id=environ["S3_KEY"], - aws_secret_access_key=environ["S3_SECRET"], - config=Config(signature_version='s3v4'), - region_name='us-east-1') \ - .Object(bucket, key).load() - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == "404": - return False + result = client.get_object( + Bucket=source_bucket, + Key=source_key + ) + except ClientError as ex: + if ex.response['Error']['Code'] == 'NoSuchKey': + print(f'======> No object found - returning None for {source_bucket}/{source_key}') + return None else: - # Something else has gone wrong. - raise - return True + raise ex + return result["Body"].read().decode() diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 4fa698105..3944c0923 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -5,6 +5,9 @@ pyjwt==1.7.1 psycopg2-binary==2.8.6 pytz==2020.1 sentry-sdk==0.19.1 +rollbar==0.15.1 +bugsnag==4.0.1 +kubernetes==12.0.0 elasticsearch==7.9.1 jira==2.0.0 schedule==1.1.0 diff --git a/ee/api/sourcemaps_reader/handler.js b/ee/api/sourcemaps_reader/handler.js deleted file mode 100644 index 117808cae..000000000 --- a/ee/api/sourcemaps_reader/handler.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; -const sourceMap = require('source-map'); -const AWS = require('aws-sdk'); -const sourceMapVersion = require('./package.json').dependencies["source-map"]; -const URL = require('url'); -const getVersion = version => version.replace(/[\^\$\=\~]/, ""); - -module.exports.sourcemapReader = async event => { - sourceMap.SourceMapConsumer.initialize({ - "lib/mappings.wasm": `https://unpkg.com/source-map@${getVersion(sourceMapVersion)}/lib/mappings.wasm` - }); - let s3; - if (process.env.S3_HOST) { - s3 = new AWS.S3({ - endpoint: process.env.S3_HOST, - accessKeyId: process.env.S3_KEY, - secretAccessKey: process.env.S3_SECRET, - s3ForcePathStyle: true, // needed with minio? - signatureVersion: 'v4' - }); - } else { - s3 = new AWS.S3({ - 'AccessKeyID': process.env.aws_access_key_id, - 'SecretAccessKey': process.env.aws_secret_access_key, - 'Region': process.env.aws_region - }); - } - - var options = { - Bucket: event.bucket, - Key: event.key - }; - return new Promise(function (resolve, reject) { - s3.getObject(options, (err, data) => { - if (err) { - console.log("Get S3 object failed"); - console.log(err); - return reject(err); - } - const sourcemap = data.Body.toString(); - - return new sourceMap.SourceMapConsumer(sourcemap) - .then(consumer => { - let results = []; - for (let i = 0; i < event.positions.length; i++) { - let original = consumer.originalPositionFor({ - line: event.positions[i].line, - column: event.positions[i].column - }); - let url = URL.parse(""); - let preview = []; - if (original.source) { - preview = consumer.sourceContentFor(original.source, true); - if (preview !== null) { - preview = preview.split("\n") - .map((line, i) => [i + 1, line]); - if (event.padding) { - let start = original.line < event.padding ? 0 : original.line - event.padding; - preview = preview.slice(start, original.line + event.padding); - } - } else { - console.log("source not found, null preview for:"); - console.log(original.source); - preview = [] - } - url = URL.parse(original.source); - } else { - console.log("couldn't find original position of:"); - console.log({ - line: event.positions[i].line, - column: event.positions[i].column - }); - } - let result = { - "absPath": url.href, - "filename": url.pathname, - "lineNo": original.line, - "colNo": original.column, - "function": original.name, - "context": preview - }; - // console.log(result); - results.push(result); - } - - // Use this code if you don't use the http event with the LAMBDA-PROXY integration - return resolve(results); - }); - }); - }); -}; - - -// let v = { -// 'key': '1725/99f96f044fa7e941dbb15d7d68b20549', -// 'positions': [{'line': 1, 'column': 943}], -// 'padding': 5, -// 'bucket': 'asayer-sourcemaps' -// }; -// let v = { -// 'key': '1/65d8d3866bb8c92f3db612cb330f270c', -// 'positions': [{'line': 1, 'column': 0}], -// 'padding': 5, -// 'bucket': 'asayer-sourcemaps-staging' -// }; -// module.exports.sourcemapReader(v).then((r) => { -// // console.log(r); -// const fs = require('fs'); -// let data = JSON.stringify(r); -// fs.writeFileSync('results.json', data); -// }); \ No newline at end of file diff --git a/ee/api/sourcemaps_reader/server.js b/ee/api/sourcemaps_reader/server.js deleted file mode 100644 index 2a1c4dcf6..000000000 --- a/ee/api/sourcemaps_reader/server.js +++ /dev/null @@ -1,38 +0,0 @@ -const http = require('http'); -const handler = require('./handler'); -const hostname = '127.0.0.1'; -const port = 3000; - -const server = http.createServer((req, res) => { - if (req.method === 'POST') { - let data = ''; - req.on('data', chunk => { - data += chunk; - }); - req.on('end', function () { - data = JSON.parse(data); - console.log("Starting parser for: " + data.key); - // process.env = {...process.env, ...data.bucket_config}; - handler.sourcemapReader(data) - .then((results) => { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(results)); - }) - .catch((e) => { - console.error("Something went wrong"); - console.error(e); - res.statusCode(500); - res.end(e); - }); - }) - } else { - res.statusCode = 405; - res.setHeader('Content-Type', 'text/plain'); - res.end('Method Not Allowed'); - } -}); - -server.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); -}); \ No newline at end of file diff --git a/ee/connectors/bigquery_utils/create_table.py b/ee/connectors/bigquery_utils/create_table.py deleted file mode 100644 index 4b166e4ae..000000000 --- a/ee/connectors/bigquery_utils/create_table.py +++ /dev/null @@ -1,357 +0,0 @@ -import os -from google.cloud import bigquery - -from db.loaders.bigquery_loader import creds_file - - -def create_tables_bigquery(): - create_sessions_table(creds_file=creds_file, - table_id=f"{os.environ['project_id']}.{os.environ['dataset']}.{os.environ['sessions_table']}") - print(f"`{os.environ['sessions_table']}` table created succesfully.") - create_events_table(creds_file=creds_file, - table_id=f"{os.environ['project_id']}.{os.environ['dataset']}.{os.environ['events_table_name']}") - print(f"`{os.environ['events_table_name']}` table created succesfully.") - - -def create_table(creds_file, table_id, schema): - client = bigquery.Client.from_service_account_json(creds_file) - table = bigquery.Table(table_id, schema=schema) - table = client.create_table(table) # Make an API request. - print( - "Created table {}.{}.{}".format(table.project, table.dataset_id, table.table_id) - ) - - -def create_sessions_table(creds_file, table_id): - schema = [ - bigquery.SchemaField("sessionid", "INT64", mode="REQUIRED"), - bigquery.SchemaField("user_agent", "STRING"), - bigquery.SchemaField("user_browser", "STRING"), - bigquery.SchemaField("user_browser_version", "STRING"), - bigquery.SchemaField("user_country", "STRING"), - bigquery.SchemaField("user_device", "STRING"), - bigquery.SchemaField("user_device_heap_size", "INT64"), - bigquery.SchemaField("user_device_memory_size", "INT64"), - - bigquery.SchemaField("user_device_type", "STRING"), - bigquery.SchemaField("user_os", "STRING"), - bigquery.SchemaField("user_os_version", "STRING"), - bigquery.SchemaField("user_uuid", "STRING"), - bigquery.SchemaField("connection_effective_bandwidth", "INT64"), - - bigquery.SchemaField("connection_type", "STRING"), - bigquery.SchemaField("metadata_key", "STRING"), - bigquery.SchemaField("metadata_value", "STRING"), - bigquery.SchemaField("referrer", "STRING"), - bigquery.SchemaField("user_anonymous_id", "STRING"), - bigquery.SchemaField("user_id", "STRING"), - bigquery.SchemaField("session_start_timestamp", "INT64"), - bigquery.SchemaField("session_end_timestamp", "INT64"), - bigquery.SchemaField("session_duration", "INT64"), - - bigquery.SchemaField("first_contentful_paint", "INT64"), - bigquery.SchemaField("speed_index", "INT64"), - bigquery.SchemaField("visually_complete", "INT64"), - bigquery.SchemaField("timing_time_to_interactive", "INT64"), - - bigquery.SchemaField("avg_cpu", "INT64"), - bigquery.SchemaField("avg_fps", "INT64"), - bigquery.SchemaField("max_cpu", "INT64"), - bigquery.SchemaField("max_fps", "INT64"), - bigquery.SchemaField("max_total_js_heap_size", "INT64"), - bigquery.SchemaField("max_used_js_heap_size", "INT64"), - - bigquery.SchemaField("js_exceptions_count", "INT64"), - bigquery.SchemaField("long_tasks_total_duration", "INT64"), - bigquery.SchemaField("long_tasks_max_duration", "INT64"), - bigquery.SchemaField("long_tasks_count", "INT64"), - bigquery.SchemaField("inputs_count", "INT64"), - bigquery.SchemaField("clicks_count", "INT64"), - bigquery.SchemaField("issues_count", "INT64"), - bigquery.SchemaField("issues", "STRING"), - bigquery.SchemaField("urls_count", "INT64"), - bigquery.SchemaField("urls", "STRING")] - create_table(creds_file, table_id, schema) - - -def create_events_table(creds_file, table_id): - - schema = [ - bigquery.SchemaField("sessionid", "INT64"), - bigquery.SchemaField("connectioninformation_downlink", "INT64"), - bigquery.SchemaField("connectioninformation_type", "STRING"), - bigquery.SchemaField("consolelog_level", "STRING"), - bigquery.SchemaField("consolelog_value", "STRING"), - bigquery.SchemaField("customevent_messageid", "INT64"), - bigquery.SchemaField("customevent_name", "STRING"), - bigquery.SchemaField("customevent_payload", "STRING"), - bigquery.SchemaField("customevent_timestamp", "INT64"), - bigquery.SchemaField("errorevent_message", "STRING"), - bigquery.SchemaField("errorevent_messageid", "INT64"), - bigquery.SchemaField("errorevent_name", "STRING"), - bigquery.SchemaField("errorevent_payload", "STRING"), - bigquery.SchemaField("errorevent_source", "STRING"), - bigquery.SchemaField("errorevent_timestamp", "INT64"), - bigquery.SchemaField("jsexception_message", "STRING"), - bigquery.SchemaField("jsexception_name", "STRING"), - bigquery.SchemaField("jsexception_payload", "STRING"), - bigquery.SchemaField("metadata_key", "STRING"), - bigquery.SchemaField("metadata_value", "STRING"), - bigquery.SchemaField("mouseclick_id", "INT64"), - bigquery.SchemaField("mouseclick_hesitationtime", "INT64"), - bigquery.SchemaField("mouseclick_label", "STRING"), - bigquery.SchemaField("pageevent_firstcontentfulpaint", "INT64"), - bigquery.SchemaField("pageevent_firstpaint", "INT64"), - bigquery.SchemaField("pageevent_messageid", "INT64"), - bigquery.SchemaField("pageevent_referrer", "STRING"), - bigquery.SchemaField("pageevent_speedindex", "INT64"), - bigquery.SchemaField("pageevent_timestamp", "INT64"), - bigquery.SchemaField("pageevent_url", "STRING"), - bigquery.SchemaField("pagerendertiming_timetointeractive", "INT64"), - bigquery.SchemaField("pagerendertiming_visuallycomplete", "INT64"), - bigquery.SchemaField("rawcustomevent_name", "STRING"), - bigquery.SchemaField("rawcustomevent_payload", "STRING"), - bigquery.SchemaField("setviewportsize_height", "INT64"), - bigquery.SchemaField("setviewportsize_width", "INT64"), - bigquery.SchemaField("timestamp_timestamp", "INT64"), - bigquery.SchemaField("user_anonymous_id", "STRING"), - bigquery.SchemaField("user_id", "STRING"), - bigquery.SchemaField("issueevent_messageid", "INT64"), - bigquery.SchemaField("issueevent_timestamp", "INT64"), - bigquery.SchemaField("issueevent_type", "STRING"), - bigquery.SchemaField("issueevent_contextstring", "STRING"), - bigquery.SchemaField("issueevent_context", "STRING"), - bigquery.SchemaField("issueevent_payload", "STRING"), - bigquery.SchemaField("customissue_name", "STRING"), - bigquery.SchemaField("customissue_payload", "STRING"), - bigquery.SchemaField("received_at", "INT64"), - bigquery.SchemaField("batch_order_number", "INT64")] - create_table(creds_file, table_id, schema) - - -def create_table_negatives(creds_file, table_id): - client = bigquery.Client.from_service_account_json(creds_file) - - schema = [ - bigquery.SchemaField("sessionid", "INT64", mode="REQUIRED"), - bigquery.SchemaField("clickevent_hesitationtime", "INT64"), - bigquery.SchemaField("clickevent_label", "STRING"), - bigquery.SchemaField("clickevent_messageid", "INT64"), - bigquery.SchemaField("clickevent_timestamp", "INT64"), - bigquery.SchemaField("connectioninformation_downlink", "INT64"), - bigquery.SchemaField("connectioninformation_type", "STRING"), - bigquery.SchemaField("consolelog_level", "STRING"), - bigquery.SchemaField("consolelog_value", "STRING"), - bigquery.SchemaField("cpuissue_duration", "INT64"), - bigquery.SchemaField("cpuissue_rate", "INT64"), - bigquery.SchemaField("cpuissue_timestamp", "INT64"), - bigquery.SchemaField("createdocument", "BOOL"), - bigquery.SchemaField("createelementnode_id", "INT64"), - bigquery.SchemaField("createelementnode_parentid", "INT64"), - bigquery.SchemaField("cssdeleterule_index", "INT64"), - bigquery.SchemaField("cssdeleterule_stylesheetid", "INT64"), - bigquery.SchemaField("cssinsertrule_index", "INT64"), - bigquery.SchemaField("cssinsertrule_rule", "STRING"), - bigquery.SchemaField("cssinsertrule_stylesheetid", "INT64"), - bigquery.SchemaField("customevent_messageid", "INT64"), - bigquery.SchemaField("customevent_name", "STRING"), - bigquery.SchemaField("customevent_payload", "STRING"), - bigquery.SchemaField("customevent_timestamp", "INT64"), - bigquery.SchemaField("domdrop_timestamp", "INT64"), - bigquery.SchemaField("errorevent_message", "STRING"), - bigquery.SchemaField("errorevent_messageid", "INT64"), - bigquery.SchemaField("errorevent_name", "STRING"), - bigquery.SchemaField("errorevent_payload", "STRING"), - bigquery.SchemaField("errorevent_source", "STRING"), - bigquery.SchemaField("errorevent_timestamp", "INT64"), - bigquery.SchemaField("fetch_duration", "INT64"), - bigquery.SchemaField("fetch_method", "STRING"), - bigquery.SchemaField("fetch_request", "STRING"), - bigquery.SchemaField("fetch_response", "STRING"), - bigquery.SchemaField("fetch_status", "INT64"), - bigquery.SchemaField("fetch_timestamp", "INT64"), - bigquery.SchemaField("fetch_url", "STRING"), - bigquery.SchemaField("graphql_operationkind", "STRING"), - bigquery.SchemaField("graphql_operationname", "STRING"), - bigquery.SchemaField("graphql_response", "STRING"), - bigquery.SchemaField("graphql_variables", "STRING"), - bigquery.SchemaField("graphqlevent_messageid", "INT64"), - bigquery.SchemaField("graphqlevent_name", "STRING"), - bigquery.SchemaField("graphqlevent_timestamp", "INT64"), - bigquery.SchemaField("inputevent_label", "STRING"), - bigquery.SchemaField("inputevent_messageid", "INT64"), - bigquery.SchemaField("inputevent_timestamp", "INT64"), - bigquery.SchemaField("inputevent_value", "STRING"), - bigquery.SchemaField("inputevent_valuemasked", "BOOL"), - bigquery.SchemaField("is_asayer_event", "BOOL"), - bigquery.SchemaField("jsexception_message", "STRING"), - bigquery.SchemaField("jsexception_name", "STRING"), - bigquery.SchemaField("jsexception_payload", "STRING"), - bigquery.SchemaField("longtasks_timestamp", "INT64"), - bigquery.SchemaField("longtasks_duration", "INT64"), - bigquery.SchemaField("longtasks_containerid", "STRING"), - bigquery.SchemaField("longtasks_containersrc", "STRING"), - bigquery.SchemaField("memoryissue_duration", "INT64"), - bigquery.SchemaField("memoryissue_rate", "INT64"), - bigquery.SchemaField("memoryissue_timestamp", "INT64"), - bigquery.SchemaField("metadata_key", "STRING"), - bigquery.SchemaField("metadata_value", "STRING"), - bigquery.SchemaField("mobx_payload", "STRING"), - bigquery.SchemaField("mobx_type", "STRING"), - bigquery.SchemaField("mouseclick_id", "INT64"), - bigquery.SchemaField("mouseclick_hesitationtime", "INT64"), - bigquery.SchemaField("mouseclick_label", "STRING"), - bigquery.SchemaField("mousemove_x", "INT64"), - bigquery.SchemaField("mousemove_y", "INT64"), - bigquery.SchemaField("movenode_id", "INT64"), - bigquery.SchemaField("movenode_index", "INT64"), - bigquery.SchemaField("movenode_parentid", "INT64"), - bigquery.SchemaField("ngrx_action", "STRING"), - bigquery.SchemaField("ngrx_duration", "INT64"), - bigquery.SchemaField("ngrx_state", "STRING"), - bigquery.SchemaField("otable_key", "STRING"), - bigquery.SchemaField("otable_value", "STRING"), - bigquery.SchemaField("pageevent_domcontentloadedeventend", "INT64"), - bigquery.SchemaField("pageevent_domcontentloadedeventstart", "INT64"), - bigquery.SchemaField("pageevent_firstcontentfulpaint", "INT64"), - bigquery.SchemaField("pageevent_firstpaint", "INT64"), - bigquery.SchemaField("pageevent_loaded", "BOOL"), - bigquery.SchemaField("pageevent_loadeventend", "INT64"), - bigquery.SchemaField("pageevent_loadeventstart", "INT64"), - bigquery.SchemaField("pageevent_messageid", "INT64"), - bigquery.SchemaField("pageevent_referrer", "STRING"), - bigquery.SchemaField("pageevent_requeststart", "INT64"), - bigquery.SchemaField("pageevent_responseend", "INT64"), - bigquery.SchemaField("pageevent_responsestart", "INT64"), - bigquery.SchemaField("pageevent_speedindex", "INT64"), - bigquery.SchemaField("pageevent_timestamp", "INT64"), - bigquery.SchemaField("pageevent_url", "STRING"), - bigquery.SchemaField("pageloadtiming_domcontentloadedeventend", "INT64"), - bigquery.SchemaField("pageloadtiming_domcontentloadedeventstart", "INT64"), - bigquery.SchemaField("pageloadtiming_firstcontentfulpaint", "INT64"), - bigquery.SchemaField("pageloadtiming_firstpaint", "INT64"), - bigquery.SchemaField("pageloadtiming_loadeventend", "INT64"), - bigquery.SchemaField("pageloadtiming_loadeventstart", "INT64"), - bigquery.SchemaField("pageloadtiming_requeststart", "INT64"), - bigquery.SchemaField("pageloadtiming_responseend", "INT64"), - bigquery.SchemaField("pageloadtiming_responsestart", "INT64"), - bigquery.SchemaField("pagerendertiming_speedindex", "INT64"), - bigquery.SchemaField("pagerendertiming_timetointeractive", "INT64"), - bigquery.SchemaField("pagerendertiming_visuallycomplete", "INT64"), - bigquery.SchemaField("performancetrack_frames", "INT64"), - bigquery.SchemaField("performancetrack_ticks", "INT64"), - bigquery.SchemaField("performancetrack_totaljsheapsize", "INT64"), - bigquery.SchemaField("performancetrack_usedjsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_avgcpu", "INT64"), - bigquery.SchemaField("performancetrackaggr_avgfps", "INT64"), - bigquery.SchemaField("performancetrackaggr_avgtotaljsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_avgusedjsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_maxcpu", "INT64"), - bigquery.SchemaField("performancetrackaggr_maxfps", "INT64"), - bigquery.SchemaField("performancetrackaggr_maxtotaljsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_maxusedjsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_mincpu", "INT64"), - bigquery.SchemaField("performancetrackaggr_minfps", "INT64"), - bigquery.SchemaField("performancetrackaggr_mintotaljsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_minusedjsheapsize", "INT64"), - bigquery.SchemaField("performancetrackaggr_timestampend", "INT64"), - bigquery.SchemaField("performancetrackaggr_timestampstart", "INT64"), - bigquery.SchemaField("profiler_args", "STRING"), - bigquery.SchemaField("profiler_duration", "INT64"), - bigquery.SchemaField("profiler_name", "STRING"), - bigquery.SchemaField("profiler_result", "STRING"), - bigquery.SchemaField("rawcustomevent_name", "STRING"), - bigquery.SchemaField("rawcustomevent_payload", "STRING"), - bigquery.SchemaField("rawerrorevent_message", "STRING"), - bigquery.SchemaField("rawerrorevent_name", "STRING"), - bigquery.SchemaField("rawerrorevent_payload", "STRING"), - bigquery.SchemaField("rawerrorevent_source", "STRING"), - bigquery.SchemaField("rawerrorevent_timestamp", "INT64"), - bigquery.SchemaField("redux_action", "STRING"), - bigquery.SchemaField("redux_duration", "INT64"), - bigquery.SchemaField("redux_state", "STRING"), - bigquery.SchemaField("removenode_id", "INT64"), - bigquery.SchemaField("removenodeattribute_id", "INT64"), - bigquery.SchemaField("removenodeattribute_name", "STRING"), - bigquery.SchemaField("resourceevent_decodedbodysize", "INT64"), - bigquery.SchemaField("resourceevent_duration", "INT64"), - bigquery.SchemaField("resourceevent_encodedbodysize", "INT64"), - bigquery.SchemaField("resourceevent_headersize", "INT64"), - bigquery.SchemaField("resourceevent_messageid", "INT64"), - bigquery.SchemaField("resourceevent_method", "STRING"), - bigquery.SchemaField("resourceevent_status", "INT64"), - bigquery.SchemaField("resourceevent_success", "BOOL"), - bigquery.SchemaField("resourceevent_timestamp", "INT64"), - bigquery.SchemaField("resourceevent_ttfb", "INT64"), - bigquery.SchemaField("resourceevent_type", "STRING"), - bigquery.SchemaField("resourceevent_url", "STRING"), - bigquery.SchemaField("resourcetiming_decodedbodysize", "INT64"), - bigquery.SchemaField("resourcetiming_duration", "INT64"), - bigquery.SchemaField("resourcetiming_encodedbodysize", "INT64"), - bigquery.SchemaField("resourcetiming_headersize", "INT64"), - bigquery.SchemaField("resourcetiming_initiator", "STRING"), - bigquery.SchemaField("resourcetiming_timestamp", "INT64"), - bigquery.SchemaField("resourcetiming_ttfb", "INT64"), - bigquery.SchemaField("resourcetiming_url", "STRING"), - bigquery.SchemaField("sessiondisconnect", "BOOL"), - bigquery.SchemaField("sessiondisconnect_timestamp", "INT64"), - bigquery.SchemaField("sessionend", "BOOL"), - bigquery.SchemaField("sessionend_timestamp", "INT64"), - bigquery.SchemaField("sessionstart_projectid", "INT64"), - bigquery.SchemaField("sessionstart_revid", "STRING"), - bigquery.SchemaField("sessionstart_timestamp", "INT64"), - bigquery.SchemaField("sessionstart_trackerversion", "STRING"), - bigquery.SchemaField("sessionstart_useragent", "STRING"), - bigquery.SchemaField("sessionstart_userbrowser", "STRING"), - bigquery.SchemaField("sessionstart_userbrowserversion", "STRING"), - bigquery.SchemaField("sessionstart_usercountry", "STRING"), - bigquery.SchemaField("sessionstart_userdevice", "STRING"), - bigquery.SchemaField("sessionstart_userdeviceheapsize", "INT64"), - bigquery.SchemaField("sessionstart_userdevicememorysize", "INT64"), - bigquery.SchemaField("sessionstart_userdevicetype", "STRING"), - bigquery.SchemaField("sessionstart_useros", "STRING"), - bigquery.SchemaField("sessionstart_userosversion", "STRING"), - bigquery.SchemaField("sessionstart_useruuid", "STRING"), - bigquery.SchemaField("setcssdata_data", "INT64"), - bigquery.SchemaField("setcssdata_id", "INT64"), - bigquery.SchemaField("setinputchecked_checked", "INT64"), - bigquery.SchemaField("setinputchecked_id", "INT64"), - bigquery.SchemaField("setinputtarget_id", "INT64"), - bigquery.SchemaField("setinputtarget_label", "INT64"), - bigquery.SchemaField("setinputvalue_id", "INT64"), - bigquery.SchemaField("setinputvalue_mask", "INT64"), - bigquery.SchemaField("setinputvalue_value", "INT64"), - bigquery.SchemaField("setnodeattribute_id", "INT64"), - bigquery.SchemaField("setnodeattribute_name", "INT64"), - bigquery.SchemaField("setnodeattribute_value", "INT64"), - bigquery.SchemaField("setnodedata_data", "INT64"), - bigquery.SchemaField("setnodedata_id", "INT64"), - bigquery.SchemaField("setnodescroll_id", "INT64"), - bigquery.SchemaField("setnodescroll_x", "INT64"), - bigquery.SchemaField("setnodescroll_y", "INT64"), - bigquery.SchemaField("setpagelocation_navigationstart", "INT64"), - bigquery.SchemaField("setpagelocation_referrer", "STRING"), - bigquery.SchemaField("setpagelocation_url", "STRING"), - bigquery.SchemaField("setpagevisibility_hidden", "BOOL"), - bigquery.SchemaField("setviewportscroll_x", "BOOL"), - bigquery.SchemaField("setviewportscroll_y", "BOOL"), - bigquery.SchemaField("setviewportsize_height", "INT64"), - bigquery.SchemaField("setviewportsize_width", "INT64"), - bigquery.SchemaField("stateaction_type", "STRING"), - bigquery.SchemaField("stateactionevent_messageid", "INT64"), - bigquery.SchemaField("stateactionevent_timestamp", "INT64"), - bigquery.SchemaField("stateactionevent_type", "STRING"), - bigquery.SchemaField("timestamp_timestamp", "INT64"), - bigquery.SchemaField("useranonymousid_id", "STRING"), - bigquery.SchemaField("userid_id", "STRING"), - bigquery.SchemaField("vuex_mutation", "STRING"), - bigquery.SchemaField("vuex_state", "STRING"), - bigquery.SchemaField("received_at", "INT64", mode="REQUIRED"), - bigquery.SchemaField("batch_order_number", "INT64", mode="REQUIRED") - ] - - table = bigquery.Table(table_id, schema=schema) - table = client.create_table(table) # Make an API request. - print( - "Created table {}.{}.{}".format(table.project, table.dataset_id, table.table_id) - ) diff --git a/ee/connectors/db/api.py b/ee/connectors/db/api.py deleted file mode 100644 index 33abf67cc..000000000 --- a/ee/connectors/db/api.py +++ /dev/null @@ -1,129 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy import MetaData -from sqlalchemy.orm import sessionmaker, session -from contextlib import contextmanager -import logging -import os -from pathlib import Path - -DATABASE = os.environ['DATABASE_NAME'] -if DATABASE == 'redshift': - import pandas_redshift as pr - -base_path = Path(__file__).parent.parent - -from db.models import Base - -logger = logging.getLogger(__file__) - - -def get_class_by_tablename(tablename): - """Return class reference mapped to table. - Raise an exception if class not found - - :param tablename: String with name of table. - :return: Class reference. - """ - for c in Base._decl_class_registry.values(): - if hasattr(c, '__tablename__') and c.__tablename__ == tablename: - return c - raise AttributeError(f'No model with tablename "{tablename}"') - - -class DBConnection: - """ - Initializes connection to a database - To update models file use: - sqlacodegen --outfile models_universal.py mysql+pymysql://{user}:{pwd}@{address} - """ - _sessions = sessionmaker() - - def __init__(self, config) -> None: - self.metadata = MetaData() - self.config = config - - if config == 'redshift': - self.pdredshift = pr - self.pdredshift.connect_to_redshift(dbname=os.environ['schema'], - host=os.environ['address'], - port=os.environ['port'], - user=os.environ['user'], - password=os.environ['password']) - - self.pdredshift.connect_to_s3(aws_access_key_id=os.environ['aws_access_key_id'], - aws_secret_access_key=os.environ['aws_secret_access_key'], - bucket=os.environ['bucket'], - subdirectory=os.environ['subdirectory']) - - self.connect_str = os.environ['connect_str'].format( - user=os.environ['user'], - password=os.environ['password'], - address=os.environ['address'], - port=os.environ['port'], - schema=os.environ['schema'] - ) - self.engine = create_engine(self.connect_str) - - elif config == 'clickhouse': - self.connect_str = os.environ['connect_str'].format( - address=os.environ['address'], - database=os.environ['database'] - ) - self.engine = create_engine(self.connect_str) - elif config == 'pg': - self.connect_str = os.environ['connect_str'].format( - user=os.environ['user'], - password=os.environ['password'], - address=os.environ['address'], - port=os.environ['port'], - database=os.environ['database'] - ) - self.engine = create_engine(self.connect_str) - elif config == 'bigquery': - pass - elif config == 'snowflake': - self.connect_str = os.environ['connect_str'].format( - user=os.environ['user'], - password=os.environ['password'], - account=os.environ['account'], - database=os.environ['database'], - schema = os.environ['schema'], - warehouse = os.environ['warehouse'] - ) - self.engine = create_engine(self.connect_str) - else: - raise ValueError("This db configuration doesn't exist. Add into keys file.") - - @contextmanager - def get_test_session(self, **kwargs) -> session: - """ - Test session context, even commits won't be persisted into db. - :Keyword Arguments: - * autoflush (``bool``) -- default: True - * autocommit (``bool``) -- default: False - * expire_on_commit (``bool``) -- default: True - """ - connection = self.engine.connect() - transaction = connection.begin() - my_session = type(self)._sessions(bind=connection, **kwargs) - yield my_session - - # Do cleanup, rollback and closing, whatever happens - my_session.close() - transaction.rollback() - connection.close() - - @contextmanager - def get_live_session(self) -> session: - """ - This is a session that can be committed. - Changes will be reflected in the database. - """ - # Automatic transaction and connection handling in session - connection = self.engine.connect() - my_session = type(self)._sessions(bind=connection) - - yield my_session - - my_session.close() - connection.close() diff --git a/ee/connectors/db/loaders/__init__.py b/ee/connectors/db/loaders/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/ee/connectors/db/loaders/bigquery_loader.py b/ee/connectors/db/loaders/bigquery_loader.py deleted file mode 100644 index 2f3747d0a..000000000 --- a/ee/connectors/db/loaders/bigquery_loader.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -from pathlib import Path - -from google.oauth2.service_account import Credentials - -# obtain the JSON file: -# In the Cloud Console, go to the Create service account key page. -# -# Go to the Create Service Account Key page -# From the Service account list, select New service account. -# In the Service account name field, enter a name. -# From the Role list, select Project > Owner. -# -# Note: The Role field affects which resources your service account can access in your project. You can revoke these roles or grant additional roles later. In production environments, do not grant the Owner, Editor, or Viewer roles. For more information, see Granting, changing, and revoking access to resources. -# Click Create. A JSON file that contains your key downloads to your computer. -# -# Put it in utils under a name bigquery_service_account - -base_path = Path(__file__).parent.parent.parent -creds_file = base_path / 'utils' / 'bigquery_service_account.json' -credentials = Credentials.from_service_account_file( - creds_file) - - -def insert_to_bigquery(df, table): - df.to_gbq(destination_table=f"{os.environ['dataset']}.{table}", - project_id=os.environ['project_id'], - if_exists='append', - credentials=credentials) - - -def transit_insert_to_bigquery(db, batch): - ... - diff --git a/ee/connectors/db/loaders/clickhouse_loader.py b/ee/connectors/db/loaders/clickhouse_loader.py deleted file mode 100644 index 2fea7fd01..000000000 --- a/ee/connectors/db/loaders/clickhouse_loader.py +++ /dev/null @@ -1,4 +0,0 @@ - -def insert_to_clickhouse(db, df, table: str): - df.to_sql(table, db.engine, if_exists='append', index=False) - diff --git a/ee/connectors/db/loaders/postgres_loader.py b/ee/connectors/db/loaders/postgres_loader.py deleted file mode 100644 index bd982c607..000000000 --- a/ee/connectors/db/loaders/postgres_loader.py +++ /dev/null @@ -1,3 +0,0 @@ - -def insert_to_postgres(db, df, table: str): - df.to_sql(table, db.engine, if_exists='append', index=False) diff --git a/ee/connectors/db/loaders/redshift_loader.py b/ee/connectors/db/loaders/redshift_loader.py deleted file mode 100644 index fe31d4fc4..000000000 --- a/ee/connectors/db/loaders/redshift_loader.py +++ /dev/null @@ -1,19 +0,0 @@ -from db.models import DetailedEvent -from psycopg2.errors import InternalError_ - - -def transit_insert_to_redshift(db, df, table): - - try: - insert_df(db.pdredshift, df, table) - except InternalError_ as e: - print(repr(e)) - print("loading failed. check stl_load_errors") - - -def insert_df(pr, df, table): - # Write the DataFrame to S3 and then to redshift - pr.pandas_to_redshift(data_frame=df, - redshift_table_name=table, - append=True, - delimiter='|') diff --git a/ee/connectors/db/loaders/snowflake_loader.py b/ee/connectors/db/loaders/snowflake_loader.py deleted file mode 100644 index b0bfde37f..000000000 --- a/ee/connectors/db/loaders/snowflake_loader.py +++ /dev/null @@ -1,5 +0,0 @@ - -def insert_to_snowflake(db, df, table): - df.to_sql(table, db.engine, if_exists='append', index=False) - - diff --git a/ee/connectors/db/models.py b/ee/connectors/db/models.py deleted file mode 100644 index 46654e249..000000000 --- a/ee/connectors/db/models.py +++ /dev/null @@ -1,389 +0,0 @@ -# coding: utf-8 -import yaml -from sqlalchemy import BigInteger, Boolean, Column, Integer, ARRAY, VARCHAR, text, VARCHAR -from sqlalchemy.ext.declarative import declarative_base -from pathlib import Path -import os - -DATABASE = os.environ['DATABASE_NAME'] - -Base = declarative_base() -metadata = Base.metadata - -base_path = Path(__file__).parent.parent - -# Load configuration file -conf = yaml.load( - open(f'{base_path}/utils/config.yml'), Loader=yaml.FullLoader) -try: - db_conf = conf[DATABASE] -except KeyError: - raise KeyError("Please provide a configuration in a YAML file with a key like\n" - "'snowflake', 'pg', 'bigquery', 'clickhouse' or 'redshift'.") - -# Get a table name from a configuration file -try: - events_table_name = db_conf['events_table_name'] -except KeyError as e: - events_table_name = None - print(repr(e)) -try: - events_detailed_table_name = db_conf['events_detailed_table_name'] -except KeyError as e: - print(repr(e)) - events_detailed_table_name = None -try: - sessions_table_name = db_conf['sessions_table'] -except KeyError as e: - print(repr(e)) - raise KeyError("Please provide a table name under a key 'table' in a YAML configuration file") - - -class Session(Base): - __tablename__ = sessions_table_name - - sessionid = Column(BigInteger, primary_key=True) - user_agent = Column(VARCHAR(5000)) - user_browser = Column(VARCHAR(5000)) - user_browser_version = Column(VARCHAR(5000)) - user_country = Column(VARCHAR(5000)) - user_device = Column(VARCHAR(5000)) - user_device_heap_size = Column(BigInteger) - user_device_memory_size = Column(BigInteger) - user_device_type = Column(VARCHAR(5000)) - user_os = Column(VARCHAR(5000)) - user_os_version = Column(VARCHAR(5000)) - user_uuid = Column(VARCHAR(5000)) - connection_effective_bandwidth = Column(BigInteger) # Downlink - connection_type = Column(VARCHAR(5000)) # "bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" - metadata_key = Column(VARCHAR(5000)) - metadata_value = Column(VARCHAR(5000)) - referrer = Column(VARCHAR(5000)) - user_anonymous_id = Column(VARCHAR(5000)) - user_id = Column(VARCHAR(5000)) - - # TIME - session_start_timestamp = Column(BigInteger) - session_end_timestamp = Column(BigInteger) - session_duration = Column(BigInteger) - - # SPEED INDEX RELATED - first_contentful_paint = Column(BigInteger) - speed_index = Column(BigInteger) - visually_complete = Column(BigInteger) - timing_time_to_interactive = Column(BigInteger) - - # PERFORMANCE - avg_cpu = Column(Integer) - avg_fps = Column(BigInteger) - max_cpu = Column(Integer) - max_fps = Column(BigInteger) - max_total_js_heap_size = Column(BigInteger) - max_used_js_heap_size = Column(BigInteger) - - # ISSUES AND EVENTS - js_exceptions_count = Column(BigInteger) - long_tasks_total_duration = Column(BigInteger) - long_tasks_max_duration = Column(BigInteger) - long_tasks_count = Column(BigInteger) - inputs_count = Column(BigInteger) - clicks_count = Column(BigInteger) - issues_count = Column(BigInteger) - issues = ARRAY(VARCHAR(5000)) - urls_count = Column(BigInteger) - urls = ARRAY(VARCHAR(5000)) - - -class Event(Base): - __tablename__ = events_table_name - - sessionid = Column(BigInteger, primary_key=True) - connectioninformation_downlink = Column(BigInteger) - connectioninformation_type = Column(VARCHAR(5000)) - consolelog_level = Column(VARCHAR(5000)) - consolelog_value = Column(VARCHAR(5000)) - customevent_messageid = Column(BigInteger) - customevent_name = Column(VARCHAR(5000)) - customevent_payload = Column(VARCHAR(5000)) - customevent_timestamp = Column(BigInteger) - errorevent_message = Column(VARCHAR(5000)) - errorevent_messageid = Column(BigInteger) - errorevent_name = Column(VARCHAR(5000)) - errorevent_payload = Column(VARCHAR(5000)) - errorevent_source = Column(VARCHAR(5000)) - errorevent_timestamp = Column(BigInteger) - jsexception_message = Column(VARCHAR(5000)) - jsexception_name = Column(VARCHAR(5000)) - jsexception_payload = Column(VARCHAR(5000)) - metadata_key = Column(VARCHAR(5000)) - metadata_value = Column(VARCHAR(5000)) - mouseclick_id = Column(BigInteger) - mouseclick_hesitationtime = Column(BigInteger) - mouseclick_label = Column(VARCHAR(5000)) - pageevent_firstcontentfulpaint = Column(BigInteger) - pageevent_firstpaint = Column(BigInteger) - pageevent_messageid = Column(BigInteger) - pageevent_referrer = Column(VARCHAR(5000)) - pageevent_speedindex = Column(BigInteger) - pageevent_timestamp = Column(BigInteger) - pageevent_url = Column(VARCHAR(5000)) - pagerendertiming_timetointeractive = Column(BigInteger) - pagerendertiming_visuallycomplete = Column(BigInteger) - rawcustomevent_name = Column(VARCHAR(5000)) - rawcustomevent_payload = Column(VARCHAR(5000)) - setviewportsize_height = Column(BigInteger) - setviewportsize_width = Column(BigInteger) - timestamp_timestamp = Column(BigInteger) - user_anonymous_id = Column(VARCHAR(5000)) - user_id = Column(VARCHAR(5000)) - issueevent_messageid = Column(BigInteger) - issueevent_timestamp = Column(BigInteger) - issueevent_type = Column(VARCHAR(5000)) - issueevent_contextstring = Column(VARCHAR(5000)) - issueevent_context = Column(VARCHAR(5000)) - issueevent_payload = Column(VARCHAR(5000)) - customissue_name = Column(VARCHAR(5000)) - customissue_payload = Column(VARCHAR(5000)) - received_at = Column(BigInteger) - batch_order_number = Column(BigInteger) - - -class DetailedEvent(Base): - __tablename__ = events_detailed_table_name - - # id = Column(Integer, primary_key=True, server_default=text("\"identity\"(119029, 0, '0,1'::text)")) - sessionid = Column(BigInteger, primary_key=True) - clickevent_hesitationtime = Column(BigInteger) - clickevent_label = Column(VARCHAR(5000)) - clickevent_messageid = Column(BigInteger) - clickevent_timestamp = Column(BigInteger) - connectioninformation_downlink = Column(BigInteger) - connectioninformation_type = Column(VARCHAR(5000)) - consolelog_level = Column(VARCHAR(5000)) - consolelog_value = Column(VARCHAR(5000)) - cpuissue_duration = Column(BigInteger) - cpuissue_rate = Column(BigInteger) - cpuissue_timestamp = Column(BigInteger) - createdocument = Column(Boolean) - createelementnode_id = Column(BigInteger) - createelementnode_parentid = Column(BigInteger) - cssdeleterule_index = Column(BigInteger) - cssdeleterule_stylesheetid = Column(BigInteger) - cssinsertrule_index = Column(BigInteger) - cssinsertrule_rule = Column(VARCHAR(5000)) - cssinsertrule_stylesheetid = Column(BigInteger) - customevent_messageid = Column(BigInteger) - customevent_name = Column(VARCHAR(5000)) - customevent_payload = Column(VARCHAR(5000)) - customevent_timestamp = Column(BigInteger) - domdrop_timestamp = Column(BigInteger) - errorevent_message = Column(VARCHAR(5000)) - errorevent_messageid = Column(BigInteger) - errorevent_name = Column(VARCHAR(5000)) - errorevent_payload = Column(VARCHAR(5000)) - errorevent_source = Column(VARCHAR(5000)) - errorevent_timestamp = Column(BigInteger) - fetch_duration = Column(BigInteger) - fetch_method = Column(VARCHAR(5000)) - fetch_request = Column(VARCHAR(5000)) - fetch_response = Column(VARCHAR(5000)) - fetch_status = Column(BigInteger) - fetch_timestamp = Column(BigInteger) - fetch_url = Column(VARCHAR(5000)) - graphql_operationkind = Column(VARCHAR(5000)) - graphql_operationname = Column(VARCHAR(5000)) - graphql_response = Column(VARCHAR(5000)) - graphql_variables = Column(VARCHAR(5000)) - graphqlevent_messageid = Column(BigInteger) - graphqlevent_name = Column(VARCHAR(5000)) - graphqlevent_timestamp = Column(BigInteger) - inputevent_label = Column(VARCHAR(5000)) - inputevent_messageid = Column(BigInteger) - inputevent_timestamp = Column(BigInteger) - inputevent_value = Column(VARCHAR(5000)) - inputevent_valuemasked = Column(Boolean) - jsexception_message = Column(VARCHAR(5000)) - jsexception_name = Column(VARCHAR(5000)) - jsexception_payload = Column(VARCHAR(5000)) - memoryissue_duration = Column(BigInteger) - memoryissue_rate = Column(BigInteger) - memoryissue_timestamp = Column(BigInteger) - metadata_key = Column(VARCHAR(5000)) - metadata_value = Column(VARCHAR(5000)) - mobx_payload = Column(VARCHAR(5000)) - mobx_type = Column(VARCHAR(5000)) - mouseclick_id = Column(BigInteger) - mouseclick_hesitationtime = Column(BigInteger) - mouseclick_label = Column(VARCHAR(5000)) - mousemove_x = Column(BigInteger) - mousemove_y = Column(BigInteger) - movenode_id = Column(BigInteger) - movenode_index = Column(BigInteger) - movenode_parentid = Column(BigInteger) - ngrx_action = Column(VARCHAR(5000)) - ngrx_duration = Column(BigInteger) - ngrx_state = Column(VARCHAR(5000)) - otable_key = Column(VARCHAR(5000)) - otable_value = Column(VARCHAR(5000)) - pageevent_domcontentloadedeventend = Column(BigInteger) - pageevent_domcontentloadedeventstart = Column(BigInteger) - pageevent_firstcontentfulpaint = Column(BigInteger) - pageevent_firstpaint = Column(BigInteger) - pageevent_loaded = Column(Boolean) - pageevent_loadeventend = Column(BigInteger) - pageevent_loadeventstart = Column(BigInteger) - pageevent_messageid = Column(BigInteger) - pageevent_referrer = Column(VARCHAR(5000)) - pageevent_requeststart = Column(BigInteger) - pageevent_responseend = Column(BigInteger) - pageevent_responsestart = Column(BigInteger) - pageevent_speedindex = Column(BigInteger) - pageevent_timestamp = Column(BigInteger) - pageevent_url = Column(VARCHAR(5000)) - pageloadtiming_domcontentloadedeventend = Column(BigInteger) - pageloadtiming_domcontentloadedeventstart = Column(BigInteger) - pageloadtiming_firstcontentfulpaint = Column(BigInteger) - pageloadtiming_firstpaint = Column(BigInteger) - pageloadtiming_loadeventend = Column(BigInteger) - pageloadtiming_loadeventstart = Column(BigInteger) - pageloadtiming_requeststart = Column(BigInteger) - pageloadtiming_responseend = Column(BigInteger) - pageloadtiming_responsestart = Column(BigInteger) - pagerendertiming_speedindex = Column(BigInteger) - pagerendertiming_timetointeractive = Column(BigInteger) - pagerendertiming_visuallycomplete = Column(BigInteger) - performancetrack_frames = Column(BigInteger) - performancetrack_ticks = Column(BigInteger) - performancetrack_totaljsheapsize = Column(BigInteger) - performancetrack_usedjsheapsize = Column(BigInteger) - performancetrackaggr_avgcpu = Column(BigInteger) - performancetrackaggr_avgfps = Column(BigInteger) - performancetrackaggr_avgtotaljsheapsize = Column(BigInteger) - performancetrackaggr_avgusedjsheapsize = Column(BigInteger) - performancetrackaggr_maxcpu = Column(BigInteger) - performancetrackaggr_maxfps = Column(BigInteger) - performancetrackaggr_maxtotaljsheapsize = Column(BigInteger) - performancetrackaggr_maxusedjsheapsize = Column(BigInteger) - performancetrackaggr_mincpu = Column(BigInteger) - performancetrackaggr_minfps = Column(BigInteger) - performancetrackaggr_mintotaljsheapsize = Column(BigInteger) - performancetrackaggr_minusedjsheapsize = Column(BigInteger) - performancetrackaggr_timestampend = Column(BigInteger) - performancetrackaggr_timestampstart = Column(BigInteger) - profiler_args = Column(VARCHAR(5000)) - profiler_duration = Column(BigInteger) - profiler_name = Column(VARCHAR(5000)) - profiler_result = Column(VARCHAR(5000)) - rawcustomevent_name = Column(VARCHAR(5000)) - rawcustomevent_payload = Column(VARCHAR(5000)) - rawerrorevent_message = Column(VARCHAR(5000)) - rawerrorevent_name = Column(VARCHAR(5000)) - rawerrorevent_payload = Column(VARCHAR(5000)) - rawerrorevent_source = Column(VARCHAR(5000)) - rawerrorevent_timestamp = Column(BigInteger) - redux_action = Column(VARCHAR(5000)) - redux_duration = Column(BigInteger) - redux_state = Column(VARCHAR(5000)) - removenode_id = Column(BigInteger) - removenodeattribute_id = Column(BigInteger) - removenodeattribute_name = Column(VARCHAR(5000)) - resourceevent_decodedbodysize = Column(BigInteger) - resourceevent_duration = Column(BigInteger) - resourceevent_encodedbodysize = Column(BigInteger) - resourceevent_headersize = Column(BigInteger) - resourceevent_messageid = Column(BigInteger) - resourceevent_method = Column(VARCHAR(5000)) - resourceevent_status = Column(BigInteger) - resourceevent_success = Column(Boolean) - resourceevent_timestamp = Column(BigInteger) - resourceevent_ttfb = Column(BigInteger) - resourceevent_type = Column(VARCHAR(5000)) - resourceevent_url = Column(VARCHAR(5000)) - resourcetiming_decodedbodysize = Column(BigInteger) - resourcetiming_duration = Column(BigInteger) - resourcetiming_encodedbodysize = Column(BigInteger) - resourcetiming_headersize = Column(BigInteger) - resourcetiming_initiator = Column(VARCHAR(5000)) - resourcetiming_timestamp = Column(BigInteger) - resourcetiming_ttfb = Column(BigInteger) - resourcetiming_url = Column(VARCHAR(5000)) - sessiondisconnect = Column(Boolean) - sessiondisconnect_timestamp = Column(BigInteger) - sessionend = Column(Boolean) - sessionend_timestamp = Column(BigInteger) - sessionstart_projectid = Column(BigInteger) - sessionstart_revid = Column(VARCHAR(5000)) - sessionstart_timestamp = Column(BigInteger) - sessionstart_trackerversion = Column(VARCHAR(5000)) - sessionstart_useragent = Column(VARCHAR(5000)) - sessionstart_userbrowser = Column(VARCHAR(5000)) - sessionstart_userbrowserversion = Column(VARCHAR(5000)) - sessionstart_usercountry = Column(VARCHAR(5000)) - sessionstart_userdevice = Column(VARCHAR(5000)) - sessionstart_userdeviceheapsize = Column(BigInteger) - sessionstart_userdevicememorysize = Column(BigInteger) - sessionstart_userdevicetype = Column(VARCHAR(5000)) - sessionstart_useros = Column(VARCHAR(5000)) - sessionstart_userosversion = Column(VARCHAR(5000)) - sessionstart_useruuid = Column(VARCHAR(5000)) - setcssdata_data = Column(BigInteger) - setcssdata_id = Column(BigInteger) - setinputchecked_checked = Column(BigInteger) - setinputchecked_id = Column(BigInteger) - setinputtarget_id = Column(BigInteger) - setinputtarget_label = Column(BigInteger) - setinputvalue_id = Column(BigInteger) - setinputvalue_mask = Column(BigInteger) - setinputvalue_value = Column(BigInteger) - setnodeattribute_id = Column(BigInteger) - setnodeattribute_name = Column(BigInteger) - setnodeattribute_value = Column(BigInteger) - setnodedata_data = Column(BigInteger) - setnodedata_id = Column(BigInteger) - setnodescroll_id = Column(BigInteger) - setnodescroll_x = Column(BigInteger) - setnodescroll_y = Column(BigInteger) - setpagelocation_navigationstart = Column(BigInteger) - setpagelocation_referrer = Column(VARCHAR(5000)) - setpagelocation_url = Column(VARCHAR(5000)) - setpagevisibility_hidden = Column(Boolean) - setviewportscroll_x = Column(BigInteger) - setviewportscroll_y = Column(BigInteger) - setviewportsize_height = Column(BigInteger) - setviewportsize_width = Column(BigInteger) - stateaction_type = Column(VARCHAR(5000)) - stateactionevent_messageid = Column(BigInteger) - stateactionevent_timestamp = Column(BigInteger) - stateactionevent_type = Column(VARCHAR(5000)) - timestamp_timestamp = Column(BigInteger) - useranonymousid_id = Column(VARCHAR(5000)) - userid_id = Column(VARCHAR(5000)) - vuex_mutation = Column(VARCHAR(5000)) - vuex_state = Column(VARCHAR(5000)) - longtask_timestamp = Column(BigInteger) - longtask_duration = Column(BigInteger) - longtask_context = Column(BigInteger) - longtask_containertype = Column(BigInteger) - longtask_containersrc = Column(VARCHAR(5000)) - longtask_containerid = Column(VARCHAR(5000)) - longtask_containername = Column(VARCHAR(5000)) - setnodeurlbasedattribute_id = Column(BigInteger) - setnodeurlbasedattribute_name = Column(VARCHAR(5000)) - setnodeurlbasedattribute_value = Column(VARCHAR(5000)) - setnodeurlbasedattribute_baseurl = Column(VARCHAR(5000)) - setstyledata_id = Column(BigInteger) - setstyledata_data = Column(VARCHAR(5000)) - setstyledata_baseurl = Column(VARCHAR(5000)) - issueevent_messageid = Column(BigInteger) - issueevent_timestamp = Column(BigInteger) - issueevent_type = Column(VARCHAR(5000)) - issueevent_contextstring = Column(VARCHAR(5000)) - issueevent_context = Column(VARCHAR(5000)) - issueevent_payload = Column(VARCHAR(5000)) - technicalinfo_type = Column(VARCHAR(5000)) - technicalinfo_value = Column(VARCHAR(5000)) - customissue_name = Column(VARCHAR(5000)) - customissue_payload = Column(VARCHAR(5000)) - pageclose = Column(Boolean) - received_at = Column(BigInteger) - batch_order_number = Column(BigInteger) diff --git a/ee/connectors/db/tables.py b/ee/connectors/db/tables.py deleted file mode 100644 index 0127cbbd1..000000000 --- a/ee/connectors/db/tables.py +++ /dev/null @@ -1,61 +0,0 @@ -from pathlib import Path - -base_path = Path(__file__).parent.parent - - -def create_tables_clickhouse(db): - with open(base_path / 'sql' / 'clickhouse_events.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_user_events` table created succesfully.") - - with open(base_path / 'sql' / 'clickhouse_events_buffer.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_user_events_buffer` table created succesfully.") - - with open(base_path / 'sql' / 'clickhouse_sessions.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_sessions` table created succesfully.") - - with open(base_path / 'sql' / 'clickhouse_sessions_buffer.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_sessions_buffer` table created succesfully.") - - -def create_tables_postgres(db): - with open(base_path / 'sql' / 'postgres_events.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_user_events` table created succesfully.") - - with open(base_path / 'sql' / 'postgres_sessions.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_sessions` table created succesfully.") - - -def create_tables_snowflake(db): - with open(base_path / 'sql' / 'snowflake_events.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_user_events` table created succesfully.") - - with open(base_path / 'sql' / 'snowflake_sessions.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_sessions` table created succesfully.") - - -def create_tables_redshift(db): - with open(base_path / 'sql' / 'redshift_events.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_user_events` table created succesfully.") - - with open(base_path / 'sql' / 'redshift_sessions.sql') as f: - q = f.read() - db.engine.execute(q) - print(f"`connector_sessions` table created succesfully.") diff --git a/ee/connectors/db/utils.py b/ee/connectors/db/utils.py deleted file mode 100644 index 7c268c6b3..000000000 --- a/ee/connectors/db/utils.py +++ /dev/null @@ -1,368 +0,0 @@ -import pandas as pd -from db.models import DetailedEvent, Event, Session, DATABASE - -dtypes_events = {'sessionid': "Int64", - 'connectioninformation_downlink': "Int64", - 'connectioninformation_type': "string", - 'consolelog_level': "string", - 'consolelog_value': "string", - 'customevent_messageid': "Int64", - 'customevent_name': "string", - 'customevent_payload': "string", - 'customevent_timestamp': "Int64", - 'errorevent_message': "string", - 'errorevent_messageid': "Int64", - 'errorevent_name': "string", - 'errorevent_payload': "string", - 'errorevent_source': "string", - 'errorevent_timestamp': "Int64", - 'jsexception_message': "string", - 'jsexception_name': "string", - 'jsexception_payload': "string", - 'metadata_key': "string", - 'metadata_value': "string", - 'mouseclick_id': "Int64", - 'mouseclick_hesitationtime': "Int64", - 'mouseclick_label': "string", - 'pageevent_firstcontentfulpaint': "Int64", - 'pageevent_firstpaint': "Int64", - 'pageevent_messageid': "Int64", - 'pageevent_referrer': "string", - 'pageevent_speedindex': "Int64", - 'pageevent_timestamp': "Int64", - 'pageevent_url': "string", - 'pagerendertiming_timetointeractive': "Int64", - 'pagerendertiming_visuallycomplete': "Int64", - 'rawcustomevent_name': "string", - 'rawcustomevent_payload': "string", - 'setviewportsize_height': "Int64", - 'setviewportsize_width': "Int64", - 'timestamp_timestamp': "Int64", - 'user_anonymous_id': "string", - 'user_id': "string", - 'issueevent_messageid': "Int64", - 'issueevent_timestamp': "Int64", - 'issueevent_type': "string", - 'issueevent_contextstring': "string", - 'issueevent_context': "string", - 'issueevent_payload': "string", - 'customissue_name': "string", - 'customissue_payload': "string", - 'received_at': "Int64", - 'batch_order_number': "Int64"} -dtypes_detailed_events = { - "sessionid": "Int64", - "clickevent_hesitationtime": "Int64", - "clickevent_label": "object", - "clickevent_messageid": "Int64", - "clickevent_timestamp": "Int64", - "connectioninformation_downlink": "Int64", - "connectioninformation_type": "object", - "consolelog_level": "object", - "consolelog_value": "object", - "cpuissue_duration": "Int64", - "cpuissue_rate": "Int64", - "cpuissue_timestamp": "Int64", - "createdocument": "boolean", - "createelementnode_id": "Int64", - "createelementnode_parentid": "Int64", - "cssdeleterule_index": "Int64", - "cssdeleterule_stylesheetid": "Int64", - "cssinsertrule_index": "Int64", - "cssinsertrule_rule": "object", - "cssinsertrule_stylesheetid": "Int64", - "customevent_messageid": "Int64", - "customevent_name": "object", - "customevent_payload": "object", - "customevent_timestamp": "Int64", - "domdrop_timestamp": "Int64", - "errorevent_message": "object", - "errorevent_messageid": "Int64", - "errorevent_name": "object", - "errorevent_payload": "object", - "errorevent_source": "object", - "errorevent_timestamp": "Int64", - "fetch_duration": "Int64", - "fetch_method": "object", - "fetch_request": "object", - "fetch_response": "object", - "fetch_status": "Int64", - "fetch_timestamp": "Int64", - "fetch_url": "object", - "graphql_operationkind": "object", - "graphql_operationname": "object", - "graphql_response": "object", - "graphql_variables": "object", - "graphqlevent_messageid": "Int64", - "graphqlevent_name": "object", - "graphqlevent_timestamp": "Int64", - "inputevent_label": "object", - "inputevent_messageid": "Int64", - "inputevent_timestamp": "Int64", - "inputevent_value": "object", - "inputevent_valuemasked": "boolean", - "jsexception_message": "object", - "jsexception_name": "object", - "jsexception_payload": "object", - "longtasks_timestamp": "Int64", - "longtasks_duration": "Int64", - "longtasks_containerid": "object", - "longtasks_containersrc": "object", - "memoryissue_duration": "Int64", - "memoryissue_rate": "Int64", - "memoryissue_timestamp": "Int64", - "metadata_key": "object", - "metadata_value": "object", - "mobx_payload": "object", - "mobx_type": "object", - "mouseclick_id": "Int64", - "mouseclick_hesitationtime": "Int64", - "mouseclick_label": "object", - "mousemove_x": "Int64", - "mousemove_y": "Int64", - "movenode_id": "Int64", - "movenode_index": "Int64", - "movenode_parentid": "Int64", - "ngrx_action": "object", - "ngrx_duration": "Int64", - "ngrx_state": "object", - "otable_key": "object", - "otable_value": "object", - "pageevent_domcontentloadedeventend": "Int64", - "pageevent_domcontentloadedeventstart": "Int64", - "pageevent_firstcontentfulpaint": "Int64", - "pageevent_firstpaint": "Int64", - "pageevent_loaded": "boolean", - "pageevent_loadeventend": "Int64", - "pageevent_loadeventstart": "Int64", - "pageevent_messageid": "Int64", - "pageevent_referrer": "object", - "pageevent_requeststart": "Int64", - "pageevent_responseend": "Int64", - "pageevent_responsestart": "Int64", - "pageevent_speedindex": "Int64", - "pageevent_timestamp": "Int64", - "pageevent_url": "object", - "pageloadtiming_domcontentloadedeventend": "Int64", - "pageloadtiming_domcontentloadedeventstart": "Int64", - "pageloadtiming_firstcontentfulpaint": "Int64", - "pageloadtiming_firstpaint": "Int64", - "pageloadtiming_loadeventend": "Int64", - "pageloadtiming_loadeventstart": "Int64", - "pageloadtiming_requeststart": "Int64", - "pageloadtiming_responseend": "Int64", - "pageloadtiming_responsestart": "Int64", - "pagerendertiming_speedindex": "Int64", - "pagerendertiming_timetointeractive": "Int64", - "pagerendertiming_visuallycomplete": "Int64", - "performancetrack_frames": "Int64", - "performancetrack_ticks": "Int64", - "performancetrack_totaljsheapsize": "Int64", - "performancetrack_usedjsheapsize": "Int64", - "performancetrackaggr_avgcpu": "Int64", - "performancetrackaggr_avgfps": "Int64", - "performancetrackaggr_avgtotaljsheapsize": "Int64", - "performancetrackaggr_avgusedjsheapsize": "Int64", - "performancetrackaggr_maxcpu": "Int64", - "performancetrackaggr_maxfps": "Int64", - "performancetrackaggr_maxtotaljsheapsize": "Int64", - "performancetrackaggr_maxusedjsheapsize": "Int64", - "performancetrackaggr_mincpu": "Int64", - "performancetrackaggr_minfps": "Int64", - "performancetrackaggr_mintotaljsheapsize": "Int64", - "performancetrackaggr_minusedjsheapsize": "Int64", - "performancetrackaggr_timestampend": "Int64", - "performancetrackaggr_timestampstart": "Int64", - "profiler_args": "object", - "profiler_duration": "Int64", - "profiler_name": "object", - "profiler_result": "object", - "rawcustomevent_name": "object", - "rawcustomevent_payload": "object", - "rawerrorevent_message": "object", - "rawerrorevent_name": "object", - "rawerrorevent_payload": "object", - "rawerrorevent_source": "object", - "rawerrorevent_timestamp": "Int64", - "redux_action": "object", - "redux_duration": "Int64", - "redux_state": "object", - "removenode_id": "Int64", - "removenodeattribute_id": "Int64", - "removenodeattribute_name": "object", - "resourceevent_decodedbodysize": "Int64", - "resourceevent_duration": "Int64", - "resourceevent_encodedbodysize": "Int64", - "resourceevent_headersize": "Int64", - "resourceevent_messageid": "Int64", - "resourceevent_method": "object", - "resourceevent_status": "Int64", - "resourceevent_success": "boolean", - "resourceevent_timestamp": "Int64", - "resourceevent_ttfb": "Int64", - "resourceevent_type": "object", - "resourceevent_url": "object", - "resourcetiming_decodedbodysize": "Int64", - "resourcetiming_duration": "Int64", - "resourcetiming_encodedbodysize": "Int64", - "resourcetiming_headersize": "Int64", - "resourcetiming_initiator": "object", - "resourcetiming_timestamp": "Int64", - "resourcetiming_ttfb": "Int64", - "resourcetiming_url": "object", - "sessiondisconnect": "boolean", - "sessiondisconnect_timestamp": "Int64", - "sessionend": "boolean", - "sessionend_timestamp": "Int64", - "sessionstart_projectid": "Int64", - "sessionstart_revid": "object", - "sessionstart_timestamp": "Int64", - "sessionstart_trackerversion": "object", - "sessionstart_useragent": "object", - "sessionstart_userbrowser": "object", - "sessionstart_userbrowserversion": "object", - "sessionstart_usercountry": "object", - "sessionstart_userdevice": "object", - "sessionstart_userdeviceheapsize": "Int64", - "sessionstart_userdevicememorysize": "Int64", - "sessionstart_userdevicetype": "object", - "sessionstart_useros": "object", - "sessionstart_userosversion": "object", - "sessionstart_useruuid": "object", - "setcssdata_data": "Int64", - "setcssdata_id": "Int64", - "setinputchecked_checked": "Int64", - "setinputchecked_id": "Int64", - "setinputtarget_id": "Int64", - "setinputtarget_label": "Int64", - "setinputvalue_id": "Int64", - "setinputvalue_mask": "Int64", - "setinputvalue_value": "Int64", - "setnodeattribute_id": "Int64", - "setnodeattribute_name": "Int64", - "setnodeattribute_value": "Int64", - "setnodedata_data": "Int64", - "setnodedata_id": "Int64", - "setnodescroll_id": "Int64", - "setnodescroll_x": "Int64", - "setnodescroll_y": "Int64", - "setpagelocation_navigationstart": "Int64", - "setpagelocation_referrer": "object", - "setpagelocation_url": "object", - "setpagevisibility_hidden": "boolean", - "setviewportscroll_x": "Int64", - "setviewportscroll_y": "Int64", - "setviewportsize_height": "Int64", - "setviewportsize_width": "Int64", - "stateaction_type": "object", - "stateactionevent_messageid": "Int64", - "stateactionevent_timestamp": "Int64", - "stateactionevent_type": "object", - "timestamp_timestamp": "Int64", - "useranonymousid_id": "object", - "userid_id": "object", - "vuex_mutation": "object", - "vuex_state": "string", - "received_at": "Int64", - "batch_order_number": "Int64" -} -dtypes_sessions = {'sessionid': 'Int64', - 'user_agent': 'string', - 'user_browser': 'string', - 'user_browser_version': 'string', - 'user_country': 'string', - 'user_device': 'string', - 'user_device_heap_size': 'Int64', - 'user_device_memory_size': 'Int64', - 'user_device_type': 'string', - 'user_os': 'string', - 'user_os_version': 'string', - 'user_uuid': 'string', - 'connection_effective_bandwidth': 'Int64', - 'connection_type': 'string', - 'metadata_key': 'string', - 'metadata_value': 'string', - 'referrer': 'string', - 'user_anonymous_id': 'string', - 'user_id': 'string', - 'session_start_timestamp': 'Int64', - 'session_end_timestamp': 'Int64', - 'session_duration': 'Int64', - 'first_contentful_paint': 'Int64', - 'speed_index': 'Int64', - 'visually_complete': 'Int64', - 'timing_time_to_interactive': 'Int64', - 'avg_cpu': 'Int64', - 'avg_fps': 'Int64', - 'max_cpu': 'Int64', - 'max_fps': 'Int64', - 'max_total_js_heap_size': 'Int64', - 'max_used_js_heap_size': 'Int64', - 'js_exceptions_count': 'Int64', - 'long_tasks_total_duration': 'Int64', - 'long_tasks_max_duration': 'Int64', - 'long_tasks_count': 'Int64', - 'inputs_count': 'Int64', - 'clicks_count': 'Int64', - 'issues_count': 'Int64', - 'issues': 'object', - 'urls_count': 'Int64', - 'urls': 'object'} - -if DATABASE == 'bigquery': - dtypes_sessions['urls'] = 'string' - dtypes_sessions['issues'] = 'string' - -detailed_events_col = [] -for col in DetailedEvent.__dict__: - if not col.startswith('_'): - detailed_events_col.append(col) - -events_col = [] -for col in Event.__dict__: - if not col.startswith('_'): - events_col.append(col) - -sessions_col = [] -for col in Session.__dict__: - if not col.startswith('_'): - sessions_col.append(col) - - -def get_df_from_batch(batch, level): - if level == 'normal': - df = pd.DataFrame([b.__dict__ for b in batch], columns=events_col) - if level == 'detailed': - df = pd.DataFrame([b.__dict__ for b in batch], columns=detailed_events_col) - if level == 'sessions': - df = pd.DataFrame([b.__dict__ for b in batch], columns=sessions_col) - - try: - df = df.drop('_sa_instance_state', axis=1) - except KeyError: - pass - - if level == 'normal': - df = df.astype(dtypes_events) - if level == 'detailed': - df['inputevent_value'] = None - df['customevent_payload'] = None - df = df.astype(dtypes_detailed_events) - if level == 'sessions': - df = df.astype(dtypes_sessions) - - if DATABASE == 'clickhouse' and level == 'sessions': - df['issues'] = df['issues'].fillna('') - df['urls'] = df['urls'].fillna('') - - for x in df.columns: - try: - if df[x].dtype == 'string': - df[x] = df[x].str.slice(0, 255) - df[x] = df[x].str.replace("|", "") - except TypeError as e: - print(repr(e)) - if df[x].dtype == 'str': - df[x] = df[x].str.slice(0, 255) - df[x] = df[x].str.replace("|", "") - return df diff --git a/ee/connectors/db/writer.py b/ee/connectors/db/writer.py deleted file mode 100644 index b999b773f..000000000 --- a/ee/connectors/db/writer.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -DATABASE = os.environ['DATABASE_NAME'] - -from db.api import DBConnection -from db.utils import get_df_from_batch -from db.tables import * - -if DATABASE == 'redshift': - from db.loaders.redshift_loader import transit_insert_to_redshift -if DATABASE == 'clickhouse': - from db.loaders.clickhouse_loader import insert_to_clickhouse -if DATABASE == 'pg': - from db.loaders.postgres_loader import insert_to_postgres -if DATABASE == 'bigquery': - from db.loaders.bigquery_loader import insert_to_bigquery - from bigquery_utils.create_table import create_tables_bigquery -if DATABASE == 'snowflake': - from db.loaders.snowflake_loader import insert_to_snowflake - - -# create tables if don't exist -try: - db = DBConnection(DATABASE) - if DATABASE == 'pg': - create_tables_postgres(db) - if DATABASE == 'clickhouse': - create_tables_clickhouse(db) - if DATABASE == 'snowflake': - create_tables_snowflake(db) - if DATABASE == 'bigquery': - create_tables_bigquery() - if DATABASE == 'redshift': - create_tables_redshift(db) - db.engine.dispose() - db = None -except Exception as e: - print(repr(e)) - print("Please create the tables with scripts provided in " - "'/sql/{DATABASE}_sessions.sql' and '/sql/{DATABASE}_events.sql'") - - -def insert_batch(db: DBConnection, batch, table, level='normal'): - - if len(batch) == 0: - return - df = get_df_from_batch(batch, level=level) - - if db.config == 'redshift': - transit_insert_to_redshift(db=db, df=df, table=table) - return - - if db.config == 'clickhouse': - insert_to_clickhouse(db=db, df=df, table=table) - - if db.config == 'pg': - insert_to_postgres(db=db, df=df, table=table) - - if db.config == 'bigquery': - insert_to_bigquery(df=df, table=table) - - if db.config == 'snowflake': - insert_to_snowflake(db=db, df=df, table=table) - diff --git a/ee/connectors/handler.py b/ee/connectors/handler.py deleted file mode 100644 index 5167c7800..000000000 --- a/ee/connectors/handler.py +++ /dev/null @@ -1,647 +0,0 @@ -from typing import Optional, Union - -from db.models import Event, DetailedEvent, Session -from msgcodec.messages import * - - -def handle_normal_message(message: Message) -> Optional[Event]: - - n = Event() - - if isinstance(message, ConnectionInformation): - n.connectioninformation_downlink = message.downlink - n.connectioninformation_type = message.type - return n - - if isinstance(message, ConsoleLog): - n.consolelog_level = message.level - n.consolelog_value = message.value - return n - - if isinstance(message, CustomEvent): - n.customevent_messageid = message.message_id - n.customevent_name = message.name - n.customevent_timestamp = message.timestamp - n.customevent_payload = message.payload - return n - - if isinstance(message, ErrorEvent): - n.errorevent_message = message.message - n.errorevent_messageid = message.message_id - n.errorevent_name = message.name - n.errorevent_payload = message.payload - n.errorevent_source = message.source - n.errorevent_timestamp = message.timestamp - return n - - if isinstance(message, JSException): - n.jsexception_name = message.name - n.jsexception_payload = message.payload - n.jsexception_message = message.message - return n - - if isinstance(message, Metadata): - n.metadata_key = message.key - n.metadata_value = message.value - return n - - if isinstance(message, MouseClick): - n.mouseclick_hesitationtime = message.hesitation_time - n.mouseclick_id = message.id - n.mouseclick_label = message.label - return n - - if isinstance(message, PageEvent): - n.pageevent_firstcontentfulpaint = message.first_contentful_paint - n.pageevent_firstpaint = message.first_paint - n.pageevent_messageid = message.message_id - n.pageevent_referrer = message.referrer - n.pageevent_speedindex = message.speed_index - n.pageevent_timestamp = message.timestamp - n.pageevent_url = message.url - return n - - if isinstance(message, PageRenderTiming): - n.pagerendertiming_timetointeractive = message.time_to_interactive - n.pagerendertiming_visuallycomplete = message.visually_complete - return n - - if isinstance(message, RawCustomEvent): - n.rawcustomevent_name = message.name - n.rawcustomevent_payload = message.payload - return n - - if isinstance(message, SetViewportSize): - n.setviewportsize_height = message.height - n.setviewportsize_width = message.width - return n - - if isinstance(message, Timestamp): - n.timestamp_timestamp = message.timestamp - return n - - if isinstance(message, UserAnonymousID): - n.user_anonymous_id = message.id - return n - - if isinstance(message, UserID): - n.user_id = message.id - return n - - if isinstance(message, IssueEvent): - n.issueevent_messageid = message.message_id - n.issueevent_timestamp = message.timestamp - n.issueevent_type = message.type - n.issueevent_contextstring = message.context_string - n.issueevent_context = message.context - n.issueevent_payload = message.payload - return n - - if isinstance(message, CustomIssue): - n.customissue_name = message.name - n.customissue_payload = message.payload - return n - - -def handle_session(n: Session, message: Message) -> Optional[Session]: - - if not n: - n = Session() - - if isinstance(message, SessionStart): - n.session_start_timestamp = message.timestamp - - n.user_uuid = message.user_uuid - n.user_agent = message.user_agent - n.user_os = message.user_os - n.user_os_version = message.user_os_version - n.user_browser = message.user_browser - n.user_browser_version = message.user_browser_version - n.user_device = message.user_device - n.user_device_type = message.user_device_type - n.user_device_memory_size = message.user_device_memory_size - n.user_device_heap_size = message.user_device_heap_size - n.user_country = message.user_country - return n - - if isinstance(message, SessionEnd): - n.session_end_timestamp = message.timestamp - try: - n.session_duration = n.session_end_timestamp - n.session_start_timestamp - except TypeError: - pass - return n - - if isinstance(message, ConnectionInformation): - n.connection_effective_bandwidth = message.downlink - n.connection_type = message.type - return n - - if isinstance(message, Metadata): - n.metadata_key = message.key - n.metadata_value = message.value - return n - - if isinstance(message, PageEvent): - n.referrer = message.referrer - n.first_contentful_paint = message.first_contentful_paint - n.speed_index = message.speed_index - n.timing_time_to_interactive = message.time_to_interactive - n.visually_complete = message.visually_complete - try: - n.urls_count += 1 - except TypeError: - n.urls_count = 1 - try: - n.urls.append(message.url) - except AttributeError: - n.urls = [message.url] - return n - - if isinstance(message, PerformanceTrackAggr): - n.avg_cpu = message.avg_cpu - n.avg_fps = message.avg_fps - n.max_cpu = message.max_cpu - n.max_fps = message.max_fps - n.max_total_js_heap_size = message.max_total_js_heap_size - n.max_used_js_heap_size = message.max_used_js_heap_size - return n - - if isinstance(message, UserID): - n.user_id = message.id - return n - - if isinstance(message, UserAnonymousID): - n.user_anonymous_id = message.id - return n - - if isinstance(message, JSException): - try: - n.js_exceptions_count += 1 - except TypeError: - n.js_exceptions_count = 1 - return n - - if isinstance(message, LongTask): - try: - n.long_tasks_total_duration += message.duration - except TypeError: - n.long_tasks_total_duration = message.duration - - try: - if n.long_tasks_max_duration > message.duration: - n.long_tasks_max_duration = message.duration - except TypeError: - n.long_tasks_max_duration = message.duration - - try: - n.long_tasks_count += 1 - except TypeError: - n.long_tasks_count = 1 - return n - - if isinstance(message, InputEvent): - try: - n.inputs_count += 1 - except TypeError: - n.inputs_count = 1 - return n - - if isinstance(message, MouseClick): - try: - n.inputs_count += 1 - except TypeError: - n.inputs_count = 1 - return n - - if isinstance(message, IssueEvent): - try: - n.issues_count += 1 - except TypeError: - n.issues_count = 1 - - - n.inputs_count = 1 - return n - - if isinstance(message, MouseClick): - try: - n.inputs_count += 1 - except TypeError: - n.inputs_count = 1 - return n - - if isinstance(message, IssueEvent): - try: - n.issues_count += 1 - except TypeError: - n.issues_count = 1 - - try: - n.issues.append(message.type) - except AttributeError: - n.issues = [message.type] - return n - - -def handle_message(message: Message) -> Optional[DetailedEvent]: - n = DetailedEvent() - - if isinstance(message, SessionEnd): - n.sessionend = True - n.sessionend_timestamp = message.timestamp - return n - - if isinstance(message, Timestamp): - n.timestamp_timestamp = message.timestamp - return n - - if isinstance(message, SessionDisconnect): - n.sessiondisconnect = True - n.sessiondisconnect_timestamp = message.timestamp - return n - - if isinstance(message, SessionStart): - n.sessionstart_trackerversion = message.tracker_version - n.sessionstart_revid = message.rev_id - n.sessionstart_timestamp = message.timestamp - n.sessionstart_useruuid = message.user_uuid - n.sessionstart_useragent = message.user_agent - n.sessionstart_useros = message.user_os - n.sessionstart_userosversion = message.user_os_version - n.sessionstart_userbrowser = message.user_browser - n.sessionstart_userbrowserversion = message.user_browser_version - n.sessionstart_userdevice = message.user_device - n.sessionstart_userdevicetype = message.user_device_type - n.sessionstart_userdevicememorysize = message.user_device_memory_size - n.sessionstart_userdeviceheapsize = message.user_device_heap_size - n.sessionstart_usercountry = message.user_country - return n - - if isinstance(message, SetViewportSize): - n.setviewportsize_width = message.width - n.setviewportsize_height = message.height - return n - - if isinstance(message, SetViewportScroll): - n.setviewportscroll_x = message.x - n.setviewportscroll_y = message.y - return n - - if isinstance(message, SetNodeScroll): - n.setnodescroll_id = message.id - n.setnodescroll_x = message.x - n.setnodescroll_y = message.y - return n - - if isinstance(message, ConsoleLog): - n.consolelog_level = message.level - n.consolelog_value = message.value - return n - - if isinstance(message, PageLoadTiming): - n.pageloadtiming_requeststart = message.request_start - n.pageloadtiming_responsestart = message.response_start - n.pageloadtiming_responseend = message.response_end - n.pageloadtiming_domcontentloadedeventstart = message.dom_content_loaded_event_start - n.pageloadtiming_domcontentloadedeventend = message.dom_content_loaded_event_end - n.pageloadtiming_loadeventstart = message.load_event_start - n.pageloadtiming_loadeventend = message.load_event_end - n.pageloadtiming_firstpaint = message.first_paint - n.pageloadtiming_firstcontentfulpaint = message.first_contentful_paint - return n - - if isinstance(message, PageRenderTiming): - n.pagerendertiming_speedindex = message.speed_index - n.pagerendertiming_visuallycomplete = message.visually_complete - n.pagerendertiming_timetointeractive = message.time_to_interactive - return n - - if isinstance(message, ResourceTiming): - n.resourcetiming_timestamp = message.timestamp - n.resourcetiming_duration = message.duration - n.resourcetiming_ttfb = message.ttfb - n.resourcetiming_headersize = message.header_size - n.resourcetiming_encodedbodysize = message.encoded_body_size - n.resourcetiming_decodedbodysize = message.decoded_body_size - n.resourcetiming_url = message.url - n.resourcetiming_initiator = message.initiator - return n - - if isinstance(message, JSException): - n.jsexception_name = message.name - n.jsexception_message = message.message - n.jsexception_payload = message.payload - return n - - if isinstance(message, RawErrorEvent): - n.rawerrorevent_timestamp = message.timestamp - n.rawerrorevent_source = message.source - n.rawerrorevent_name = message.name - n.rawerrorevent_message = message.message - n.rawerrorevent_payload = message.payload - return n - - if isinstance(message, RawCustomEvent): - n.rawcustomevent_name = message.name - n.rawcustomevent_payload = message.payload - return n - - if isinstance(message, UserID): - n.userid_id = message.id - return n - - if isinstance(message, UserAnonymousID): - n.useranonymousid_id = message.id - return n - - if isinstance(message, Metadata): - n.metadata_key = message.key - n.metadata_value = message.value - return n - - if isinstance(message, PerformanceTrack): - n.performancetrack_frames = message.frames - n.performancetrack_ticks = message.ticks - n.performancetrack_totaljsheapsize = message.total_js_heap_size - n.performancetrack_usedjsheapsize = message.used_js_heap_size - return n - - if isinstance(message, PerformanceTrackAggr): - n.performancetrackaggr_timestampstart = message.timestamp_start - n.performancetrackaggr_timestampend = message.timestamp_end - n.performancetrackaggr_minfps = message.min_fps - n.performancetrackaggr_avgfps = message.avg_fps - n.performancetrackaggr_maxfps = message.max_fps - n.performancetrackaggr_mincpu = message.min_cpu - n.performancetrackaggr_avgcpu = message.avg_cpu - n.performancetrackaggr_maxcpu = message.max_cpu - n.performancetrackaggr_mintotaljsheapsize = message.min_total_js_heap_size - n.performancetrackaggr_avgtotaljsheapsize = message.avg_total_js_heap_size - n.performancetrackaggr_maxtotaljsheapsize = message.max_total_js_heap_size - n.performancetrackaggr_minusedjsheapsize = message.min_used_js_heap_size - n.performancetrackaggr_avgusedjsheapsize = message.avg_used_js_heap_size - n.performancetrackaggr_maxusedjsheapsize = message.max_used_js_heap_size - return n - - if isinstance(message, ConnectionInformation): - n.connectioninformation_downlink = message.downlink - n.connectioninformation_type = message.type - return n - - if isinstance(message, PageEvent): - n.pageevent_messageid = message.message_id - n.pageevent_timestamp = message.timestamp - n.pageevent_url = message.url - n.pageevent_referrer = message.referrer - n.pageevent_loaded = message.loaded - n.pageevent_requeststart = message.request_start - n.pageevent_responsestart = message.response_start - n.pageevent_responseend = message.response_end - n.pageevent_domcontentloadedeventstart = message.dom_content_loaded_event_start - n.pageevent_domcontentloadedeventend = message.dom_content_loaded_event_end - n.pageevent_loadeventstart = message.load_event_start - n.pageevent_loadeventend = message.load_event_end - n.pageevent_firstpaint = message.first_paint - n.pageevent_firstcontentfulpaint = message.first_contentful_paint - n.pageevent_speedindex = message.speed_index - return n - - if isinstance(message, InputEvent): - n.inputevent_messageid = message.message_id - n.inputevent_timestamp = message.timestamp - n.inputevent_value = message.value - n.inputevent_valuemasked = message.value_masked - n.inputevent_label = message.label - return n - - if isinstance(message, ClickEvent): - n.clickevent_messageid = message.message_id - n.clickevent_timestamp = message.timestamp - n.clickevent_hesitationtime = message.hesitation_time - n.clickevent_label = message.label - return n - - if isinstance(message, ErrorEvent): - n.errorevent_messageid = message.message_id - n.errorevent_timestamp = message.timestamp - n.errorevent_source = message.source - n.errorevent_name = message.name - n.errorevent_message = message.message - n.errorevent_payload = message.payload - return n - - if isinstance(message, ResourceEvent): - n.resourceevent_messageid = message.message_id - n.resourceevent_timestamp = message.timestamp - n.resourceevent_duration = message.duration - n.resourceevent_ttfb = message.ttfb - n.resourceevent_headersize = message.header_size - n.resourceevent_encodedbodysize = message.encoded_body_size - n.resourceevent_decodedbodysize = message.decoded_body_size - n.resourceevent_url = message.url - n.resourceevent_type = message.type - n.resourceevent_success = message.success - n.resourceevent_method = message.method - n.resourceevent_status = message.status - return n - - if isinstance(message, CustomEvent): - n.customevent_messageid = message.message_id - n.customevent_timestamp = message.timestamp - n.customevent_name = message.name - n.customevent_payload = message.payload - return n - - # if isinstance(message, CreateDocument): - # n.createdocument = True - # return n - # - # if isinstance(message, CreateElementNode): - # n.createelementnode_id = message.id - # if isinstance(message.parent_id, tuple): - # n.createelementnode_parentid = message.parent_id[0] - # else: - # n.createelementnode_parentid = message.parent_id - # return n - - # if isinstance(message, CSSInsertRule): - # n.cssinsertrule_stylesheetid = message.id - # n.cssinsertrule_rule = message.rule - # n.cssinsertrule_index = message.index - # return n - # - # if isinstance(message, CSSDeleteRule): - # n.cssdeleterule_stylesheetid = message.id - # n.cssdeleterule_index = message.index - # return n - - if isinstance(message, Fetch): - n.fetch_method = message.method - n.fetch_url = message.url - n.fetch_request = message.request - n.fetch_status = message.status - n.fetch_timestamp = message.timestamp - n.fetch_duration = message.duration - return n - - if isinstance(message, Profiler): - n.profiler_name = message.name - n.profiler_duration = message.duration - n.profiler_args = message.args - n.profiler_result = message.result - return n - - if isinstance(message, GraphQL): - n.graphql_operationkind = message.operation_kind - n.graphql_operationname = message.operation_name - n.graphql_variables = message.variables - n.graphql_response = message.response - return n - - if isinstance(message, GraphQLEvent): - n.graphqlevent_messageid = message.message_id - n.graphqlevent_timestamp = message.timestamp - n.graphqlevent_name = message.name - return n - - if isinstance(message, DomDrop): - n.domdrop_timestamp = message.timestamp - return n - - if isinstance(message, MouseClick): - n.mouseclick_id = message.id - n.mouseclick_hesitationtime = message.hesitation_time - n.mouseclick_label = message.label - return n - - if isinstance(message, SetPageLocation): - n.setpagelocation_url = message.url - n.setpagelocation_referrer = message.referrer - n.setpagelocation_navigationstart = message.navigation_start - return n - - if isinstance(message, MouseMove): - n.mousemove_x = message.x - n.mousemove_y = message.y - return n - - if isinstance(message, LongTask): - n.longtasks_timestamp = message.timestamp - n.longtasks_duration = message.duration - n.longtask_context = message.context - n.longtask_containertype = message.container_type - n.longtasks_containersrc = message.container_src - n.longtasks_containerid = message.container_id - n.longtasks_containername = message.container_name - return n - - if isinstance(message, SetNodeURLBasedAttribute): - n.setnodeurlbasedattribute_id = message.id - n.setnodeurlbasedattribute_name = message.name - n.setnodeurlbasedattribute_value = message.value - n.setnodeurlbasedattribute_baseurl = message.base_url - return n - - if isinstance(message, SetStyleData): - n.setstyledata_id = message.id - n.setstyledata_data = message.data - n.setstyledata_baseurl = message.base_url - return n - - if isinstance(message, IssueEvent): - n.issueevent_messageid = message.message_id - n.issueevent_timestamp = message.timestamp - n.issueevent_type = message.type - n.issueevent_contextstring = message.context_string - n.issueevent_context = message.context - n.issueevent_payload = message.payload - return n - - if isinstance(message, TechnicalInfo): - n.technicalinfo_type = message.type - n.technicalinfo_value = message.value - return n - - if isinstance(message, CustomIssue): - n.customissue_name = message.name - n.customissue_payload = message.payload - return n - - if isinstance(message, PageClose): - n.pageclose = True - return n - - if isinstance(message, IOSSessionStart): - n.iossessionstart_timestamp = message.timestamp - n.iossessionstart_projectid = message.project_id - n.iossessionstart_trackerversion = message.tracker_version - n.iossessionstart_revid = message.rev_id - n.iossessionstart_useruuid = message.user_uuid - n.iossessionstart_useros = message.user_os - n.iossessionstart_userosversion = message.user_os_version - n.iossessionstart_userdevice = message.user_device - n.iossessionstart_userdevicetype = message.user_device_type - n.iossessionstart_usercountry = message.user_country - return n - - if isinstance(message, IOSSessionEnd): - n.iossessionend_timestamp = message.timestamp - return n - - if isinstance(message, IOSMetadata): - n.iosmetadata_timestamp = message.timestamp - n.iosmetadata_length = message.length - n.iosmetadata_key = message.key - n.iosmetadata_value = message.value - return n - - if isinstance(message, IOSUserID): - n.iosuserid_timestamp = message.timestamp - n.iosuserid_length = message.length - n.iosuserid_value = message.value - return n - - if isinstance(message, IOSUserAnonymousID): - n.iosuseranonymousid_timestamp = message.timestamp - n.iosuseranonymousid_length = message.length - n.iosuseranonymousid_value = message.value - return n - - if isinstance(message, IOSScreenLeave): - n.iosscreenleave_timestamp = message.timestamp - n.iosscreenleave_length = message.length - n.iosscreenleave_title = message.title - n.iosscreenleave_viewname = message.view_name - return n - - if isinstance(message, IOSLog): - n.ioslog_timestamp = message.timestamp - n.ioslog_length = message.length - n.ioslog_severity = message.severity - n.ioslog_content = message.content - return n - - if isinstance(message, IOSInternalError): - n.iosinternalerror_timestamp = message.timestamp - n.iosinternalerror_length = message.length - n.iosinternalerror_content = message.content - return n - - if isinstance(message, IOSPerformanceAggregated): - n.iosperformanceaggregated_timestampstart = message.timestamp_start - n.iosperformanceaggregated_timestampend = message.timestamp_end - n.iosperformanceaggregated_minfps = message.min_fps - n.iosperformanceaggregated_avgfps = message.avg_fps - n.iosperformanceaggregated_maxfps = message.max_fps - n.iosperformanceaggregated_mincpu = message.min_cpu - n.iosperformanceaggregated_avgcpu = message.avg_cpu - n.iosperformanceaggregated_maxcpu = message.max_cpu - n.iosperformanceaggregated_minmemory = message.min_memory - n.iosperformanceaggregated_avgmemory = message.avg_memory - n.iosperformanceaggregated_maxmemory = message.max_memory - n.iosperformanceaggregated_minbattery = message.min_battery - n.iosperformanceaggregated_avgbattery = message.avg_battery - n.iosperformanceaggregated_maxbattery = message.max_battery - return n - return None diff --git a/ee/connectors/main.py b/ee/connectors/main.py deleted file mode 100644 index 57349f6e9..000000000 --- a/ee/connectors/main.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from kafka import KafkaConsumer -from datetime import datetime -from collections import defaultdict - -from msgcodec.codec import MessageCodec -from msgcodec.messages import SessionEnd -from db.api import DBConnection -from db.models import events_detailed_table_name, events_table_name, sessions_table_name, conf -from db.writer import insert_batch -from handler import handle_message, handle_normal_message, handle_session - -DATABASE = os.environ['DATABASE_NAME'] -LEVEL = conf[DATABASE]['level'] - -db = DBConnection(DATABASE) - -if LEVEL == 'detailed': - table_name = events_detailed_table_name -elif LEVEL == 'normal': - table_name = events_table_name - - -def main(): - batch_size = 4000 - sessions_batch_size = 400 - batch = [] - sessions = defaultdict(lambda: None) - sessions_batch = [] - - codec = MessageCodec() - consumer = KafkaConsumer(security_protocol="SSL", - bootstrap_servers=[os.environ['KAFKA_SERVER_1'], - os.environ['KAFKA_SERVER_2']], - group_id=f"connector_{DATABASE}", - auto_offset_reset="earliest", - enable_auto_commit=False) - - consumer.subscribe(topics=["events", "messages"]) - print("Kafka consumer subscribed") - for msg in consumer: - message = codec.decode(msg.value) - if message is None: - print('-') - continue - - if LEVEL == 'detailed': - n = handle_message(message) - elif LEVEL == 'normal': - n = handle_normal_message(message) - - session_id = codec.decode_key(msg.key) - sessions[session_id] = handle_session(sessions[session_id], message) - if sessions[session_id]: - sessions[session_id].sessionid = session_id - - # put in a batch for insertion if received a SessionEnd - if isinstance(message, SessionEnd): - if sessions[session_id]: - sessions_batch.append(sessions[session_id]) - - # try to insert sessions - if len(sessions_batch) >= sessions_batch_size: - attempt_session_insert(sessions_batch) - for s in sessions_batch: - try: - del sessions[s.sessionid] - except KeyError as e: - print(repr(e)) - sessions_batch = [] - - if n: - n.sessionid = session_id - n.received_at = int(datetime.now().timestamp() * 1000) - n.batch_order_number = len(batch) - batch.append(n) - else: - continue - - # insert a batch of events - if len(batch) >= batch_size: - attempt_batch_insert(batch) - batch = [] - consumer.commit() - print("sessions in cache:", len(sessions)) - - -def attempt_session_insert(sess_batch): - if sess_batch: - try: - print("inserting sessions...") - insert_batch(db, sess_batch, table=sessions_table_name, level='sessions') - print("inserted sessions succesfully") - except TypeError as e: - print("Type conversion error") - print(repr(e)) - except ValueError as e: - print("Message value could not be processed or inserted correctly") - print(repr(e)) - except Exception as e: - print(repr(e)) - - -def attempt_batch_insert(batch): - # insert a batch - try: - print("inserting...") - insert_batch(db=db, batch=batch, table=table_name, level=LEVEL) - print("inserted succesfully") - except TypeError as e: - print("Type conversion error") - print(repr(e)) - except ValueError as e: - print("Message value could not be processed or inserted correctly") - print(repr(e)) - except Exception as e: - print(repr(e)) - - -if __name__ == '__main__': - main() diff --git a/ee/connectors/msgcodec/codec.py b/ee/connectors/msgcodec/codec.py deleted file mode 100644 index 18f074a33..000000000 --- a/ee/connectors/msgcodec/codec.py +++ /dev/null @@ -1,670 +0,0 @@ -import io - -from msgcodec.messages import * - - -class Codec: - """ - Implements encode/decode primitives - """ - - @staticmethod - def read_boolean(reader: io.BytesIO): - b = reader.read(1) - return b == 1 - - @staticmethod - def read_uint(reader: io.BytesIO): - """ - The ending "big" doesn't play any role here, - since we're dealing with data per one byte - """ - x = 0 # the result - s = 0 # the shift (our result is big-ending) - i = 0 # n of byte (max 9 for uint64) - while True: - b = reader.read(1) - num = int.from_bytes(b, "big", signed=False) - # print(i, x) - - if num < 0x80: - if i > 9 | i == 9 & num > 1: - raise OverflowError() - return int(x | num << s) - x |= (num & 0x7f) << s - s += 7 - i += 1 - - @staticmethod - def read_int(reader: io.BytesIO) -> int: - """ - ux, err := ReadUint(reader) - x := int64(ux >> 1) - if err != nil { - return x, err - } - if ux&1 != 0 { - x = ^x - } - return x, err - """ - ux = Codec.read_uint(reader) - x = int(ux >> 1) - - if ux & 1 != 0: - x = - x - 1 - return x - - @staticmethod - def read_string(reader: io.BytesIO) -> str: - length = Codec.read_uint(reader) - s = reader.read(length) - try: - return s.decode("utf-8", errors="replace").replace("\x00", "\uFFFD") - except UnicodeDecodeError: - return None - - -class MessageCodec(Codec): - - def encode(self, m: Message) -> bytes: - ... - - def decode(self, b: bytes) -> Message: - reader = io.BytesIO(b) - message_id = self.read_message_id(reader) - - if message_id == 0: - return Timestamp( - timestamp=self.read_uint(reader) - ) - if message_id == 1: - return SessionStart( - timestamp=self.read_uint(reader), - project_id=self.read_uint(reader), - tracker_version=self.read_string(reader), - rev_id=self.read_string(reader), - user_uuid=self.read_string(reader), - user_agent=self.read_string(reader), - user_os=self.read_string(reader), - user_os_version=self.read_string(reader), - user_browser=self.read_string(reader), - user_browser_version=self.read_string(reader), - user_device=self.read_string(reader), - user_device_type=self.read_string(reader), - user_device_memory_size=self.read_uint(reader), - user_device_heap_size=self.read_uint(reader), - user_country=self.read_string(reader) - ) - - if message_id == 2: - return SessionDisconnect( - timestamp=self.read_uint(reader) - ) - - if message_id == 3: - return SessionEnd( - timestamp=self.read_uint(reader) - ) - - if message_id == 4: - return SetPageLocation( - url=self.read_string(reader), - referrer=self.read_string(reader), - navigation_start=self.read_uint(reader) - ) - - if message_id == 5: - return SetViewportSize( - width=self.read_uint(reader), - height=self.read_uint(reader) - ) - - if message_id == 6: - return SetViewportScroll( - x=self.read_int(reader), - y=self.read_int(reader) - ) - - if message_id == 7: - return CreateDocument() - - if message_id == 8: - return CreateElementNode( - id=self.read_uint(reader), - parent_id=self.read_uint(reader), - index=self.read_uint(reader), - tag=self.read_string(reader), - svg=self.read_boolean(reader), - ) - - if message_id == 9: - return CreateTextNode( - id=self.read_uint(reader), - parent_id=self.read_uint(reader), - index=self.read_uint(reader) - ) - - if message_id == 10: - return MoveNode( - id=self.read_uint(reader), - parent_id=self.read_uint(reader), - index=self.read_uint(reader) - ) - - if message_id == 11: - return RemoveNode( - id=self.read_uint(reader) - ) - - if message_id == 12: - return SetNodeAttribute( - id=self.read_uint(reader), - name=self.read_string(reader), - value=self.read_string(reader) - ) - - if message_id == 13: - return RemoveNodeAttribute( - id=self.read_uint(reader), - name=self.read_string(reader) - ) - - if message_id == 14: - return SetNodeData( - id=self.read_uint(reader), - data=self.read_string(reader) - ) - - if message_id == 15: - return SetCSSData( - id=self.read_uint(reader), - data=self.read_string(reader) - ) - - if message_id == 16: - return SetNodeScroll( - id=self.read_uint(reader), - x=self.read_int(reader), - y=self.read_int(reader), - ) - - if message_id == 17: - return SetInputTarget( - id=self.read_uint(reader), - label=self.read_string(reader) - ) - - if message_id == 18: - return SetInputValue( - id=self.read_uint(reader), - value=self.read_string(reader), - mask=self.read_int(reader), - ) - - if message_id == 19: - return SetInputChecked( - id=self.read_uint(reader), - checked=self.read_boolean(reader) - ) - - if message_id == 20: - return MouseMove( - x=self.read_uint(reader), - y=self.read_uint(reader) - ) - - if message_id == 21: - return MouseClick( - id=self.read_uint(reader), - hesitation_time=self.read_uint(reader), - label=self.read_string(reader) - ) - - if message_id == 22: - return ConsoleLog( - level=self.read_string(reader), - value=self.read_string(reader) - ) - - if message_id == 23: - return PageLoadTiming( - request_start=self.read_uint(reader), - response_start=self.read_uint(reader), - response_end=self.read_uint(reader), - dom_content_loaded_event_start=self.read_uint(reader), - dom_content_loaded_event_end=self.read_uint(reader), - load_event_start=self.read_uint(reader), - load_event_end=self.read_uint(reader), - first_paint=self.read_uint(reader), - first_contentful_paint=self.read_uint(reader) - ) - - if message_id == 24: - return PageRenderTiming( - speed_index=self.read_uint(reader), - visually_complete=self.read_uint(reader), - time_to_interactive=self.read_uint(reader), - ) - - if message_id == 25: - return JSException( - name=self.read_string(reader), - message=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 26: - return RawErrorEvent( - timestamp=self.read_uint(reader), - source=self.read_string(reader), - name=self.read_string(reader), - message=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 27: - return RawCustomEvent( - name=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 28: - return UserID( - id=self.read_string(reader) - ) - - if message_id == 29: - return UserAnonymousID( - id=self.read_string(reader) - ) - - if message_id == 30: - return Metadata( - key=self.read_string(reader), - value=self.read_string(reader) - ) - - if message_id == 31: - return PageEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - url=self.read_string(reader), - referrer=self.read_string(reader), - loaded=self.read_boolean(reader), - request_start=self.read_uint(reader), - response_start=self.read_uint(reader), - response_end=self.read_uint(reader), - dom_content_loaded_event_start=self.read_uint(reader), - dom_content_loaded_event_end=self.read_uint(reader), - load_event_start=self.read_uint(reader), - load_event_end=self.read_uint(reader), - first_paint=self.read_uint(reader), - first_contentful_paint=self.read_uint(reader), - speed_index=self.read_uint(reader), - visually_complete=self.read_uint(reader), - time_to_interactive=self.read_uint(reader) - ) - - if message_id == 32: - return InputEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - value=self.read_string(reader), - value_masked=self.read_boolean(reader), - label=self.read_string(reader), - ) - - if message_id == 33: - return ClickEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - hesitation_time=self.read_uint(reader), - label=self.read_string(reader) - ) - - if message_id == 34: - return ErrorEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - source=self.read_string(reader), - name=self.read_string(reader), - message=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 35: - - message_id = self.read_uint(reader) - ts = self.read_uint(reader) - if ts > 9999999999999: - ts = None - return ResourceEvent( - message_id=message_id, - timestamp=ts, - duration=self.read_uint(reader), - ttfb=self.read_uint(reader), - header_size=self.read_uint(reader), - encoded_body_size=self.read_uint(reader), - decoded_body_size=self.read_uint(reader), - url=self.read_string(reader), - type=self.read_string(reader), - success=self.read_boolean(reader), - method=self.read_string(reader), - status=self.read_uint(reader) - ) - - if message_id == 36: - return CustomEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - name=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 37: - return CSSInsertRule( - id=self.read_uint(reader), - rule=self.read_string(reader), - index=self.read_uint(reader) - ) - - if message_id == 38: - return CSSDeleteRule( - id=self.read_uint(reader), - index=self.read_uint(reader) - ) - - if message_id == 39: - return Fetch( - method=self.read_string(reader), - url=self.read_string(reader), - request=self.read_string(reader), - response=self.read_string(reader), - status=self.read_uint(reader), - timestamp=self.read_uint(reader), - duration=self.read_uint(reader) - ) - - if message_id == 40: - return Profiler( - name=self.read_string(reader), - duration=self.read_uint(reader), - args=self.read_string(reader), - result=self.read_string(reader) - ) - - if message_id == 41: - return OTable( - key=self.read_string(reader), - value=self.read_string(reader) - ) - - if message_id == 42: - return StateAction( - type=self.read_string(reader) - ) - - if message_id == 43: - return StateActionEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - type=self.read_string(reader) - ) - - if message_id == 44: - return Redux( - action=self.read_string(reader), - state=self.read_string(reader), - duration=self.read_uint(reader) - ) - - if message_id == 45: - return Vuex( - mutation=self.read_string(reader), - state=self.read_string(reader), - ) - - if message_id == 46: - return MobX( - type=self.read_string(reader), - payload=self.read_string(reader), - ) - - if message_id == 47: - return NgRx( - action=self.read_string(reader), - state=self.read_string(reader), - duration=self.read_uint(reader) - ) - - if message_id == 48: - return GraphQL( - operation_kind=self.read_string(reader), - operation_name=self.read_string(reader), - variables=self.read_string(reader), - response=self.read_string(reader) - ) - - if message_id == 49: - return PerformanceTrack( - frames=self.read_int(reader), - ticks=self.read_int(reader), - total_js_heap_size=self.read_uint(reader), - used_js_heap_size=self.read_uint(reader) - ) - - if message_id == 50: - return GraphQLEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - name=self.read_string(reader) - ) - - if message_id == 52: - return DomDrop( - timestamp=self.read_uint(reader) - ) - - if message_id == 53: - return ResourceTiming( - timestamp=self.read_uint(reader), - duration=self.read_uint(reader), - ttfb=self.read_uint(reader), - header_size=self.read_uint(reader), - encoded_body_size=self.read_uint(reader), - decoded_body_size=self.read_uint(reader), - url=self.read_string(reader), - initiator=self.read_string(reader) - ) - - if message_id == 54: - return ConnectionInformation( - downlink=self.read_uint(reader), - type=self.read_string(reader) - ) - - if message_id == 55: - return SetPageVisibility( - hidden=self.read_boolean(reader) - ) - - if message_id == 56: - return PerformanceTrackAggr( - timestamp_start=self.read_uint(reader), - timestamp_end=self.read_uint(reader), - min_fps=self.read_uint(reader), - avg_fps=self.read_uint(reader), - max_fps=self.read_uint(reader), - min_cpu=self.read_uint(reader), - avg_cpu=self.read_uint(reader), - max_cpu=self.read_uint(reader), - min_total_js_heap_size=self.read_uint(reader), - avg_total_js_heap_size=self.read_uint(reader), - max_total_js_heap_size=self.read_uint(reader), - min_used_js_heap_size=self.read_uint(reader), - avg_used_js_heap_size=self.read_uint(reader), - max_used_js_heap_size=self.read_uint(reader) - ) - - if message_id == 59: - return LongTask( - timestamp=self.read_uint(reader), - duration=self.read_uint(reader), - context=self.read_uint(reader), - container_type=self.read_uint(reader), - container_src=self.read_string(reader), - container_id=self.read_string(reader), - container_name=self.read_string(reader) - ) - - if message_id == 60: - return SetNodeURLBasedAttribute( - id=self.read_uint(reader), - name=self.read_string(reader), - value=self.read_string(reader), - base_url=self.read_string(reader) - ) - - if message_id == 61: - return SetStyleData( - id=self.read_uint(reader), - data=self.read_string(reader), - base_url=self.read_string(reader) - ) - - if message_id == 62: - return IssueEvent( - message_id=self.read_uint(reader), - timestamp=self.read_uint(reader), - type=self.read_string(reader), - context_string=self.read_string(reader), - context=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 63: - return TechnicalInfo( - type=self.read_string(reader), - value=self.read_string(reader) - ) - - if message_id == 64: - return CustomIssue( - name=self.read_string(reader), - payload=self.read_string(reader) - ) - - if message_id == 65: - return PageClose() - - if message_id == 90: - return IOSSessionStart( - timestamp=self.read_uint(reader), - project_id=self.read_uint(reader), - tracker_version=self.read_string(reader), - rev_id=self.read_string(reader), - user_uuid=self.read_string(reader), - user_os=self.read_string(reader), - user_os_version=self.read_string(reader), - user_device=self.read_string(reader), - user_device_type=self.read_string(reader), - user_country=self.read_string(reader) - ) - - if message_id == 91: - return IOSSessionEnd( - timestamp=self.read_uint(reader) - ) - - if message_id == 92: - return IOSMetadata( - timestamp=self.read_uint(reader), - length=self.read_uint(reader), - key=self.read_string(reader), - value=self.read_string(reader) - ) - - if message_id == 94: - return IOSUserID( - timestamp=self.read_uint(reader), - length=self.read_uint(reader), - value=self.read_string(reader) - ) - - if message_id == 95: - return IOSUserAnonymousID( - timestamp=self.read_uint(reader), - length=self.read_uint(reader), - value=self.read_string(reader) - ) - - if message_id == 99: - return IOSScreenLeave( - timestamp=self.read_uint(reader), - length=self.read_uint(reader), - title=self.read_string(reader), - view_name=self.read_string(reader) - ) - - if message_id == 103: - return IOSLog( - timestamp=self.read_uint(reader), - length=self.read_uint(reader), - severity=self.read_string(reader), - content=self.read_string(reader) - ) - - if message_id == 104: - return IOSInternalError( - timestamp=self.read_uint(reader), - length=self.read_uint(reader), - content=self.read_string(reader) - ) - - if message_id == 110: - return IOSPerformanceAggregated( - timestamp_start=self.read_uint(reader), - timestamp_end=self.read_uint(reader), - min_fps=self.read_uint(reader), - avg_fps=self.read_uint(reader), - max_fps=self.read_uint(reader), - min_cpu=self.read_uint(reader), - avg_cpu=self.read_uint(reader), - max_cpu=self.read_uint(reader), - min_memory=self.read_uint(reader), - avg_memory=self.read_uint(reader), - max_memory=self.read_uint(reader), - min_battery=self.read_uint(reader), - avg_battery=self.read_uint(reader), - max_battery=self.read_uint(reader) - ) - - def read_message_id(self, reader: io.BytesIO) -> int: - """ - Read and return the first byte where the message id is encoded - """ - id_ = self.read_uint(reader) - return id_ - - @staticmethod - def check_message_id(b: bytes) -> int: - """ - todo: make it static and without reader. It's just the first byte - Read and return the first byte where the message id is encoded - """ - reader = io.BytesIO(b) - id_ = Codec.read_uint(reader) - - return id_ - - @staticmethod - def decode_key(b) -> int: - """ - Decode the message key (encoded with little endian) - """ - try: - decoded = int.from_bytes(b, "little", signed=False) - except Exception as e: - raise UnicodeDecodeError(f"Error while decoding message key (SessionID) from {b}\n{e}") - return decoded diff --git a/ee/connectors/msgcodec/messages.py b/ee/connectors/msgcodec/messages.py deleted file mode 100644 index c6e53b445..000000000 --- a/ee/connectors/msgcodec/messages.py +++ /dev/null @@ -1,752 +0,0 @@ -""" -Representations of Kafka messages -""" -from abc import ABC - - -class Message(ABC): - pass - - -class Timestamp(Message): - __id__ = 0 - - def __init__(self, timestamp): - self.timestamp = timestamp - - -class SessionStart(Message): - __id__ = 1 - - def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid, - user_agent, user_os, user_os_version, user_browser, user_browser_version, - user_device, user_device_type, user_device_memory_size, user_device_heap_size, - user_country): - self.timestamp = timestamp - self.project_id = project_id - self.tracker_version = tracker_version - self.rev_id = rev_id - self.user_uuid = user_uuid - self.user_agent = user_agent - self.user_os = user_os - self.user_os_version = user_os_version - self.user_browser = user_browser - self.user_browser_version = user_browser_version - self.user_device = user_device - self.user_device_type = user_device_type - self.user_device_memory_size = user_device_memory_size - self.user_device_heap_size = user_device_heap_size - self.user_country = user_country - - -class SessionDisconnect(Message): - __id__ = 2 - - def __init__(self, timestamp): - self.timestamp = timestamp - - -class SessionEnd(Message): - __id__ = 3 - __name__ = 'SessionEnd' - - def __init__(self, timestamp): - self.timestamp = timestamp - - -class SetPageLocation(Message): - __id__ = 4 - - def __init__(self, url, referrer, navigation_start): - self.url = url - self.referrer = referrer - self.navigation_start = navigation_start - - -class SetViewportSize(Message): - __id__ = 5 - - def __init__(self, width, height): - self.width = width - self.height = height - - -class SetViewportScroll(Message): - __id__ = 6 - - def __init__(self, x, y): - self.x = x - self.y = y - - -class CreateDocument(Message): - __id__ = 7 - - -class CreateElementNode(Message): - __id__ = 8 - - def __init__(self, id, parent_id, index, tag, svg): - self.id = id - self.parent_id = parent_id, - self.index = index - self.tag = tag - self.svg = svg - - -class CreateTextNode(Message): - __id__ = 9 - - def __init__(self, id, parent_id, index): - self.id = id - self.parent_id = parent_id - self.index = index - - -class MoveNode(Message): - __id__ = 10 - - def __init__(self, id, parent_id, index): - self.id = id - self.parent_id = parent_id - self.index = index - - -class RemoveNode(Message): - __id__ = 11 - - def __init__(self, id): - self.id = id - - -class SetNodeAttribute(Message): - __id__ = 12 - - def __init__(self, id, name: str, value: str): - self.id = id - self.name = name - self.value = value - - -class RemoveNodeAttribute(Message): - __id__ = 13 - - def __init__(self, id, name: str): - self.id = id - self.name = name - - -class SetNodeData(Message): - __id__ = 14 - - def __init__(self, id, data: str): - self.id = id - self.data = data - - -class SetCSSData(Message): - __id__ = 15 - - def __init__(self, id, data: str): - self.id = id - self.data = data - - -class SetNodeScroll(Message): - __id__ = 16 - - def __init__(self, id, x: int, y: int): - self.id = id - self.x = x - self.y = y - - -class SetInputTarget(Message): - __id__ = 17 - - def __init__(self, id, label: str): - self.id = id - self.label = label - - -class SetInputValue(Message): - __id__ = 18 - - def __init__(self, id, value: str, mask: int): - self.id = id - self.value = value - self.mask = mask - - -class SetInputChecked(Message): - __id__ = 19 - - def __init__(self, id, checked: bool): - self.id = id - self.checked = checked - - -class MouseMove(Message): - __id__ = 20 - - def __init__(self, x, y): - self.x = x - self.y = y - - -class MouseClick(Message): - __id__ = 21 - - def __init__(self, id, hesitation_time, label: str): - self.id = id - self.hesitation_time = hesitation_time - self.label = label - - -class ConsoleLog(Message): - __id__ = 22 - - def __init__(self, level: str, value: str): - self.level = level - self.value = value - - -class PageLoadTiming(Message): - __id__ = 23 - - def __init__(self, request_start, response_start, response_end, dom_content_loaded_event_start, - dom_content_loaded_event_end, load_event_start, load_event_end, - first_paint, first_contentful_paint): - self.request_start = request_start - self.response_start = response_start - self.response_end = response_end - self.dom_content_loaded_event_start = dom_content_loaded_event_start - self.dom_content_loaded_event_end = dom_content_loaded_event_end - self.load_event_start = load_event_start - self.load_event_end = load_event_end - self.first_paint = first_paint - self.first_contentful_paint = first_contentful_paint - - -class PageRenderTiming(Message): - __id__ = 24 - - def __init__(self, speed_index, visually_complete, time_to_interactive): - self.speed_index = speed_index - self.visually_complete = visually_complete - self.time_to_interactive = time_to_interactive - -class JSException(Message): - __id__ = 25 - - def __init__(self, name: str, message: str, payload: str): - self.name = name - self.message = message - self.payload = payload - - -class RawErrorEvent(Message): - __id__ = 26 - - def __init__(self, timestamp, source: str, name: str, message: str, - payload: str): - self.timestamp = timestamp - self.source = source - self.name = name - self.message = message - self.payload = payload - - -class RawCustomEvent(Message): - __id__ = 27 - - def __init__(self, name: str, payload: str): - self.name = name - self.payload = payload - - -class UserID(Message): - __id__ = 28 - - def __init__(self, id: str): - self.id = id - - -class UserAnonymousID(Message): - __id__ = 29 - - def __init__(self, id: str): - self.id = id - - -class Metadata(Message): - __id__ = 30 - - def __init__(self, key: str, value: str): - self.key = key - self.value = value - - -class PerformanceTrack(Message): - __id__ = 49 - - def __init__(self, frames: int, ticks: int, total_js_heap_size, - used_js_heap_size): - self.frames = frames - self.ticks = ticks - self.total_js_heap_size = total_js_heap_size - self.used_js_heap_size = used_js_heap_size - - -class PageEvent(Message): - __id__ = 31 - - def __init__(self, message_id, timestamp, url: str, referrer: str, - loaded: bool, request_start, response_start, response_end, - dom_content_loaded_event_start, dom_content_loaded_event_end, - load_event_start, load_event_end, first_paint, first_contentful_paint, - speed_index, visually_complete, time_to_interactive): - self.message_id = message_id - self.timestamp = timestamp - self.url = url - self.referrer = referrer - self.loaded = loaded - self.request_start = request_start - self.response_start = response_start - self.response_end = response_end - self.dom_content_loaded_event_start = dom_content_loaded_event_start - self.dom_content_loaded_event_end = dom_content_loaded_event_end - self.load_event_start = load_event_start - self.load_event_end = load_event_end - self.first_paint = first_paint - self.first_contentful_paint = first_contentful_paint - self.speed_index = speed_index - self.visually_complete = visually_complete - self.time_to_interactive = time_to_interactive - - -class InputEvent(Message): - __id__ = 32 - - def __init__(self, message_id, timestamp, value: str, value_masked: bool, label: str): - self.message_id = message_id - self.timestamp = timestamp - self.value = value - self.value_masked = value_masked - self.label = label - - -class ClickEvent(Message): - __id__ = 33 - - def __init__(self, message_id, timestamp, hesitation_time, label: str): - self.message_id = message_id - self.timestamp = timestamp - self.hesitation_time = hesitation_time - self.label = label - - -class ErrorEvent(Message): - __id__ = 34 - - def __init__(self, message_id, timestamp, source: str, name: str, message: str, - payload: str): - self.message_id = message_id - self.timestamp = timestamp - self.source = source - self.name = name - self.message = message - self.payload = payload - - -class ResourceEvent(Message): - __id__ = 35 - - def __init__(self, message_id, timestamp, duration, ttfb, header_size, encoded_body_size, - decoded_body_size, url: str, type: str, success: bool, method: str, status): - self.message_id = message_id - self.timestamp = timestamp - self.duration = duration - self.ttfb = ttfb - self.header_size = header_size - self.encoded_body_size = encoded_body_size - self.decoded_body_size = decoded_body_size - self.url = url - self.type = type - self.success = success - self.method = method - self.status = status - - -class CustomEvent(Message): - __id__ = 36 - - def __init__(self, message_id, timestamp, name: str, payload: str): - self.message_id = message_id - self.timestamp = timestamp - self.name = name - self.payload = payload - - -class CSSInsertRule(Message): - __id__ = 37 - - def __init__(self, id, rule: str, index): - self.id = id - self.rule = rule - self.index = index - - -class CSSDeleteRule(Message): - __id__ = 38 - - def __init__(self, id, index): - self.id = id - self.index = index - - -class Fetch(Message): - __id__ = 39 - - def __init__(self, method: str, url: str, request: str, response: str, status, - timestamp, duration): - self.method = method - self.url = url - self.request = request - self.response = response - self.status = status - self.timestamp = timestamp - self.duration = duration - - -class Profiler(Message): - __id__ = 40 - - def __init__(self, name: str, duration, args: str, result: str): - self.name = name - self.duration = duration - self.args = args - self.result = result - - -class OTable(Message): - __id__ = 41 - - def __init__(self, key: str, value: str): - self.key = key - self.value = value - - -class StateAction(Message): - __id__ = 42 - - def __init__(self, type: str): - self.type = type - - -class StateActionEvent(Message): - __id__ = 43 - - def __init__(self, message_id, timestamp, type: str): - self.message_id = message_id - self.timestamp = timestamp - self.type = type - - -class Redux(Message): - __id__ = 44 - - def __init__(self, action: str, state: str, duration): - self.action = action - self.state = state - self.duration = duration - - -class Vuex(Message): - __id__ = 45 - - def __init__(self, mutation: str, state: str): - self.mutation = mutation - self.state = state - - -class MobX(Message): - __id__ = 46 - - def __init__(self, type: str, payload: str): - self.type = type - self.payload = payload - - -class NgRx(Message): - __id__ = 47 - - def __init__(self, action: str, state: str, duration): - self.action = action - self.state = state - self.duration = duration - - -class GraphQL(Message): - __id__ = 48 - - def __init__(self, operation_kind: str, operation_name: str, - variables: str, response: str): - self.operation_kind = operation_kind - self.operation_name = operation_name - self.variables = variables - self.response = response - - -class PerformanceTrack(Message): - __id__ = 49 - - def __init__(self, frames: int, ticks: int, - total_js_heap_size, used_js_heap_size): - self.frames = frames - self.ticks = ticks - self.total_js_heap_size = total_js_heap_size - self.used_js_heap_size = used_js_heap_size - - -class GraphQLEvent(Message): - __id__ = 50 - - def __init__(self, message_id, timestamp, name: str): - self.message_id = message_id - self.timestamp = timestamp - self.name = name - - -class DomDrop(Message): - __id__ = 52 - - def __init__(self, timestamp): - self.timestamp = timestamp - - -class ResourceTiming(Message): - __id__ = 53 - - def __init__(self, timestamp, duration, ttfb, header_size, encoded_body_size, - decoded_body_size, url, initiator): - self.timestamp = timestamp - self.duration = duration - self.ttfb = ttfb - self.header_size = header_size - self.encoded_body_size = encoded_body_size - self.decoded_body_size = decoded_body_size - self.url = url - self.initiator = initiator - - -class ConnectionInformation(Message): - __id__ = 54 - - def __init__(self, downlink, type: str): - self.downlink = downlink - self.type = type - - -class SetPageVisibility(Message): - __id__ = 55 - - def __init__(self, hidden: bool): - self.hidden = hidden - - -class PerformanceTrackAggr(Message): - __id__ = 56 - - def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, - max_fps, min_cpu, avg_cpu, max_cpu, - min_total_js_heap_size, avg_total_js_heap_size, - max_total_js_heap_size, min_used_js_heap_size, - avg_used_js_heap_size, max_used_js_heap_size - ): - self.timestamp_start = timestamp_start - self.timestamp_end = timestamp_end - self.min_fps = min_fps - self.avg_fps = avg_fps - self.max_fps = max_fps - self.min_cpu = min_cpu - self.avg_cpu = avg_cpu - self.max_cpu = max_cpu - self.min_total_js_heap_size = min_total_js_heap_size - self.avg_total_js_heap_size = avg_total_js_heap_size - self.max_total_js_heap_size = max_total_js_heap_size - self.min_used_js_heap_size = min_used_js_heap_size - self.avg_used_js_heap_size = avg_used_js_heap_size - self.max_used_js_heap_size = max_used_js_heap_size - - -class LongTask(Message): - __id__ = 59 - - def __init__(self, timestamp, duration, context, container_type, container_src: str, - container_id: str, container_name: str): - self.timestamp = timestamp - self.duration = duration - self.context = context - self.container_type = container_type - self.container_src = container_src - self.container_id = container_id - self.container_name = container_name - - -class SetNodeURLBasedAttribute(Message): - __id__ = 60 - - def __init__(self, id, name: str, value: str, base_url: str): - self.id = id - self.name = name - self.value = value - self.base_url = base_url - - -class SetStyleData(Message): - __id__ = 61 - - def __init__(self, id, data: str, base_url: str): - self.id = id - self.data = data - self.base_url = base_url - - -class IssueEvent(Message): - __id__ = 62 - - def __init__(self, message_id, timestamp, type: str, context_string: str, - context: str, payload: str): - self.message_id = message_id - self.timestamp = timestamp - self.type = type - self.context_string = context_string - self.context = context - self.payload = payload - - -class TechnicalInfo(Message): - __id__ = 63 - - def __init__(self, type: str, value: str): - self.type = type - self.value = value - - -class CustomIssue(Message): - __id__ = 64 - - def __init__(self, name: str, payload: str): - self.name = name - self.payload = payload - - -class PageClose(Message): - __id__ = 65 - - -class IOSSessionStart(Message): - __id__ = 90 - - def __init__(self, timestamp, project_id, tracker_version: str, - rev_id: str, user_uuid: str, user_os: str, user_os_version: str, - user_device: str, user_device_type: str, user_country: str): - self.timestamp = timestamp - self.project_id = project_id - self.tracker_version = tracker_version - self.rev_id = rev_id - self.user_uuid = user_uuid - self.user_os = user_os - self.user_os_version = user_os_version - self.user_device = user_device - self.user_device_type = user_device_type - self.user_country = user_country - - -class IOSSessionEnd(Message): - __id__ = 91 - - def __init__(self, timestamp): - self.timestamp = timestamp - - -class IOSMetadata(Message): - __id__ = 92 - - def __init__(self, timestamp, length, key: str, value: str): - self.timestamp = timestamp - self.length = length - self.key = key - self.value = value - - -class IOSUserID(Message): - __id__ = 94 - - def __init__(self, timestamp, length, value: str): - self.timestamp = timestamp - self.length = length - self.value = value - - -class IOSUserAnonymousID(Message): - __id__ = 95 - - def __init__(self, timestamp, length, value: str): - self.timestamp = timestamp - self.length = length - self.value = value - - -class IOSScreenLeave(Message): - __id__ = 99 - - def __init__(self, timestamp, length, title: str, view_name: str): - self.timestamp = timestamp - self.length = length - self.title = title - self.view_name = view_name - - -class IOSLog(Message): - __id__ = 103 - - def __init__(self, timestamp, length, severity: str, content: str): - self.timestamp = timestamp - self.length = length - self.severity = severity - self.content = content - - -class IOSInternalError(Message): - __id__ = 104 - - def __init__(self, timestamp, length, content: str): - self.timestamp = timestamp - self.length = length - self.content = content - - -class IOSPerformanceAggregated(Message): - __id__ = 110 - - def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, - max_fps, min_cpu, avg_cpu, max_cpu, - min_memory, avg_memory, max_memory, - min_battery, avg_battery, max_battery - ): - self.timestamp_start = timestamp_start - self.timestamp_end = timestamp_end - self.min_fps = min_fps - self.avg_fps = avg_fps - self.max_fps = max_fps - self.min_cpu = min_cpu - self.avg_cpu = avg_cpu - self.max_cpu = max_cpu - self.min_memory = min_memory - self.avg_memory = avg_memory - self.max_memory = max_memory - self.min_battery = min_battery - self.avg_battery = avg_battery - self.max_battery = max_battery diff --git a/ee/connectors/requirements.txt b/ee/connectors/requirements.txt deleted file mode 100644 index a6b6a0720..000000000 --- a/ee/connectors/requirements.txt +++ /dev/null @@ -1,43 +0,0 @@ -certifi==2020.12.5 -chardet==4.0.0 -clickhouse-driver==0.2.0 -clickhouse-sqlalchemy==0.1.5 -idna==2.10 -kafka-python==2.0.2 -pandas==1.2.3 -psycopg2-binary==2.8.6 -pytz==2021.1 -requests==2.25.1 -SQLAlchemy==1.3.23 -tzlocal==2.1 -urllib3==1.26.3 -PyYAML==5.4.1 -pandas-redshift -awswrangler -google-auth-httplib2 -google-auth-oauthlib -google-cloud-bigquery -pandas-gbq -snowflake-connector-python==2.4.1 -snowflake-sqlalchemy==1.2.4 -asn1crypto==1.4.0 -azure-common==1.1.25 -azure-core==1.8.2 -azure-storage-blob==12.5.0 -boto3==1.15.18 -botocore==1.18.18 -cffi==1.14.3 -cryptography==2.9.2 -isodate==0.6.0 -jmespath==0.10.0 -msrest==0.6.19 -oauthlib==3.1.0 -oscrypto==1.2.1 -pycparser==2.20 -pycryptodomex==3.9.8 -PyJWT==1.7.1 -pyOpenSSL==19.1.0 -python-dateutil==2.8.1 -requests-oauthlib==1.3.0 -s3transfer==0.3.3 -six==1.15.0 diff --git a/ee/connectors/sql/clickhouse_events.sql b/ee/connectors/sql/clickhouse_events.sql deleted file mode 100644 index b5eb8b440..000000000 --- a/ee/connectors/sql/clickhouse_events.sql +++ /dev/null @@ -1,56 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_events -( - sessionid UInt64, - connectioninformation_downlink Nullable(UInt64), - connectioninformation_type Nullable(String), - consolelog_level Nullable(String), - consolelog_value Nullable(String), - customevent_messageid Nullable(UInt64), - customevent_name Nullable(String), - customevent_payload Nullable(String), - customevent_timestamp Nullable(UInt64), - errorevent_message Nullable(String), - errorevent_messageid Nullable(UInt64), - errorevent_name Nullable(String), - errorevent_payload Nullable(String), - errorevent_source Nullable(String), - errorevent_timestamp Nullable(UInt64), - jsexception_message Nullable(String), - jsexception_name Nullable(String), - jsexception_payload Nullable(String), - metadata_key Nullable(String), - metadata_value Nullable(String), - mouseclick_id Nullable(UInt64), - mouseclick_hesitationtime Nullable(UInt64), - mouseclick_label Nullable(String), - pageevent_firstcontentfulpaint Nullable(UInt64), - pageevent_firstpaint Nullable(UInt64), - pageevent_messageid Nullable(UInt64), - pageevent_referrer Nullable(String), - pageevent_speedindex Nullable(UInt64), - pageevent_timestamp Nullable(UInt64), - pageevent_url Nullable(String), - pagerendertiming_timetointeractive Nullable(UInt64), - pagerendertiming_visuallycomplete Nullable(UInt64), - rawcustomevent_name Nullable(String), - rawcustomevent_payload Nullable(String), - setviewportsize_height Nullable(UInt64), - setviewportsize_width Nullable(UInt64), - timestamp_timestamp Nullable(UInt64), - user_anonymous_id Nullable(String), - user_id Nullable(String), - issueevent_messageid Nullable(UInt64), - issueevent_timestamp Nullable(UInt64), - issueevent_type Nullable(String), - issueevent_contextstring Nullable(String), - issueevent_context Nullable(String), - issueevent_payload Nullable(String), - customissue_name Nullable(String), - customissue_payload Nullable(String), - received_at UInt64, - batch_order_number UInt64 -) ENGINE = MergeTree() -PARTITION BY intDiv(received_at, 100000) -ORDER BY (received_at, batch_order_number, sessionid) -PRIMARY KEY (received_at) -SETTINGS use_minimalistic_part_header_in_zookeeper=1, index_granularity=1000; \ No newline at end of file diff --git a/ee/connectors/sql/clickhouse_events_buffer.sql b/ee/connectors/sql/clickhouse_events_buffer.sql deleted file mode 100644 index ed291c824..000000000 --- a/ee/connectors/sql/clickhouse_events_buffer.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_events_buffer -( - sessionid UInt64, - connectioninformation_downlink Nullable(UInt64), - connectioninformation_type Nullable(String), - consolelog_level Nullable(String), - consolelog_value Nullable(String), - customevent_messageid Nullable(UInt64), - customevent_name Nullable(String), - customevent_payload Nullable(String), - customevent_timestamp Nullable(UInt64), - errorevent_message Nullable(String), - errorevent_messageid Nullable(UInt64), - errorevent_name Nullable(String), - errorevent_payload Nullable(String), - errorevent_source Nullable(String), - errorevent_timestamp Nullable(UInt64), - jsexception_message Nullable(String), - jsexception_name Nullable(String), - jsexception_payload Nullable(String), - metadata_key Nullable(String), - metadata_value Nullable(String), - mouseclick_id Nullable(UInt64), - mouseclick_hesitationtime Nullable(UInt64), - mouseclick_label Nullable(String), - pageevent_firstcontentfulpaint Nullable(UInt64), - pageevent_firstpaint Nullable(UInt64), - pageevent_messageid Nullable(UInt64), - pageevent_referrer Nullable(String), - pageevent_speedindex Nullable(UInt64), - pageevent_timestamp Nullable(UInt64), - pageevent_url Nullable(String), - pagerendertiming_timetointeractive Nullable(UInt64), - pagerendertiming_visuallycomplete Nullable(UInt64), - rawcustomevent_name Nullable(String), - rawcustomevent_payload Nullable(String), - setviewportsize_height Nullable(UInt64), - setviewportsize_width Nullable(UInt64), - timestamp_timestamp Nullable(UInt64), - user_anonymous_id Nullable(String), - user_id Nullable(String), - issueevent_messageid Nullable(UInt64), - issueevent_timestamp Nullable(UInt64), - issueevent_type Nullable(String), - issueevent_contextstring Nullable(String), - issueevent_context Nullable(String), - issueevent_payload Nullable(String), - customissue_name Nullable(String), - customissue_payload Nullable(String), - received_at UInt64, - batch_order_number UInt64 -) ENGINE = Buffer(default, connector_events, 16, 10, 120, 10000, 1000000, 10000, 100000000); diff --git a/ee/connectors/sql/clickhouse_sessions.sql b/ee/connectors/sql/clickhouse_sessions.sql deleted file mode 100644 index 4d648553e..000000000 --- a/ee/connectors/sql/clickhouse_sessions.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_user_sessions -( --- SESSION METADATA - sessionid UInt64, - user_agent Nullable(String), - user_browser Nullable(String), - user_browser_version Nullable(String), - user_country Nullable(String), - user_device Nullable(String), - user_device_heap_size Nullable(UInt64), - user_device_memory_size Nullable(UInt64), - user_device_type Nullable(String), - user_os Nullable(String), - user_os_version Nullable(String), - user_uuid Nullable(String), - connection_effective_bandwidth Nullable(UInt64), -- Downlink - connection_type Nullable(String), --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" - metadata_key Nullable(String), - metadata_value Nullable(String), - referrer Nullable(String), - user_anonymous_id Nullable(String), - user_id Nullable(String), --- TIME - session_start_timestamp Nullable(UInt64), - session_end_timestamp Nullable(UInt64), - session_duration Nullable(UInt64), --- SPEED INDEX RELATED - first_contentful_paint Nullable(UInt64), - speed_index Nullable(UInt64), - visually_complete Nullable(UInt64), - timing_time_to_interactive Nullable(UInt64), --- PERFORMANCE - avg_cpu Nullable(UInt64), - avg_fps Nullable(UInt64), - max_cpu Nullable(UInt64), - max_fps Nullable(UInt64), - max_total_js_heap_size Nullable(UInt64), - max_used_js_heap_size Nullable(UInt64), --- ISSUES AND EVENTS - js_exceptions_count Nullable(UInt64), - long_tasks_total_duration Nullable(UInt64), - long_tasks_max_duration Nullable(UInt64), - long_tasks_count Nullable(UInt64), - inputs_count Nullable(UInt64), - clicks_count Nullable(UInt64), - issues_count Nullable(UInt64), - issues Array(Nullable(String)), - urls_count Nullable(UInt64), - urls Array(Nullable(String)) -) ENGINE = MergeTree() -ORDER BY (sessionid) -PRIMARY KEY (sessionid); \ No newline at end of file diff --git a/ee/connectors/sql/clickhouse_sessions_buffer.sql b/ee/connectors/sql/clickhouse_sessions_buffer.sql deleted file mode 100644 index 540700d45..000000000 --- a/ee/connectors/sql/clickhouse_sessions_buffer.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_user_sessions_buffer -( --- SESSION METADATA - sessionid UInt64, - user_agent Nullable(String), - user_browser Nullable(String), - user_browser_version Nullable(String), - user_country Nullable(String), - user_device Nullable(String), - user_device_heap_size Nullable(UInt64), - user_device_memory_size Nullable(UInt64), - user_device_type Nullable(String), - user_os Nullable(String), - user_os_version Nullable(String), - user_uuid Nullable(String), - connection_effective_bandwidth Nullable(UInt64), -- Downlink - connection_type Nullable(String), --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" - metadata_key Nullable(String), - metadata_value Nullable(String), - referrer Nullable(String), - user_anonymous_id Nullable(String), - user_id Nullable(String), --- TIME - session_start_timestamp Nullable(UInt64), - session_end_timestamp Nullable(UInt64), - session_duration Nullable(UInt64), --- SPEED INDEX RELATED - first_contentful_paint Nullable(UInt64), - speed_index Nullable(UInt64), - visually_complete Nullable(UInt64), - timing_time_to_interactive Nullable(UInt64), --- PERFORMANCE - avg_cpu Nullable(UInt64), - avg_fps Nullable(UInt64), - max_cpu Nullable(UInt64), - max_fps Nullable(UInt64), - max_total_js_heap_size Nullable(UInt64), - max_used_js_heap_size Nullable(UInt64), --- ISSUES AND EVENTS - js_exceptions_count Nullable(UInt64), - long_tasks_total_duration Nullable(UInt64), - long_tasks_max_duration Nullable(UInt64), - long_tasks_count Nullable(UInt64), - inputs_count Nullable(UInt64), - clicks_count Nullable(UInt64), - issues_count Nullable(UInt64), - issues Array(Nullable(String)), - urls_count Nullable(UInt64), - urls Array(Nullable(String)) -) ENGINE = Buffer(default, connector_user_sessions, 16, 10, 120, 10000, 1000000, 10000, 100000000); diff --git a/ee/connectors/sql/postgres_events.sql b/ee/connectors/sql/postgres_events.sql deleted file mode 100644 index 986de4df9..000000000 --- a/ee/connectors/sql/postgres_events.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_events -( - sessionid bigint, - connectioninformation_downlink bigint, - connectioninformation_type text, - consolelog_level text, - consolelog_value text, - customevent_messageid bigint, - customevent_name text, - customevent_payload text, - customevent_timestamp bigint, - errorevent_message text, - errorevent_messageid bigint, - errorevent_name text, - errorevent_payload text, - errorevent_source text, - errorevent_timestamp bigint, - jsexception_message text, - jsexception_name text, - jsexception_payload text, - metadata_key text, - metadata_value text, - mouseclick_id bigint, - mouseclick_hesitationtime bigint, - mouseclick_label text, - pageevent_firstcontentfulpaint bigint, - pageevent_firstpaint bigint, - pageevent_messageid bigint, - pageevent_referrer text, - pageevent_speedindex bigint, - pageevent_timestamp bigint, - pageevent_url text, - pagerendertiming_timetointeractive bigint, - pagerendertiming_visuallycomplete bigint, - rawcustomevent_name text, - rawcustomevent_payload text, - setviewportsize_height bigint, - setviewportsize_width bigint, - timestamp_timestamp bigint, - user_anonymous_id text, - user_id text, - issueevent_messageid bigint, - issueevent_timestamp bigint, - issueevent_type text, - issueevent_contextstring text, - issueevent_context text, - issueevent_payload text, - customissue_name text, - customissue_payload text, - received_at bigint, - batch_order_number bigint -); \ No newline at end of file diff --git a/ee/connectors/sql/postgres_sessions.sql b/ee/connectors/sql/postgres_sessions.sql deleted file mode 100644 index 1f68309c2..000000000 --- a/ee/connectors/sql/postgres_sessions.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_user_sessions -( --- SESSION METADATA - sessionid bigint, - user_agent text, - user_browser text, - user_browser_version text, - user_country text, - user_device text, - user_device_heap_size bigint, - user_device_memory_size bigint, - user_device_type text, - user_os text, - user_os_version text, - user_uuid text, - connection_effective_bandwidth bigint, -- Downlink - connection_type text, --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" - metadata_key text, - metadata_value text, - referrer text, - user_anonymous_id text, - user_id text, --- TIME - session_start_timestamp bigint, - session_end_timestamp bigint, - session_duration bigint, --- SPEED INDEX RELATED - first_contentful_paint bigint, - speed_index bigint, - visually_complete bigint, - timing_time_to_interactive bigint, --- PERFORMANCE - avg_cpu bigint, - avg_fps bigint, - max_cpu bigint, - max_fps bigint, - max_total_js_heap_size bigint, - max_used_js_heap_size bigint, --- ISSUES AND EVENTS - js_exceptions_count bigint, - long_tasks_total_duration bigint, - long_tasks_max_duration bigint, - long_tasks_count bigint, - inputs_count bigint, - clicks_count bigint, - issues_count bigint, - issues text[], - urls_count bigint, - urls text[] -); \ No newline at end of file diff --git a/ee/connectors/sql/redshift_events.sql b/ee/connectors/sql/redshift_events.sql deleted file mode 100644 index c310e3202..000000000 --- a/ee/connectors/sql/redshift_events.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE connector_events -( - sessionid BIGINT, - connectioninformation_downlink BIGINT, - connectioninformation_type VARCHAR(300), - consolelog_level VARCHAR(300), - consolelog_value VARCHAR(300), - customevent_messageid BIGINT, - customevent_name VARCHAR(300), - customevent_payload VARCHAR(300), - customevent_timestamp BIGINT, - errorevent_message VARCHAR(300), - errorevent_messageid BIGINT, - errorevent_name VARCHAR(300), - errorevent_payload VARCHAR(300), - errorevent_source VARCHAR(300), - errorevent_timestamp BIGINT, - jsexception_message VARCHAR(300), - jsexception_name VARCHAR(300), - jsexception_payload VARCHAR(300), - metadata_key VARCHAR(300), - metadata_value VARCHAR(300), - mouseclick_id BIGINT, - mouseclick_hesitationtime BIGINT, - mouseclick_label VARCHAR(300), - pageevent_firstcontentfulpaint BIGINT, - pageevent_firstpaint BIGINT, - pageevent_messageid BIGINT, - pageevent_referrer VARCHAR(300), - pageevent_speedindex BIGINT, - pageevent_timestamp BIGINT, - pageevent_url VARCHAR(300), - pagerendertiming_timetointeractive BIGINT, - pagerendertiming_visuallycomplete BIGINT, - rawcustomevent_name VARCHAR(300), - rawcustomevent_payload VARCHAR(300), - setviewportsize_height BIGINT, - setviewportsize_width BIGINT, - timestamp_timestamp BIGINT, - user_anonymous_id VARCHAR(300), - user_id VARCHAR(300), - issueevent_messageid BIGINT, - issueevent_timestamp BIGINT, - issueevent_type VARCHAR(300), - issueevent_contextstring VARCHAR(300), - issueevent_context VARCHAR(300), - issueevent_payload VARCHAR(300), - customissue_name VARCHAR(300), - customissue_payload VARCHAR(300), - received_at BIGINT, - batch_order_number BIGINT -); \ No newline at end of file diff --git a/ee/connectors/sql/redshift_sessions.sql b/ee/connectors/sql/redshift_sessions.sql deleted file mode 100644 index f1750dcc2..000000000 --- a/ee/connectors/sql/redshift_sessions.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE TABLE connector_user_sessions -( --- SESSION METADATA - sessionid bigint, - user_agent VARCHAR, - user_browser VARCHAR, - user_browser_version VARCHAR, - user_country VARCHAR, - user_device VARCHAR, - user_device_heap_size bigint, - user_device_memory_size bigint, - user_device_type VARCHAR, - user_os VARCHAR, - user_os_version VARCHAR, - user_uuid VARCHAR, - connection_effective_bandwidth bigint, -- Downlink - connection_type VARCHAR, --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" - metadata_key VARCHAR, - metadata_value VARCHAR, - referrer VARCHAR, - user_anonymous_id VARCHAR, - user_id VARCHAR, --- TIME - session_start_timestamp bigint, - session_end_timestamp bigint, - session_duration bigint, --- SPEED INDEX RELATED - first_contentful_paint bigint, - speed_index bigint, - visually_complete bigint, - timing_time_to_interactive bigint, --- PERFORMANCE - avg_cpu bigint, - avg_fps bigint, - max_cpu bigint, - max_fps bigint, - max_total_js_heap_size bigint, - max_used_js_heap_size bigint, --- ISSUES AND EVENTS - js_exceptions_count bigint, - long_tasks_total_duration bigint, - long_tasks_max_duration bigint, - long_tasks_count bigint, - inputs_count bigint, - clicks_count bigint, - issues_count bigint, - issues VARCHAR, - urls_count bigint, - urls VARCHAR -); \ No newline at end of file diff --git a/ee/connectors/sql/snowflake_events.sql b/ee/connectors/sql/snowflake_events.sql deleted file mode 100644 index 986de4df9..000000000 --- a/ee/connectors/sql/snowflake_events.sql +++ /dev/null @@ -1,52 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_events -( - sessionid bigint, - connectioninformation_downlink bigint, - connectioninformation_type text, - consolelog_level text, - consolelog_value text, - customevent_messageid bigint, - customevent_name text, - customevent_payload text, - customevent_timestamp bigint, - errorevent_message text, - errorevent_messageid bigint, - errorevent_name text, - errorevent_payload text, - errorevent_source text, - errorevent_timestamp bigint, - jsexception_message text, - jsexception_name text, - jsexception_payload text, - metadata_key text, - metadata_value text, - mouseclick_id bigint, - mouseclick_hesitationtime bigint, - mouseclick_label text, - pageevent_firstcontentfulpaint bigint, - pageevent_firstpaint bigint, - pageevent_messageid bigint, - pageevent_referrer text, - pageevent_speedindex bigint, - pageevent_timestamp bigint, - pageevent_url text, - pagerendertiming_timetointeractive bigint, - pagerendertiming_visuallycomplete bigint, - rawcustomevent_name text, - rawcustomevent_payload text, - setviewportsize_height bigint, - setviewportsize_width bigint, - timestamp_timestamp bigint, - user_anonymous_id text, - user_id text, - issueevent_messageid bigint, - issueevent_timestamp bigint, - issueevent_type text, - issueevent_contextstring text, - issueevent_context text, - issueevent_payload text, - customissue_name text, - customissue_payload text, - received_at bigint, - batch_order_number bigint -); \ No newline at end of file diff --git a/ee/connectors/sql/snowflake_sessions.sql b/ee/connectors/sql/snowflake_sessions.sql deleted file mode 100644 index c66bac2e6..000000000 --- a/ee/connectors/sql/snowflake_sessions.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE TABLE IF NOT EXISTS connector_user_sessions -( --- SESSION METADATA - sessionid bigint, - user_agent text, - user_browser text, - user_browser_version text, - user_country text, - user_device text, - user_device_heap_size bigint, - user_device_memory_size bigint, - user_device_type text, - user_os text, - user_os_version text, - user_uuid text, - connection_effective_bandwidth bigint, -- Downlink - connection_type text, --"bluetooth", "cellular", "ethernet", "none", "wifi", "wimax", "other", "unknown" - metadata_key text, - metadata_value text, - referrer text, - user_anonymous_id text, - user_id text, --- TIME - session_start_timestamp bigint, - session_end_timestamp bigint, - session_duration bigint, --- SPEED INDEX RELATED - first_contentful_paint bigint, - speed_index bigint, - visually_complete bigint, - timing_time_to_interactive bigint, --- PERFORMANCE - avg_cpu bigint, - avg_fps bigint, - max_cpu bigint, - max_fps bigint, - max_total_js_heap_size bigint, - max_used_js_heap_size bigint, --- ISSUES AND EVENTS - js_exceptions_count bigint, - long_tasks_total_duration bigint, - long_tasks_max_duration bigint, - long_tasks_count bigint, - inputs_count bigint, - clicks_count bigint, - issues_count bigint, - issues array, - urls_count bigint, - urls array -); \ No newline at end of file diff --git a/ee/connectors/utils/bigquery.env.example b/ee/connectors/utils/bigquery.env.example deleted file mode 100644 index 16d970501..000000000 --- a/ee/connectors/utils/bigquery.env.example +++ /dev/null @@ -1,7 +0,0 @@ -table_id='{project_id}.{dataset}.{table}' -project_id=name-123456 -dataset=datasetname -sessions_table=connector_user_sessions -events_table_name=connector_events -events_detailed_table_name=connector_events_detailed -level=normal diff --git a/ee/connectors/utils/bigquery_service_account.json.example b/ee/connectors/utils/bigquery_service_account.json.example deleted file mode 100644 index e6473eed7..000000000 --- a/ee/connectors/utils/bigquery_service_account.json.example +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "service_account", - "project_id": "aaaaaa-123456", - "private_key_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "private_key": "-----BEGIN PRIVATE KEY-----\some_letters_and_numbers\n-----END PRIVATE KEY-----\n", - "client_email": "abc-aws@aaaaa-123456.iam.gserviceaccount.com", - "client_id": "12345678910111213", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/bigquery-connector-aws%40asayer-143408.iam.gserviceaccount.com" -} diff --git a/ee/connectors/utils/clickhouse.env.example b/ee/connectors/utils/clickhouse.env.example deleted file mode 100644 index 038fa2a87..000000000 --- a/ee/connectors/utils/clickhouse.env.example +++ /dev/null @@ -1,7 +0,0 @@ -connect_str='clickhouse+native://{address}/{database}' -address=1.1.1.1:9000 -database=default -sessions_table=connector_user_sessions_buffer -events_table_name=connector_events_buffer -events_detailed_table_name=connector_events_detailed_buffer -level=normal diff --git a/ee/connectors/utils/pg.env.example b/ee/connectors/utils/pg.env.example deleted file mode 100644 index e50b041f8..000000000 --- a/ee/connectors/utils/pg.env.example +++ /dev/null @@ -1,10 +0,0 @@ -connect_str='postgresql://{user}:{password}@{address}:{port}/{database}' -address=1.1.1.1 -port=8080 -database=dev -user=qwerty -password=qwertyQWERTY12345 -sessions_table=connector_user_sessions -events_table_name=connector_events -events_detailed_table_name=connector_events_detailed -level=normal diff --git a/ee/connectors/utils/redshift.env.example b/ee/connectors/utils/redshift.env.example deleted file mode 100644 index d78b9a8a2..000000000 --- a/ee/connectors/utils/redshift.env.example +++ /dev/null @@ -1,15 +0,0 @@ -aws_access_key_id=QWERTYQWERTYQWERTY -aws_secret_access_key=abcdefgh12345678 -region_name=eu-central-3 -bucket=name_of_the_bucket -subdirectory=name_of_the_bucket_subdirectory -connect_str='postgresql://{user}:{password}@{address}:{port}/{schema}' -address=redshift-cluster-1.aaaaaaaaa.eu-central-3.redshift.amazonaws.com -port=5439 -schema=dev -user=admin -password=admin -sessions_table=connector_user_sessions -events_table_name=connector_events -events_detailed_table_name=connector_events_detailed -level=normal diff --git a/ee/connectors/utils/snowflake.env.example b/ee/connectors/utils/snowflake.env.example deleted file mode 100644 index deed20462..000000000 --- a/ee/connectors/utils/snowflake.env.example +++ /dev/null @@ -1,11 +0,0 @@ -connect_str='snowflake://{user}:{password}@{account}/{database}/{schema}?warehouse={warehouse}' -user=admin -password=12345678 -account=aaaaaaa.eu-central-3 -database=dev -schema=public -warehouse=SOME_WH -sessions_table=connector_user_sessions -events_table_name=connector_events -events_detailed_table_name=connector_events_detailed -level=normal diff --git a/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql b/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql index e880024d3..9010cb07a 100644 --- a/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql @@ -47,78 +47,77 @@ CREATE TABLE tenants CREATE TYPE user_role AS ENUM ('owner', 'admin', 'member'); CREATE TABLE users ( - user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - tenant_id integer NOT NULL REFERENCES tenants (tenant_id) ON DELETE CASCADE, - email text NOT NULL UNIQUE, - role user_role NOT NULL DEFAULT 'member', - name text NOT NULL, - created_at timestamp without time zone NOT NULL default (now() at time zone 'utc'), - deleted_at timestamp without time zone NULL DEFAULT NULL, - appearance jsonb NOT NULL default '{ - "role": "dev", + user_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + tenant_id integer NOT NULL REFERENCES tenants (tenant_id) ON DELETE CASCADE, + email text NOT NULL UNIQUE, + role user_role NOT NULL DEFAULT 'member', + name text NOT NULL, + created_at timestamp without time zone NOT NULL default (now() at time zone 'utc'), + deleted_at timestamp without time zone NULL DEFAULT NULL, + appearance jsonb NOT NULL default '{ "dashboard": { - "cpu": false, - "fps": false, - "avgCpu": false, - "avgFps": false, - "errors": true, - "crashes": false, - "overview": true, - "sessions": true, - "topMetrics": true, - "callsErrors": false, - "pageMetrics": true, - "performance": true, - "timeToRender": false, - "userActivity": false, - "avgFirstPaint": false, - "countSessions": false, - "errorsPerType": false, - "slowestImages": true, - "speedLocation": false, - "slowestDomains": false, - "avgPageLoadTime": false, - "avgTillFirstBit": false, - "avgTimeToRender": false, - "avgVisitedPages": false, - "avgImageLoadTime": false, - "busiestTimeOfDay": true, - "errorsPerDomains": false, - "missingResources": false, - "resourcesByParty": false, - "sessionsFeedback": false, - "slowestResources": false, - "avgUsedJsHeapSize": false, - "domainsErrors_4xx": false, - "domainsErrors_5xx": false, - "memoryConsumption": false, - "pagesDomBuildtime": false, - "pagesResponseTime": false, - "avgRequestLoadTime": false, - "avgSessionDuration": false, - "sessionsPerBrowser": false, "applicationActivity": true, - "sessionsFrustration": false, - "avgPagesDomBuildtime": false, - "avgPagesResponseTime": false, - "avgTimeToInteractive": false, - "resourcesCountByType": false, - "resourcesLoadingTime": false, - "avgDomContentLoadStart": false, + "avgCpu": true, + "avgDomContentLoadStart": true, "avgFirstContentfulPixel": false, - "resourceTypeVsResponseEnd": false, - "impactedSessionsByJsErrors": false, - "impactedSessionsBySlowPages": false, - "resourcesVsVisuallyComplete": false, - "pagesResponseTimeDistribution": false + "avgFirstPaint": false, + "avgFps": false, + "avgImageLoadTime": true, + "avgPageLoadTime": true, + "avgPagesDomBuildtime": true, + "avgPagesResponseTime": false, + "avgRequestLoadTime": true, + "avgSessionDuration": false, + "avgTillFirstBit": false, + "avgTimeToInteractive": true, + "avgTimeToRender": true, + "avgUsedJsHeapSize": true, + "avgVisitedPages": false, + "busiestTimeOfDay": true, + "callsErrors_4xx": true, + "callsErrors_5xx": true, + "countSessions": true, + "cpu": true, + "crashes": true, + "errors": true, + "errorsPerDomains": true, + "errorsPerType": true, + "errorsTrend": true, + "fps": false, + "impactedSessionsByJsErrors": true, + "impactedSessionsBySlowPages": true, + "memoryConsumption": true, + "missingResources": true, + "overview": true, + "pageMetrics": true, + "pagesResponseTime": true, + "pagesResponseTimeDistribution": true, + "performance": true, + "resourceTypeVsResponseEnd": true, + "resourcesByParty": false, + "resourcesCountByType": true, + "resourcesLoadingTime": true, + "resourcesVsVisuallyComplete": true, + "sessions": true, + "sessionsFeedback": false, + "sessionsFrustration": false, + "sessionsPerBrowser": false, + "slowestDomains": true, + "slowestImages": true, + "slowestResources": true, + "speedLocation": true, + "timeToRender": false, + "topMetrics": true, + "userActivity": false }, - "sessionsLive": false, - "sessionsDevtools": true + "runs": false, + "tests": false, + "pagesDomBuildtime": false }'::jsonb, - api_key text UNIQUE default generate_api_key(20) not null, - jwt_iat timestamp without time zone NULL DEFAULT NULL, - data jsonb NOT NULL DEFAULT '{}'::jsonb, - weekly_report boolean NOT NULL DEFAULT TRUE + api_key text UNIQUE default generate_api_key(20) not null, + jwt_iat timestamp without time zone NULL DEFAULT NULL, + data jsonb NOT NULL DEFAULT '{}'::jsonb, + weekly_report boolean NOT NULL DEFAULT TRUE ); @@ -141,7 +140,7 @@ CREATE TABLE oauth_authentication provider oauth_provider NOT NULL, provider_user_id text NOT NULL, token text NOT NULL, - UNIQUE (user_id, provider) + UNIQUE (provider, provider_user_id) ); @@ -446,6 +445,7 @@ CREATE INDEX user_viewed_errors_error_id_idx ON public.user_viewed_errors (error -- --- sessions.sql --- + CREATE TYPE device_type AS ENUM ('desktop', 'tablet', 'mobile', 'other'); CREATE TYPE country AS ENUM ('UN', 'RW', 'SO', 'YE', 'IQ', 'SA', 'IR', 'CY', 'TZ', 'SY', 'AM', 'KE', 'CD', 'DJ', 'UG', 'CF', 'SC', 'JO', 'LB', 'KW', 'OM', 'QA', 'BH', 'AE', 'IL', 'TR', 'ET', 'ER', 'EG', 'SD', 'GR', 'BI', 'EE', 'LV', 'AZ', 'LT', 'SJ', 'GE', 'MD', 'BY', 'FI', 'AX', 'UA', 'MK', 'HU', 'BG', 'AL', 'PL', 'RO', 'XK', 'ZW', 'ZM', 'KM', 'MW', 'LS', 'BW', 'MU', 'SZ', 'RE', 'ZA', 'YT', 'MZ', 'MG', 'AF', 'PK', 'BD', 'TM', 'TJ', 'LK', 'BT', 'IN', 'MV', 'IO', 'NP', 'MM', 'UZ', 'KZ', 'KG', 'TF', 'HM', 'CC', 'PW', 'VN', 'TH', 'ID', 'LA', 'TW', 'PH', 'MY', 'CN', 'HK', 'BN', 'MO', 'KH', 'KR', 'JP', 'KP', 'SG', 'CK', 'TL', 'RU', 'MN', 'AU', 'CX', 'MH', 'FM', 'PG', 'SB', 'TV', 'NR', 'VU', 'NC', 'NF', 'NZ', 'FJ', 'LY', 'CM', 'SN', 'CG', 'PT', 'LR', 'CI', 'GH', 'GQ', 'NG', 'BF', 'TG', 'GW', 'MR', 'BJ', 'GA', 'SL', 'ST', 'GI', 'GM', 'GN', 'TD', 'NE', 'ML', 'EH', 'TN', 'ES', 'MA', 'MT', 'DZ', 'FO', 'DK', 'IS', 'GB', 'CH', 'SE', 'NL', 'AT', 'BE', 'DE', 'LU', 'IE', 'MC', 'FR', 'AD', 'LI', 'JE', 'IM', 'GG', 'SK', 'CZ', 'NO', 'VA', 'SM', 'IT', 'SI', 'ME', 'HR', 'BA', 'AO', 'NA', 'SH', 'BV', 'BB', 'CV', 'GY', 'GF', 'SR', 'PM', 'GL', 'PY', 'UY', 'BR', 'FK', 'GS', 'JM', 'DO', 'CU', 'MQ', 'BS', 'BM', 'AI', 'TT', 'KN', 'DM', 'AG', 'LC', 'TC', 'AW', 'VG', 'VC', 'MS', 'MF', 'BL', 'GP', 'GD', 'KY', 'BZ', 'SV', 'GT', 'HN', 'NI', 'CR', 'VE', 'EC', 'CO', 'PA', 'HT', 'AR', 'CL', 'BO', 'PE', 'MX', 'PF', 'PN', 'KI', 'TK', 'TO', 'WF', 'WS', 'NU', 'MP', 'GU', 'PR', 'VI', 'UM', 'AS', 'CA', 'US', 'PS', 'RS', 'AQ', 'SX', 'CW', 'BQ', 'SS'); CREATE TYPE platform AS ENUM ('web','ios','android'); @@ -456,7 +456,7 @@ CREATE TABLE sessions project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, tracker_version text NOT NULL, start_ts bigint NOT NULL, - duration integer NULL, + duration integer NOT NULL, rev_id text DEFAULT NULL, platform platform NOT NULL DEFAULT 'web', is_snippet boolean NOT NULL DEFAULT FALSE, @@ -508,7 +508,6 @@ CREATE INDEX ON sessions (project_id, metadata_7); CREATE INDEX ON sessions (project_id, metadata_8); CREATE INDEX ON sessions (project_id, metadata_9); CREATE INDEX ON sessions (project_id, metadata_10); --- CREATE INDEX ON sessions (rehydration_id); CREATE INDEX ON sessions (project_id, watchdogs_score DESC); CREATE INDEX platform_idx ON public.sessions (platform); @@ -559,18 +558,6 @@ CREATE TABLE user_favorite_sessions ); --- --- assignments.sql --- - -create table assigned_sessions -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - issue_id text NOT NULL, - provider oauth_provider NOT NULL, - created_by integer NOT NULL, - created_at timestamp default timezone('utc'::text, now()) NOT NULL, - provider_data jsonb default '{}'::jsonb NOT NULL -); - -- --- events_common.sql --- CREATE SCHEMA events_common; @@ -626,6 +613,7 @@ CREATE INDEX requests_url_gin_idx2 ON events_common.requests USING GIN (RIGHT(ur gin_trgm_ops); -- --- events.sql --- + CREATE SCHEMA events; CREATE TABLE events.pages @@ -648,7 +636,6 @@ CREATE TABLE events.pages time_to_interactive integer DEFAULT NULL, response_time bigint DEFAULT NULL, response_end bigint DEFAULT NULL, - ttfb integer DEFAULT NULL, PRIMARY KEY (session_id, message_id) ); CREATE INDEX ON events.pages (session_id); @@ -668,11 +655,6 @@ CREATE INDEX pages_base_referrer_gin_idx2 ON events.pages USING GIN (RIGHT(base_ gin_trgm_ops); CREATE INDEX ON events.pages (response_time); CREATE INDEX ON events.pages (response_end); -CREATE INDEX pages_path_gin_idx ON events.pages USING GIN (path gin_trgm_ops); -CREATE INDEX pages_path_idx ON events.pages (path); -CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0; -CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0; -CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0; CREATE TABLE events.clicks @@ -739,61 +721,6 @@ CREATE INDEX ON events.state_actions (name); CREATE INDEX state_actions_name_gin_idx ON events.state_actions USING GIN (name gin_trgm_ops); CREATE INDEX ON events.state_actions (timestamp); -CREATE TYPE events.resource_type AS ENUM ('other', 'script', 'stylesheet', 'fetch', 'img', 'media'); -CREATE TYPE events.resource_method AS ENUM ('GET' , 'HEAD' , 'POST' , 'PUT' , 'DELETE' , 'CONNECT' , 'OPTIONS' , 'TRACE' , 'PATCH' ); -CREATE TABLE events.resources -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - message_id bigint NOT NULL, - timestamp bigint NOT NULL, - duration bigint NULL, - type events.resource_type NOT NULL, - url text NOT NULL, - url_host text NOT NULL, - url_hostpath text NOT NULL, - success boolean NOT NULL, - status smallint NULL, - method events.resource_method NULL, - ttfb bigint NULL, - header_size bigint NULL, - encoded_body_size integer NULL, - decoded_body_size integer NULL, - PRIMARY KEY (session_id, message_id) -); -CREATE INDEX ON events.resources (session_id); -CREATE INDEX ON events.resources (timestamp); -CREATE INDEX ON events.resources (success); -CREATE INDEX ON events.resources (status); -CREATE INDEX ON events.resources (type); -CREATE INDEX ON events.resources (duration) WHERE duration > 0; -CREATE INDEX ON events.resources (url_host); - -CREATE INDEX resources_url_gin_idx ON events.resources USING GIN (url gin_trgm_ops); -CREATE INDEX resources_url_idx ON events.resources (url); -CREATE INDEX resources_url_hostpath_gin_idx ON events.resources USING GIN (url_hostpath gin_trgm_ops); -CREATE INDEX resources_url_hostpath_idx ON events.resources (url_hostpath); - - - -CREATE TABLE events.performance -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - message_id bigint NOT NULL, - min_fps smallint NOT NULL, - avg_fps smallint NOT NULL, - max_fps smallint NOT NULL, - min_cpu smallint NOT NULL, - avg_cpu smallint NOT NULL, - max_cpu smallint NOT NULL, - min_total_js_heap_size bigint NOT NULL, - avg_total_js_heap_size bigint NOT NULL, - max_total_js_heap_size bigint NOT NULL, - min_used_js_heap_size bigint NOT NULL, - avg_used_js_heap_size bigint NOT NULL, - max_used_js_heap_size bigint NOT NULL, - PRIMARY KEY (session_id, message_id) -); CREATE OR REPLACE FUNCTION events.funnel(steps integer[], m integer) RETURNS boolean AS @@ -835,4 +762,4 @@ CREATE INDEX autocomplete_type_idx ON public.autocomplete (type); CREATE INDEX autocomplete_value_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops); -COMMIT; +COMMIT; \ No newline at end of file diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 2706ca2e4..b2024c7d4 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -21,7 +21,6 @@ import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails'; import APIClient from './api_client'; import * as routes from './routes'; -import { OB_DEFAULT_TAB } from 'App/routes'; import Signup from './components/Signup/Signup'; import { fetchTenants } from 'Duck/user'; @@ -49,7 +48,6 @@ const SIGNUP_PATH = routes.signup(); const FORGOT_PASSWORD = routes.forgotPassword(); const CLIENT_PATH = routes.client(); const ONBOARDING_PATH = routes.onboarding(); -const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); @withRouter @connect((state) => { @@ -69,7 +67,6 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB); organisation: state.getIn([ 'user', 'client', 'name' ]), tenantId: state.getIn([ 'user', 'client', 'tenantId' ]), tenants: state.getIn(['user', 'tenants']), - onboarding: state.getIn([ 'user', 'onboarding' ]) }; }, { fetchUserInfo, fetchTenants @@ -95,7 +92,7 @@ class Router extends React.Component { } render() { - const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, tenants, onboarding } = this.props; + const { isLoggedIn, jwt, siteId, sites, loading, changePassword, location, tenants } = this.props; const siteIdList = sites.map(({ id }) => id).toJS(); const hideHeader = location.pathname && location.pathname.includes('/session/'); @@ -124,9 +121,6 @@ class Router extends React.Component { } } /> - { onboarding && - - } { siteIdList.length === 0 && } diff --git a/frontend/app/assets/apple-touch-icon.png b/frontend/app/assets/apple-touch-icon.png deleted file mode 100644 index 2adaf4f0d8494a9a15b508b280111f338755cd5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6253 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 diff --git a/frontend/app/assets/favicon-32x32.png b/frontend/app/assets/favicon-32x32.png deleted file mode 100644 index 980396723f37de2afc0502752524bf2625bc64cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1090 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 diff --git a/frontend/app/assets/favicon@1x.png b/frontend/app/assets/favicon@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..393d5d3cc6c5763dff0a4c964456b2f9fc13e3c6 GIT binary patch literal 2127 zcmV-V2(b5wP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$`xk*GpR9FdJS8HsQRTf>}ckiX8 zR0?IL0yPL0H1e#&W2_dzVGQ_85Mwk(CX-0iI59eA@Ga=>-6JJ3S@%=+o zq?RgUp_a!GF?OJ&v=ye*N=r*=d++SE_c`Z&aMF9vd#|E%GT$X9gYmV ze>dWxQUkQj3P?lF0F9clyg>ph0RctY#%aT^Q(_6SB;|<-+0KGYW>!qk1*DXV>6s8w zqhrUWv9Rhaj4X+DQy?Vk=&3iZ2Boj$g=~)d-8}1<>l47H8kNeH4xn#s+R5%@GDBA_ zD#07iRA5*pM(@cX{O#4lSpQZFPGw0>3jg(5TZYN+Jm9hH%Blia*CK&rxd=eb*_A@Yp0R>l%X zS;x}VJ&0v1_oAt#*9PQ!(VQ{Z{$dp#|NUeX7e)DucO*i12%=ug=@2JXkwl_eh{Sng zYuOpv0S@?l=P^{>UXRCKX+lrmpw48{G45VE2|Hg{fEzCxr`{1*fe7a0wnX*$;l_0x zm9S4tCfuMO^@8p@2_k9A{D36i`uCbK`;I!iUDGBC+5Fj~3b5g>i?DfhH7YAgoErJW zQzD+2=7%W}i4uC+WK0w=ChCF8CX)o#;^w$U=8haci97$k8%rMDj@<`;_KPrgaw$H0 zWC7OQT8S|u(;+OTNM3=Uu;Q4_yaK`|WNt9+5=~WNvdV;z*>Ru~i&oX*-hb`FvF?6d z9mf%t&p!|KtFOdu3o4Lu$*4AEKlfRJnp1vU72wfRc4S7W5=G?_17{Vdu>6X0WHRPa zF3RGyx@OFK^i#aNwFyI6+gDOF3{PJ_8(Z&Ng6b({3ap8#vQv5sxxb=CfxPDg*S;g5 zk#HL-%7^2*+h^dLbqjIz+_RN1*Z$mp3J<)~h|AY(##c>81N95Xj>5aQU4ehzG#}@T zE($qjIY$!aZ0s3%a5D2bkkceIQkpAbeqz}Oymt3Id~km?rcaQ|%Bl9<(Jn0AumvmL ztV3(pi9qDKX%n&SuIq76^-Q@3+)b(>WeyA_j-eP*SrQ>2zGgBv@DfRs>I;5}FCO|W zp1tK)C@aZW2xZ-e`;Tr(evA44*o6NaITm)^Fm*~$Omj|-yF5n5 z#D{`)vXGR@e})2nP2a#EHq`CM{*KPPT4J+oWFh{v;37;bEA>m!a{RchA%-|9_o%!_ z1fT*W;*v1zkVZ~`O_|d}z{}{}#zXk?Cyi+9>CyePk;z0@aq(3AvAPn)1;c`=A5WaX zeKlX0N={+9vV;J^8F#W0Fy?{3>9{-AIj52{*Kp_<9{HdF4Xyvnhe#Z`{QU9w%i>?7 zVoXT@9vB+JD|_~0T|+&3b8=^>=Qhm?RTLn2{wQkS=RjnHNU&RuTXRPb)_k%Tn|2-4 zuo&?iCjO!rPh2?%i_f1BfZ5o3s0EL0+lB)@KbeEP;-g5urDg!6Ejb{bYZzpZWY|s= zUPP3(`KNBoeez>d%dIOaNa3D^GjYcy(@>D+HEHEg*Kw?_`x={@4`}#kSFi+@QMZ~; zZjJ07>Ub7rhM(%p+cykCoJU;h|;I_&b% zJs#Yj5PHT!?*kR<>#&<6yb;lRASeH;<;H33Rf!xP{Ah=~^Zl4!J{n&)|A?1&?nCd; zfb%r(aqwIcSOI-}eXZC6Bx!CoW3pIc+AqIup7^XGgvB!3E-Vk~ zYdtvv$jnh$=^Ou4Z-V=AWutvc&(LI}s3tBbidV`%Bb}PL?iguYc>}7%E~|MuoI+N+ zKHXwb+1a3r21*bXp`qmbIBOZ`j8M2TZe6?Va3(GPrDk#)MU8g#OplUmW1UD&yS9DS z%N%1a#t5KB=^5LOjGlR!<(Rd#(H=+9MrqAzU9sZu{{e)${x&=TH<$nb002ovPDHLk FV1h*j@O%IO literal 0 HcmV?d00001 diff --git a/frontend/app/assets/favicon@2x.png b/frontend/app/assets/favicon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c99e774af9b12cefee4d8c90798de5906107d2ae GIT binary patch literal 5829 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 P00000NkvXXu0mjfqNy)p literal 0 HcmV?d00001 diff --git a/frontend/app/assets/favicon@3x.png b/frontend/app/assets/favicon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d38be71c0f2d2689f32492eeac4045458f2fec0 GIT binary patch literal 10941 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 literal 0 HcmV?d00001 diff --git a/frontend/app/assets/favicon@4x.png b/frontend/app/assets/favicon@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..19f3a4256df203350c7e2c3f5fb8a6e48f8f7431 GIT binary patch literal 16394 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 literal 0 HcmV?d00001 diff --git a/frontend/app/assets/favicon@6x.png b/frontend/app/assets/favicon@6x.png new file mode 100644 index 0000000000000000000000000000000000000000..dbf51e62c2e1aad3de79f59abfc87082f5186554 GIT binary patch literal 32078 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

{coPvv>_ZRCR&PKtg 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 A&;S4c literal 0 HcmV?d00001 diff --git a/frontend/app/assets/index.html b/frontend/app/assets/index.html index f90b87ff2..3147d2337 100644 --- a/frontend/app/assets/index.html +++ b/frontend/app/assets/index.html @@ -5,9 +5,12 @@ - - - + + + + + + diff --git a/frontend/app/components/BugFinder/CustomFilters/FilterItem.js b/frontend/app/components/BugFinder/CustomFilters/FilterItem.js index 8b60b601c..e929a53c4 100644 --- a/frontend/app/components/BugFinder/CustomFilters/FilterItem.js +++ b/frontend/app/components/BugFinder/CustomFilters/FilterItem.js @@ -6,7 +6,7 @@ import cn from 'classnames'; const FilterItem = ({ className = '', icon, label, onClick }) => { return (

- { icon && } + { label }
); diff --git a/frontend/app/components/BugFinder/DateRange.js b/frontend/app/components/BugFinder/DateRange.js index 60e98ffa1..4f2ce8e22 100644 --- a/frontend/app/components/BugFinder/DateRange.js +++ b/frontend/app/components/BugFinder/DateRange.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { applyFilter } from 'Duck/filters'; +import { applyFilter, fetchList } from 'Duck/filters'; import { fetchList as fetchFunnelsList } from 'Duck/funnels'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; @@ -8,10 +8,11 @@ import DateRangeDropdown from 'Shared/DateRangeDropdown'; startDate: state.getIn([ 'filters', 'appliedFilter', 'startDate' ]), endDate: state.getIn([ 'filters', 'appliedFilter', 'endDate' ]), }), { - applyFilter, fetchFunnelsList + applyFilter, fetchList, fetchFunnelsList }) export default class DateRange extends React.PureComponent { onDateChange = (e) => { + this.props.fetchList(e.rangeValue) this.props.fetchFunnelsList(e.rangeValue) this.props.applyFilter(e) } diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index af5adf937..c29cdd6a5 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -3,15 +3,14 @@ import { connect } from 'react-redux'; import cn from 'classnames'; import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI' import stl from './sessionMenu.css'; -import { fetchWatchdogStatus } from 'Duck/watchdogs'; +import { fetchList, fetchWatchdogStatus } from 'Duck/watchdogs'; import { setActiveFlow, clearEvents } from 'Duck/filters'; import { setActiveTab } from 'Duck/sessions'; -import { issues_types } from 'Types/session/issue' function SessionsMenu(props) { const { activeFlow, activeTab, watchdogs = [], keyMap, wdTypeCount, - fetchWatchdogStatus, toggleRehydratePanel } = props; + fetchList, fetchWatchdogStatus, toggleRehydratePanel } = props; const onMenuItemClick = (filter) => { props.onMenuItemClick(filter) @@ -22,6 +21,7 @@ function SessionsMenu(props) { } useEffect(() => { + fetchList() fetchWatchdogStatus() }, []) @@ -62,7 +62,7 @@ function SessionsMenu(props) { />
- { issues_types.filter(item => item.visible).map(item => ( + { watchdogs.filter(item => item.visible).map(item => ( ({ + watchdogs: state.getIn(['watchdogs', 'list']).sortBy(i => i.order), activeTab: state.getIn([ 'sessions', 'activeTab' ]), keyMap: state.getIn([ 'sessions', 'keyMap' ]), wdTypeCount: state.getIn([ 'sessions', 'wdTypeCount' ]), activeFlow: state.getIn([ 'filters', 'activeFlow' ]), captureRate: state.getIn(['watchdogs', 'captureRate']), }), { - fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab + fetchList, fetchWatchdogStatus, setActiveFlow, clearEvents, setActiveTab })(SessionsMenu); diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 16586fd1d..754146a36 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,22 +1,20 @@ import React from 'react' import { connect } from 'react-redux' -import { edit, save, init, update } from 'Duck/integrations/slack' +import { edit, save, init } from 'Duck/integrations/slack' import { Form, Input, Button, Message } from 'UI' import { confirm } from 'UI/Confirmation'; import { remove } from 'Duck/integrations/slack' class SlackAddForm extends React.PureComponent { + componentWillUnmount() { this.props.init({}); } save = () => { - const instance = this.props.instance; - if(instance.exists()) { - this.props.update(this.props.instance) - } else { - this.props.save(this.props.instance) - } + this.props.save(this.props.instance).then(function() { + + }) } remove = async (id) => { @@ -104,4 +102,4 @@ export default connect(state => ({ instance: state.getIn(['slack', 'instance']), saving: state.getIn(['slack', 'saveRequest', 'loading']), errors: state.getIn([ 'slack', 'saveRequest', 'errors' ]), -}), { edit, save, init, remove, update })(SlackAddForm) \ No newline at end of file +}), { edit, save, init, remove })(SlackAddForm) \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index e854dfce2..88529ce58 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,8 +1,7 @@ import React from 'react' import { connect } from 'react-redux' -import { NoContent } from 'UI'; +import { TextEllipsis, NoContent } from 'UI'; import { remove, edit } from 'Duck/integrations/slack' -import DocLink from 'Shared/DocLink/DocLink'; function SlackChannelList(props) { const { list } = props; @@ -15,12 +14,7 @@ function SlackChannelList(props) { return (
-
Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.
- -
- } + title="No data available." size="small" show={ list.size === 0 } > @@ -30,12 +24,21 @@ function SlackChannelList(props) { className="border-t px-5 py-2 flex items-center justify-between cursor-pointer" onClick={() => onEdit(c)} > -
+
{c.name}
-
- {c.endpoint} -
+ + {c.endpoint} +
+ } + />
+ {/*
+ +
*/}
))} diff --git a/frontend/app/components/Client/ManageUsers/ManageUsers.js b/frontend/app/components/Client/ManageUsers/ManageUsers.js index 9f0a4244d..c8d1c633d 100644 --- a/frontend/app/components/Client/ManageUsers/ManageUsers.js +++ b/frontend/app/components/Client/ManageUsers/ManageUsers.js @@ -7,7 +7,6 @@ import styles from './manageUsers.css'; import UserItem from './UserItem'; import { confirm } from 'UI/Confirmation'; import { toast } from 'react-toastify'; -import BannerMessage from 'Shared/BannerMessage'; const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; const LIMIT_WARNING = 'You have reached users limit.'; @@ -39,7 +38,7 @@ class ManageUsers extends React.PureComponent { } adminLabel = (user) => { - if (user.superAdmin) return 'Owner'; + if (user.superAdmin) return 'Super Admin'; return user.admin ? 'Admin' : ''; }; @@ -159,37 +158,28 @@ class ManageUsers extends React.PureComponent { onClose={ this.closeModal } />
-
-
- { !hideHeader &&

{ (isAdmin ? 'Manage ' : '') + 'Users' }

} - { hideHeader &&

{ `Team Size ${members.size}` }

} - - this.init() } - /> -
- } - // disabled={ canAddUsers } - content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` } - size="tiny" - inverted - position="top left" - /> -
-
- { !account.smtp && - - Inviting new users require email messaging. Please setup SMTP. - +
+ { !hideHeader &&

{ (isAdmin ? 'Manage ' : '') + 'Users' }

} + { hideHeader &&

{ `Team Size ${members.size}` }

} + + this.init() } + /> +
} -
+ // disabled={ canAddUsers } + content={ `${ !canAddUsers ? (!isAdmin ? PERMISSION_WARNING : LIMIT_WARNING) : 'Add team member' }` } + size="tiny" + inverted + position="top left" + />
setTab(CLIENT_TABS.NOTIFICATIONS) } />
diff --git a/frontend/app/components/Client/ProfileSettings/OptOut.js b/frontend/app/components/Client/ProfileSettings/OptOut.js index 6e4643d7b..dea675a60 100644 --- a/frontend/app/components/Client/ProfileSettings/OptOut.js +++ b/frontend/app/components/Client/ProfileSettings/OptOut.js @@ -6,7 +6,7 @@ import { updateClient } from 'Duck/user' function OptOut(props) { const { optOut } = props; const onChange = () => { - props.updateClient({ optOut: !optOut }) + props.updateClient({ optOut: !optOut, name: 'OpenReplay' }) } return (
diff --git a/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css b/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css index dde6009e4..cf1b14578 100644 --- a/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css +++ b/frontend/app/components/Dashboard/Widgets/SessionsPerBrowser/Bar.css @@ -1,5 +1,5 @@ .bar { - height: 5px; + height: 10px; width: 100%; border-radius: 3px; display: flex; diff --git a/frontend/app/components/Errors/Error/ErrorInfo.js b/frontend/app/components/Errors/Error/ErrorInfo.js index 4726bc613..c9681f25d 100644 --- a/frontend/app/components/Errors/Error/ErrorInfo.js +++ b/frontend/app/components/Errors/Error/ErrorInfo.js @@ -18,7 +18,7 @@ import SideSection from './SideSection'; export default class ErrorInfo extends React.PureComponent { ensureInstance() { const { errorId, loading, errorOnFetch } = this.props; - if (!loading && + if (!loading && !errorOnFetch && this.props.errorIdInStore !== errorId && errorId != null) { this.props.fetch(errorId); diff --git a/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js b/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js index 0a8a997dd..18702f5aa 100644 --- a/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js +++ b/frontend/app/components/Funnels/FunnelDetails/FunnelDetails.js @@ -34,9 +34,9 @@ const FunnelDetails = (props) => { useEffect(() => { if (funnels.size === 0) { - props.fetchList(); + props.fetchList(); + props.fetchIssueTypes() } - props.fetchIssueTypes() props.fetch(funnelId).then(() => { setMounted(true); diff --git a/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js b/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js index b7d140b1b..a747c9905 100644 --- a/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js +++ b/frontend/app/components/Funnels/FunnelHeader/FunnelHeader.js @@ -108,7 +108,6 @@ const FunnelHeader = (props) => { startDate={funnelFilters.startDate} endDate={funnelFilters.endDate} onDateChange={onDateChange} - customRangeRight />
diff --git a/frontend/app/components/Header/Discover/featureItem.css b/frontend/app/components/Header/Discover/featureItem.css index d434c8f91..0c0d54b9c 100644 --- a/frontend/app/components/Header/Discover/featureItem.css +++ b/frontend/app/components/Header/Discover/featureItem.css @@ -1,5 +1,5 @@ .wrapper { - padding: 7px 0; + padding: 10px 0; } .checkbox { diff --git a/frontend/app/components/Header/OnboardingExplore/FeatureItem.js b/frontend/app/components/Header/OnboardingExplore/FeatureItem.js index cbb0c3472..bbc31b3ad 100644 --- a/frontend/app/components/Header/OnboardingExplore/FeatureItem.js +++ b/frontend/app/components/Header/OnboardingExplore/FeatureItem.js @@ -6,7 +6,7 @@ import stl from './featureItem.css'; const FeatureItem = ({ label, completed = false, subText, onClick }) => { return (
diff --git a/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js b/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js index c6d7aa179..a6893fc17 100644 --- a/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js +++ b/frontend/app/components/Header/OnboardingExplore/OnboardingExplore.js @@ -121,7 +121,7 @@ class OnboardingExplore extends React.PureComponent {
- Make the best out of OpenReplay by completing your project setup: + Follow the steps below to complete this project setup and make the best out of OpenReplay.
@@ -131,7 +131,7 @@ class OnboardingExplore extends React.PureComponent { key={ task.task } label={ task.task } completed={ task.done } - onClick={() => this.onClick(task) } + onClick={task.URL && (() => this.onClick(task)) } /> ))}
diff --git a/frontend/app/components/Header/OnboardingExplore/featureItem.css b/frontend/app/components/Header/OnboardingExplore/featureItem.css index b0fe2dbb9..e0b005408 100644 --- a/frontend/app/components/Header/OnboardingExplore/featureItem.css +++ b/frontend/app/components/Header/OnboardingExplore/featureItem.css @@ -1,5 +1,5 @@ .wrapper { - padding: 6px 0; + padding: 10px 0; display: flex; align-items: center; } diff --git a/frontend/app/components/Login/Login.js b/frontend/app/components/Login/Login.js index bd619bae5..f79b2fc05 100644 --- a/frontend/app/components/Login/Login.js +++ b/frontend/app/components/Login/Login.js @@ -63,7 +63,7 @@ export default class Login extends React.Component {

Login to OpenReplay

- { tenants.length === 0 &&
Don't have an account? Sign up
} + { tenants.length === 0 &&
Don't have an account?Sign up
}
{ window.ENV.CAPTCHA_ENABLED && ( diff --git a/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js b/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js index d9d24c7df..0a048e3cb 100644 --- a/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js +++ b/frontend/app/components/Onboarding/components/OnboardingNavButton/OnboardingNavButton.js @@ -5,7 +5,6 @@ import { Button } from 'UI' import { OB_TABS, onboarding as onboardingRoute } from 'App/routes' import * as routes from '../../../../routes' import { sessions } from 'App/routes'; -import { setOnboarding } from 'Duck/user'; const withSiteId = routes.withSiteId; const MENU_ITEMS = [OB_TABS.INSTALLING, OB_TABS.IDENTIFY_USERS, OB_TABS.MANAGE_USERS, OB_TABS.INTEGRATIONS] @@ -26,14 +25,9 @@ const OnboardingNavButton = (props) => { const tab = MENU_ITEMS[activeIndex+1] history.push(withSiteId(onboardingRoute(tab), siteId)); } else { - onDone() + history.push(sessions()); } } - - const onDone = () => { - props.setOnboarding(false); - history.push(sessions()); - } return ( <> @@ -41,7 +35,7 @@ const OnboardingNavButton = (props) => { primary size="small" plain - onClick={onDone} + onClick={() => history.push(sessions())} > {activeIndex === 0 ? 'Done. See Recorded Sessions' : 'Skip Optional Steps and See Recorded Sessions'} @@ -59,4 +53,4 @@ const OnboardingNavButton = (props) => { ) } -export default withRouter(connect(null, { setOnboarding })(OnboardingNavButton)) \ No newline at end of file +export default withRouter(OnboardingNavButton) \ No newline at end of file diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js index 755718ef0..a093c4f94 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -28,10 +28,9 @@ const codeSnippet = ` r.setMetadata=function(k,v){r.push([4,k,v])}; r.event=function(k,p,i){r.push([5,k,p,i])}; r.issue=function(k,p){r.push([6,k,p])}; - r.isActive=function(){return false}; - r.getSessionToken=function(){}; - r.i="https://${window.location.hostname}/ingest"; -})(0, "PROJECT_KEY", "//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",1,XXX); + r.isActive=r.active=function(){return false}; + r.getSessionToken=r.sessionID=function(){}; +})(0,PROJECT_HASH,"//${window.location.hostname}/static/openreplay.js",1,XXX); `; diff --git a/frontend/app/components/Session_/Issues/IssueDetails.js b/frontend/app/components/Session_/Issues/IssueDetails.js index f91f0ad73..111dd15fe 100644 --- a/frontend/app/components/Session_/Issues/IssueDetails.js +++ b/frontend/app/components/Session_/Issues/IssueDetails.js @@ -14,9 +14,9 @@ class IssueDetails extends React.PureComponent { write = (e, { name, value }) => this.setState({ [ name ]: value }); render() { - const { sessionId, issue, loading, users, issueTypeIcons, issuesIntegration } = this.props; + const { sessionId, issue, loading, users, issueTypeIcons, provider } = this.props; const activities = issue.activities; - const provider = issuesIntegration.provider; + const assignee = users.filter(({id}) => issue.assignee === id).first(); return ( @@ -53,5 +53,5 @@ export default connect(state => ({ users: state.getIn(['assignments', 'users']), loading: state.getIn(['assignments', 'fetchAssignment', 'loading']), issueTypeIcons: state.getIn(['assignments', 'issueTypeIcons']), - issuesIntegration: state.getIn([ 'issues', 'list']).first() || {}, + provider: state.getIn([ 'issues', 'list']).provider, }))(IssueDetails); diff --git a/frontend/app/components/Session_/Issues/IssueForm.js b/frontend/app/components/Session_/Issues/IssueForm.js index 81dda0504..f305c3ee7 100644 --- a/frontend/app/components/Session_/Issues/IssueForm.js +++ b/frontend/app/components/Session_/Issues/IssueForm.js @@ -7,8 +7,7 @@ import { addActivity, init, edit, fetchAssignments, fetchMeta } from 'Duck/assig const SelectedValue = ({ icon, text }) => { return(
- {/* */} - { icon } + { text }
) @@ -38,7 +37,7 @@ class IssueForm extends React.PureComponent { addActivity(sessionId, instance).then(() => { const { errors } = this.props; - if (!errors || errors.length === 0) { + if (errors.length === 0) { this.props.init({projectId: instance.projectId}); this.props.fetchAssignments(sessionId); this.props.closeHandler(); @@ -53,9 +52,8 @@ class IssueForm extends React.PureComponent { const { creating, projects, users, issueTypes, instance, closeHandler, metaLoading } = this.props; const projectOptions = projects.map(({name, id}) => ({text: name, value: id })).toArray(); const userOptions = users.map(({name, id}) => ({text: name, value: id })).toArray(); - - const issueTypeOptions = issueTypes.map(({name, id, iconUrl, color }) => { - return {text: name, value: id, iconUrl, color } + const issueTypeOptions = issueTypes.map(({name, id, iconUrl }) => { + return {text: name, value: id, iconUrl, icon: } }).toArray(); const selectedIssueType = issueTypes.filter(issue => issue.id == instance.issueType).first(); @@ -82,7 +80,6 @@ class IssueForm extends React.PureComponent { { {/* */} {/* */}
- { typeIcon } - {/* */} + { issue.id } {/*
{ '@ 00:13 Secs'}
*/} { assignee && diff --git a/frontend/app/components/Session_/Issues/IssueListItem.js b/frontend/app/components/Session_/Issues/IssueListItem.js index 51b5bb25c..9145ffaa5 100644 --- a/frontend/app/components/Session_/Issues/IssueListItem.js +++ b/frontend/app/components/Session_/Issues/IssueListItem.js @@ -11,8 +11,7 @@ const IssueListItem = ({ issue, onClick, icon, user, active }) => { >
- { icon } - {/* */} + { issue.id }
diff --git a/frontend/app/components/Session_/Issues/Issues.js b/frontend/app/components/Session_/Issues/Issues.js index bba3eaf40..66fc80bc8 100644 --- a/frontend/app/components/Session_/Issues/Issues.js +++ b/frontend/app/components/Session_/Issues/Issues.js @@ -21,7 +21,7 @@ import stl from './issues.css'; fetchIssueLoading: state.getIn(['assignments', 'fetchAssignment', 'loading']), fetchIssuesLoading: state.getIn(['assignments', 'fetchAssignments', 'loading']), projectsLoading: state.getIn(['assignments', 'fetchProjects', 'loading']), - issuesIntegration: state.getIn([ 'issues', 'list']).first() || {}, + provider: state.getIn([ 'issues', 'list']).provider, }), { fetchAssigment, fetchAssignments, fetchMeta, fetchProjects }) @withToggle('isModalDisplayed', 'toggleModal') class Issues extends React.Component { @@ -64,10 +64,9 @@ class Issues extends React.Component { render() { const { sessionId, activeIssue, isModalDisplayed, projectsLoading, - fetchIssueLoading, issues, metaLoading, fetchIssuesLoading, issuesIntegration + fetchIssueLoading, issues, metaLoading, fetchIssuesLoading, provider } = this.props; const { showModal } = this.state; - const provider = issuesIntegration.provider return (
diff --git a/frontend/app/components/Session_/Network/Network.js b/frontend/app/components/Session_/Network/Network.js index 4c30e6f32..b3ea3dafb 100644 --- a/frontend/app/components/Session_/Network/Network.js +++ b/frontend/app/components/Session_/Network/Network.js @@ -158,30 +158,30 @@ export default class Network extends React.PureComponent { let filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ])); -// const referenceLines = []; -// if (domContentLoadedTime != null) { -// referenceLines.push({ -// time: domContentLoadedTime, -// color: DOM_LOADED_TIME_COLOR, -// }) -// } -// if (loadTime != null) { -// referenceLines.push({ -// time: loadTime, -// color: LOAD_TIME_COLOR, -// }) -// } -// -// let tabs = TABS; -// if (!fetchPresented) { -// tabs = TABS.map(tab => tab.key === XHR -// ? { -// text: renderXHRText(), -// key: XHR, -// } -// : tab -// ); -// } + const referenceLines = []; + if (domContentLoadedTime != null) { + referenceLines.push({ + time: domContentLoadedTime, + color: DOM_LOADED_TIME_COLOR, + }) + } + if (loadTime != null) { + referenceLines.push({ + time: loadTime, + color: LOAD_TIME_COLOR, + }) + } + + let tabs = TABS; + if (!fetchPresented) { + tabs = TABS.map(tab => tab.key === XHR + ? { + text: renderXHRText(), + key: XHR, + } + : tab + ); + } const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0); const transferredSize = filtered diff --git a/frontend/app/components/Session_/Network/NetworkContent.js b/frontend/app/components/Session_/Network/NetworkContent.js index 7eeebf538..1f10eaffe 100644 --- a/frontend/app/components/Session_/Network/NetworkContent.js +++ b/frontend/app/components/Session_/Network/NetworkContent.js @@ -168,13 +168,13 @@ export default class NetworkContent extends React.PureComponent { const referenceLines = []; if (domContentLoadedTime != null) { referenceLines.push({ - time: domContentLoadedTime.time, + time: domContentLoadedTime, color: DOM_LOADED_TIME_COLOR, }) } if (loadTime != null) { referenceLines.push({ - time: loadTime.time, + time: loadTime, color: LOAD_TIME_COLOR, }) } @@ -239,13 +239,13 @@ export default class NetworkContent extends React.PureComponent { /> diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 3589be148..aeab1af64 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -18,14 +18,12 @@ const getPointerIcon = (type) => { case 'log': return 'funnel/exclamation-circle'; case 'stack': - return 'funnel/patch-exclamation-fill'; + return 'funnel/file-exclamation'; case 'resource': return 'funnel/file-medical-alt'; case 'dead_click': return 'funnel/dizzy'; - case 'click_rage': - return 'funnel/dizzy'; case 'excessive_scrolling': return 'funnel/mouse'; case 'bad_request': @@ -63,7 +61,6 @@ const getPointerIcon = (type) => { fetchList: state.fetchList, })) @connect(state => ({ - issues: state.getIn([ 'sessions', 'current', 'issues' ]), showDevTools: state.getIn([ 'user', 'account', 'appearance', 'sessionsDevtools' ]), clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) && state.getIn([ 'sessions', 'current', 'clickRageTime' ]), @@ -98,7 +95,6 @@ export default class Timeline extends React.PureComponent { clickRageTime, stackList, fetchList, - issues } = this.props; const scale = 100 / endTime; @@ -128,28 +124,6 @@ export default class Timeline extends React.PureComponent { /> )) } - { - issues.map(iss => ( -
- - { iss.name } -
- } - /> -
- )) - } { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => (
- { "Exception" } + { "Exception:" }
{ e.message }
@@ -304,7 +278,7 @@ export default class Timeline extends React.PureComponent { icon={getPointerIcon('log')} content={
- { "Console" } + { "Console:" }
{ l.value }
@@ -406,7 +380,7 @@ export default class Timeline extends React.PureComponent { icon={getPointerIcon('fetch')} content={
- { "Failed Fetch" } + { "Failed Fetch:" }
{ e.name }
@@ -447,7 +421,7 @@ export default class Timeline extends React.PureComponent { icon={getPointerIcon('stack')} content={
- { "Stack Event" } + { "Stack Event:" }
{ e.name }
diff --git a/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js b/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js index 93a901f0a..dc58d8b81 100644 --- a/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js +++ b/frontend/app/components/Session_/StackEvents/UserEvent/UserEvent.js @@ -46,7 +46,7 @@ export default class UserEvent extends React.PureComponent { case STACKDRIVER: return ; default: - return ; + return ; } } diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.js b/frontend/app/components/Signup/SignupForm/SignupForm.js index 0a5de9507..b5ea500f3 100644 --- a/frontend/app/components/Signup/SignupForm/SignupForm.js +++ b/frontend/app/components/Signup/SignupForm/SignupForm.js @@ -137,7 +137,7 @@ export default class SignupForm extends React.Component {
-
By creating an account, you agree to our Terms of Service and Privacy Policy.
+
By creating an account, you agree to our Terms of Service and Privacy Policy
diff --git a/frontend/app/components/shared/BannerMessage/BannerMessage.js b/frontend/app/components/shared/BannerMessage/BannerMessage.js deleted file mode 100644 index d1d66b991..000000000 --- a/frontend/app/components/shared/BannerMessage/BannerMessage.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import { Icon } from 'UI' - -const BannerMessage= (props) => { - const { icon = 'info-circle', children } = props; - - return ( - <> -
-
-
-
- -
-
- {children} -
-
-
-
- - ) -} - -export default BannerMessage; \ No newline at end of file diff --git a/frontend/app/components/shared/BannerMessage/index.js b/frontend/app/components/shared/BannerMessage/index.js deleted file mode 100644 index 4d6ad92b8..000000000 --- a/frontend/app/components/shared/BannerMessage/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './BannerMessage' \ No newline at end of file diff --git a/frontend/app/components/shared/DateRange.js b/frontend/app/components/shared/DateRange.js index 7b627ab28..83feae0d8 100644 --- a/frontend/app/components/shared/DateRange.js +++ b/frontend/app/components/shared/DateRange.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; function DateRange (props) { - const { startDate, endDate, rangeValue, className, onDateChange, customRangeRight=false } = props; + const { startDate, endDate, rangeValue, className, onDateChange } = props; return ( ); } diff --git a/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js b/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js index f29d47745..4165506d4 100644 --- a/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js +++ b/frontend/app/components/shared/DateRangeDropdown/DateRangeDropdown.js @@ -66,7 +66,7 @@ export default class DateRangeDropdown extends React.PureComponent { } render() { - const { customRangeRight, button = false, className, direction = 'right', customHidden=false, show30Minutes=false } = this.props; + const { button = false, className, direction = 'right', customHidden=false, show30Minutes=false } = this.props; const { showDateRangePopup, value, range } = this.state; let options = getDateRangeOptions(range); @@ -108,7 +108,7 @@ export default class DateRangeDropdown extends React.PureComponent { { showDateRangePopup && -
+
{ @@ -9,10 +9,7 @@ export default function DocLink({ className = '', url, label }) { return (
) diff --git a/frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js b/frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js deleted file mode 100644 index c308f33bf..000000000 --- a/frontend/app/components/shared/IntegrateSlackButton/IntegrateSlackButton.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import { IconButton } from 'UI' -import { CLIENT_TABS, client as clientRoute } from 'App/routes'; -import { withRouter } from 'react-router-dom'; - -function IntegrateSlackButton({ history, tenantId }) { - const gotoPreferencesIntegrations = () => { - history.push(clientRoute(CLIENT_TABS.INTEGRATIONS)); - } - - return ( -
- -
- ) -} - -export default withRouter(connect(state => ({ - tenantId: state.getIn([ 'user', 'client', 'tenantId' ]), -}))(IntegrateSlackButton)) diff --git a/frontend/app/components/shared/IntegrateSlackButton/index.js b/frontend/app/components/shared/IntegrateSlackButton/index.js deleted file mode 100644 index f2f8f2e16..000000000 --- a/frontend/app/components/shared/IntegrateSlackButton/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './IntegrateSlackButton' \ No newline at end of file diff --git a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js index cee55088b..a3011db44 100644 --- a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js +++ b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js @@ -23,7 +23,7 @@ const NoSessionsMessage= (props) => {
- It takes a few minutes for first recordings to appear. All set but they are still not showing up? Check our troubleshooting section. + It takes a few minutes for first recordings to appear. All set but they are still not showing up? Check our troubleshooting section.
diff --git a/frontend/app/components/shared/SharePopup/SharePopup.js b/frontend/app/components/shared/SharePopup/SharePopup.js index 347a95733..845229034 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.js +++ b/frontend/app/components/shared/SharePopup/SharePopup.js @@ -4,7 +4,6 @@ import withRequest from 'HOCs/withRequest'; import { Popup, Dropdown, Icon, IconButton } from 'UI'; import { pause } from 'Player'; import styles from './sharePopup.css'; -import IntegrateSlackButton from '../IntegrateSlackButton/IntegrateSlackButton'; @connect(state => ({ channels: state.getIn([ 'slack', 'list' ]), @@ -19,7 +18,7 @@ export default class SharePopup extends React.PureComponent { state = { comment: '', isOpen: false, - channelId: this.props.channels.getIn([ 0, 'webhookId' ]), + channelId: this.props.channels.getIn([ 0, 'id' ]), } editMessage = e => this.setState({ comment: e.target.value }) @@ -46,10 +45,10 @@ export default class SharePopup extends React.PureComponent { changeChannel = (e, { value }) => this.setState({ channelId: value }) render() { - const { trigger, loading, channels } = this.props; + const { trigger, loading, channels, tenantId } = this.props; const { comment, isOpen, channelId } = this.state; - const options = channels.map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS(); + const options = channels.map(({ id, name }) => ({ value: id, text: name })).toJS(); return ( { options.length === 0 ?
- + + +
:
diff --git a/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js index 350378cf4..bacb71bfe 100644 --- a/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -27,10 +27,9 @@ const codeSnippet = ` r.setMetadata=function(k,v){r.push([4,k,v])}; r.event=function(k,p,i){r.push([5,k,p,i])}; r.issue=function(k,p){r.push([6,k,p])}; - r.isActive=function(){return false}; - r.getSessionToken=function(){}; - r.i="https://${window.location.hostname}/ingest"; -})(0, "PROJECT_KEY", "//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",1,XXX); + r.isActive=r.active=function(){return false}; + r.getSessionToken=r.sessionID=function(){}; +})(0,PROJECT_HASH,"//${window.location.hostname}/static/openreplay.js",1,XXX); `; diff --git a/frontend/app/components/ui/ErrorDetails/ErrorDetails.js b/frontend/app/components/ui/ErrorDetails/ErrorDetails.js index 2a6afdd1e..68f48cb82 100644 --- a/frontend/app/components/ui/ErrorDetails/ErrorDetails.js +++ b/frontend/app/components/ui/ErrorDetails/ErrorDetails.js @@ -4,7 +4,7 @@ import cn from 'classnames'; import { IconButton, Icon } from 'UI'; import { connect } from 'react-redux'; -const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps'; +const docLink = 'https://docs.openreplay.com/plugins/sourcemaps'; function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }) { const [showRaw, setShowRaw] = useState(false) diff --git a/frontend/app/duck/assignments.js b/frontend/app/duck/assignments.js index b48e2ff45..c6c7d3cda 100644 --- a/frontend/app/duck/assignments.js +++ b/frontend/app/duck/assignments.js @@ -5,7 +5,6 @@ import withRequestState, { RequestTypes } from './requestStateCreator'; import { createListUpdater, createItemInListUpdater } from './funcTools/tools'; import { editType, initType } from './funcTools/crud/types'; import { createInit, createEdit } from './funcTools/crud'; -import IssuesType from 'Types/issue/issuesType' const idKey = 'id'; const name = 'assignment'; @@ -42,20 +41,17 @@ const reducer = (state = initialState, action = {}) => { return state.mergeIn([ 'instance' ], action.instance); case FETCH_PROJECTS.SUCCESS: return state.set('projects', List(action.data)).set('projectsFetched', true); - case FETCH_ASSIGNMENTS.SUCCESS: - return state.set('list', List(action.data.issues).map(Assignment)); + case FETCH_ASSIGNMENTS.SUCCESS: + return state.set('list', List(action.data).map(Assignment)); case FETCH_ASSIGNMENT.SUCCESS: return state.set('activeIssue', Assignment({ ...action.data, users})); case FETCH_META.SUCCESS: - issueTypes = List(action.data.issueTypes).map(IssuesType); + issueTypes = action.data.issueTypes; var issueTypeIcons = {} - // for (var i =0; i < issueTypes.length; i++) { - // issueTypeIcons[issueTypes[i].id] = issueTypes[i].iconUrl - // } - issueTypes.forEach(iss => { - issueTypeIcons[iss.id] = iss.iconUrl - }) - return state.set('issueTypes', issueTypes) + for (var i =0; i < issueTypes.length; i++) { + issueTypeIcons[issueTypes[i].id] = issueTypes[i].iconUrl + } + return state.set('issueTypes', List(issueTypes)) .set('users', List(action.data.users)) .set('issueTypeIcons', issueTypeIcons) case ADD_ACTIVITY.SUCCESS: diff --git a/frontend/app/duck/integrations/slack.js b/frontend/app/duck/integrations/slack.js index e4c2803ff..1d59bc16b 100644 --- a/frontend/app/duck/integrations/slack.js +++ b/frontend/app/duck/integrations/slack.js @@ -4,7 +4,6 @@ import Config from 'Types/integrations/slackConfig'; import { createItemInListUpdater } from '../funcTools/tools'; const SAVE = new RequestTypes('slack/SAVE'); -const UPDATE = new RequestTypes('slack/UPDATE'); const REMOVE = new RequestTypes('slack/REMOVE'); const FETCH_LIST = new RequestTypes('slack/FETCH_LIST'); const EDIT = 'slack/EDIT'; @@ -21,7 +20,6 @@ const reducer = (state = initialState, action = {}) => { switch (action.type) { case FETCH_LIST.SUCCESS: return state.set('list', List(action.data).map(Config)); - case UPDATE.SUCCESS: case SAVE.SUCCESS: const config = Config(action.data); return state @@ -59,13 +57,6 @@ export function save(instance) { }; } -export function update(instance) { - return { - types: UPDATE.toArray(), - call: client => client.put(`/integrations/slack/${instance.webhookId}`, instance.toData()), - }; -} - export function edit(instance) { return { type: EDIT, diff --git a/frontend/app/duck/user.js b/frontend/app/duck/user.js index deb41e715..f7ed2b1a2 100644 --- a/frontend/app/duck/user.js +++ b/frontend/app/duck/user.js @@ -19,7 +19,6 @@ const PUT_CLIENT = new RequestTypes('user/PUT_CLIENT'); const PUSH_NEW_SITE = 'user/PUSH_NEW_SITE'; const SET_SITE_ID = 'user/SET_SITE_ID'; -const SET_ONBOARDING = 'user/SET_ONBOARDING'; const SITE_ID_STORAGE_KEY = "__$user-siteId$__"; const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY); @@ -30,8 +29,7 @@ const initialState = Map({ siteId: null, passwordRequestError: false, passwordErrors: List(), - tenants: [], - onboarding: false + tenants: [] }); const setClient = (state, data) => { @@ -49,21 +47,17 @@ const setClient = (state, data) => { const reducer = (state = initialState, action = {}) => { switch (action.type) { - case UPDATE_PASSWORD.SUCCESS: + case SIGNUP.SUCCESS: case LOGIN.SUCCESS: return setClient( state.set('account', Account(action.data.user)), action.data.client, ); - case SIGNUP.SUCCESS: - return setClient( - state.set('account', Account(action.data.user)), - action.data.client, - ).set('onboarding', true); case REQUEST_RESET_PASSWORD.SUCCESS: break; case UPDATE_APPEARANCE.REQUEST: //TODO: failure handling return state.mergeIn([ 'account', 'appearance' ], action.appearance) + case UPDATE_PASSWORD.SUCCESS: case UPDATE_ACCOUNT.SUCCESS: case FETCH_ACCOUNT.SUCCESS: return state.set('account', Account(action.data)).set('passwordErrors', List()); @@ -83,8 +77,6 @@ const reducer = (state = initialState, action = {}) => { case PUSH_NEW_SITE: return state.updateIn([ 'client', 'sites' ], list => list.push(action.newSite)); - case SET_ONBOARDING: - return state.set('onboarding', action.state) } return state; }; @@ -195,11 +187,3 @@ export function pushNewSite(newSite) { newSite, }; } - -export function setOnboarding(state = false) { - return { - type: SET_ONBOARDING, - state - }; -} - diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.js b/frontend/app/player/MessageDistributor/MessageDistributor.js index c21d54ccb..fa3641b7a 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.js +++ b/frontend/app/player/MessageDistributor/MessageDistributor.js @@ -10,7 +10,7 @@ import ReduxAction from 'Types/session/reduxAction'; import { update } from '../store'; import { - init as initListsDepr, + init as initLists, append as listAppend, setStartTime as setListsStartTime } from '../lists'; @@ -43,14 +43,6 @@ export const INITIAL_STATE = { skipIntervals: [], } -function initLists() { - const lists = {}; - for (var i = 0; i < LIST_NAMES.length; i++) { - lists[ LIST_NAMES[i] ] = new ListWalker(); - } - return lists; -} - import type { Message, @@ -86,7 +78,16 @@ export default class MessageDistributor extends StatedScreen { #scrollManager: ListWalker = new ListWalker(); #decoder = new Decoder(); - #lists = initLists(); + #lists = { + redux: new ListWalker(), + mobx: new ListWalker(), + vuex: new ListWalker(), + ngrx: new ListWalker(), + graphql: new ListWalker(), + exceptions: new ListWalker(), + profiles: new ListWalker(), + longtasks: new ListWalker(), + } #activirtManager: ActivityManager; @@ -105,7 +106,7 @@ export default class MessageDistributor extends StatedScreen { /* == REFACTOR_ME == */ const eventList = sess.events.toJSON(); - initListsDepr({ + initLists({ event: eventList, stack: sess.stackEvents.toJSON(), resource: sess.resources.toJSON(), @@ -235,16 +236,10 @@ export default class MessageDistributor extends StatedScreen { const llEvent = this.#locationEventManager.moveToLast(t, index); if (!!llEvent) { if (llEvent.domContentLoadedTime != null) { - stateToUpdate.domContentLoadedTime = { - time: llEvent.domContentLoadedTime + this.#navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & setLocation: add navigationStart to db) - value: llEvent.domContentLoadedTime, - } + stateToUpdate.domContentLoadedTime = llEvent.domContentLoadedTime + this.#navigationStartOffset; } if (llEvent.loadTime != null) { - stateToUpdate.loadTime = { - time: llEvent.loadTime + this.#navigationStartOffset, - value: llEvent.loadTime, - } + stateToUpdate.loadTime = llEvent.domContentLoadedTime + this.#navigationStartOffset } if (llEvent.domBuildingTime != null) { stateToUpdate.domBuildingTime = llEvent.domBuildingTime; diff --git a/frontend/app/svg/icons/funnel/cpu.svg b/frontend/app/svg/icons/funnel/cpu.svg deleted file mode 100644 index f4922a33f..000000000 --- a/frontend/app/svg/icons/funnel/cpu.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/dizzy.svg b/frontend/app/svg/icons/funnel/dizzy.svg deleted file mode 100644 index 4f026cd64..000000000 --- a/frontend/app/svg/icons/funnel/dizzy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/emoji-angry.svg b/frontend/app/svg/icons/funnel/emoji-angry.svg deleted file mode 100644 index e9c147cb9..000000000 --- a/frontend/app/svg/icons/funnel/emoji-angry.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/file-earmark-break.svg b/frontend/app/svg/icons/funnel/file-earmark-break.svg deleted file mode 100644 index 244e6b211..000000000 --- a/frontend/app/svg/icons/funnel/file-earmark-break.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/image.svg b/frontend/app/svg/icons/funnel/image.svg deleted file mode 100644 index 36bd4649f..000000000 --- a/frontend/app/svg/icons/funnel/image.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/app/svg/icons/funnel/sd-card.svg b/frontend/app/svg/icons/funnel/sd-card.svg index 4e55e699b..8d4991cb1 100644 --- a/frontend/app/svg/icons/funnel/sd-card.svg +++ b/frontend/app/svg/icons/funnel/sd-card.svg @@ -1,4 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/app/types/account/account.js b/frontend/app/types/account/account.js index fa672dfcc..42b0e9ac3 100644 --- a/frontend/app/types/account/account.js +++ b/frontend/app/types/account/account.js @@ -10,7 +10,6 @@ export default Member.extend({ banner: undefined, email: '', verifiedEmail: undefined, - smtp: false, license: '', expirationDate: undefined, }, { diff --git a/frontend/app/types/integrations/issueTracker.js b/frontend/app/types/integrations/issueTracker.js index c03ed1005..ef53b01f1 100644 --- a/frontend/app/types/integrations/issueTracker.js +++ b/frontend/app/types/integrations/issueTracker.js @@ -7,6 +7,7 @@ export const ACCESS_KEY_ID_LENGTH = 20; export default Record({ username: undefined, token: undefined, + provider: undefined, url: undefined, provider: 'jira' }, { diff --git a/frontend/app/types/issue/issuesType.js b/frontend/app/types/issue/issuesType.js index c40864bea..1f0679d5a 100644 --- a/frontend/app/types/issue/issuesType.js +++ b/frontend/app/types/issue/issuesType.js @@ -2,16 +2,7 @@ import Record from 'Types/Record'; export default Record({ id: undefined, - color: undefined, - description: '', name: undefined, iconUrl: undefined }, { - fromJS: ({ iconUrl, color, ...issueType }) => ({ - ...issueType, - color, - iconUrl: iconUrl ? - : -
, - }), }) diff --git a/frontend/app/types/session/issue.js b/frontend/app/types/session/issue.js deleted file mode 100644 index 87878b6bb..000000000 --- a/frontend/app/types/session/issue.js +++ /dev/null @@ -1,43 +0,0 @@ -import Record from 'Types/Record'; -import { List } from 'immutable'; -import Watchdog from 'Types/watchdog' - -export const issues_types = List([ - { 'type': 'js_exception', 'visible': true, 'order': 0, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' }, - { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, - { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' }, - { 'type': 'click_rage', 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/dizzy' }, - { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/emoji-angry' }, - { 'type': 'memory', 'visible': true, 'order': 5, 'name': 'High Memory', 'icon': 'funnel/sd-card' }, - { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' }, - { 'type': 'crash', 'visible': true, 'order': 7, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' }, - { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' } -]).map(Watchdog) - -export const issues_types_map = {} -issues_types.forEach(i => { - issues_types_map[i.type] = { type: i.type, visible: i.visible, order: i.order, name: i.name, } -}); - -export default Record({ - issueId: undefined, - name: '', - visible: true, - sessionId: undefined, - time: undefined, - seqIndex: undefined, - payload: {}, - projectId: undefined, - type: '', - contextString: '', - context: '', - icon: 'info' -}, { - idKey: 'issueId', - fromJS: ({ type, ...rest }) => ({ - ...rest, - type, - icon: issues_types_map[type].icon, - name: issues_types_map[type].name, - }), -}); diff --git a/frontend/app/types/session/session.js b/frontend/app/types/session/session.js index 132afcc7d..daa2f4ae0 100644 --- a/frontend/app/types/session/session.js +++ b/frontend/app/types/session/session.js @@ -7,7 +7,7 @@ import StackEvent from './stackEvent'; import Resource from './resource'; import CustomField from './customField'; import SessionError from './error'; -import Issue from './issue'; + const SOURCE_JS = 'js_exception'; @@ -66,7 +66,6 @@ export default Record({ errorsCount: 0, watchdogs: [], issueTypes: [], - issues: [], userDeviceHeapSize: 0, userDeviceMemorySize: 0, errors: List(), @@ -81,7 +80,6 @@ export default Record({ projectId, errors, stackEvents = [], - issues = [], ...session }) => { const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration); @@ -111,10 +109,6 @@ export default Record({ .map(se => StackEvent({ ...se, time: se.timestamp - startedAt })); const exceptions = List(errors) .map(SessionError) - - const issuesList = List(issues) - .map(e => Issue({ ...e, time: e.timestamp - startedAt })) - return { ...session, isIOS: session.platform === "ios", @@ -134,7 +128,6 @@ export default Record({ userNumericHash: hashString(session.userId || session.userAnonymousId || session.userUuid || ""), userDisplayName: session.userId || session.userAnonymousId || 'Anonymous User', firstResourceTime, - issues: issuesList, }; }, idKey: "sessionId", diff --git a/frontend/app/types/watchdog.js b/frontend/app/types/watchdog.js index fdc30dfb2..76d874c64 100644 --- a/frontend/app/types/watchdog.js +++ b/frontend/app/types/watchdog.js @@ -22,6 +22,7 @@ const WATCHDOG_TYPES = [ ] export const names = { + // 'all' : { label: 'All', icon: 'all' }, 'js_exception' : { label: 'JS Exceptions', icon: 'funnel/exclamation-circle' }, 'bad_request': { label: 'Bad Request', icon: 'funnel/patch-exclamation-fill' }, 'missing_resource': { label: 'Missing Resources', icon: 'funnel/image-fill' }, @@ -32,6 +33,13 @@ export const names = { 'cpu': { label: 'CPU', icon: 'funnel/hdd-fill' }, 'dead_click': { label: 'Dead Click', icon: 'funnel/emoji-dizzy-fill' }, 'custom': { label: 'Custom', icon: 'funnel/exclamation-circle-fill' }, + + // 'errors' : { label: 'Errors', icon: 'console/error' }, + // 'missing_image': { label: 'Missing Images', icon: 'image' }, + // 'slow_session': { label: 'Slow Sessions', icon: 'turtle' }, + // 'high_engagement': { label: 'High Engagements', icon: 'high-engagement' }, + // 'performance_issues': { label: 'Mem/CPU Issues', icon: 'tachometer-slowest' }, + // 'default': { label: 'Default', icon: 'window-alt' }, } const CONJUGATED_ISSUE_TYPES = { @@ -85,6 +93,8 @@ export default Record({ } }, fromJS: (item) => ({ - ...item + ...item, + name: item.name, + icon: names[item.type] ? names[item.type].icon : 'turtle' }), }); diff --git a/frontend/env.js b/frontend/env.js index 7c8c52d2b..fdc173aeb 100644 --- a/frontend/env.js +++ b/frontend/env.js @@ -13,7 +13,7 @@ const oss = { ORIGIN: () => 'window.location.origin', API_EDP: () => 'window.location.origin + "/api"', ASSETS_HOST: () => 'window.location.origin + "/assets"', - VERSION: '1.0.0', + VERSION: '1.0.1', SOURCEMAP: true, MINIO_ENDPOINT: process.env.MINIO_ENDPOINT, MINIO_PORT: process.env.MINIO_PORT, diff --git a/scripts/helm/README.md b/scripts/helm/README.md index 4d5c7e54e..1a79331a0 100644 --- a/scripts/helm/README.md +++ b/scripts/helm/README.md @@ -1,36 +1,48 @@ -## Helm charts for installing OpenReplay components +## Helm charts for installing openreplay components. Installation components are separated by namepaces. **Namespace:** -- **app:** Core OpenReplay application related components. - - alerts - - assets +- **app:** Core openreplay application related components. + - alert + - auth + - cache - chalice + - clickhouse - ender - - sink - - storage + - events + - failover + - filesink + - filestorage - http - integrations - - db + - ios-proxy + - metadata + - negative + - pg-stateless + - pg + - preprocessing + - redis + - ws - **db:** Contains following databases and backend components. - - kafka (ee) + - kafka - redis - postgresql - - clickhouse (ee) + - clickhouse - minio + - sqs - nfs-server -- **longhorn:** Storage solution for kubernetes PVs. +- **longhorn:** On-Prem storage solution for kubernetes PVs. - **nginx-ingress:** Nginx ingress for internet traffic to enter the kubernetes cluster. **Scripts:** - **install.sh** - Installs OpenReplay in a single node machine, for trial runs / demo. + Installs openreplay in a single node machine, for trial runs / demo. This script is a wrapper around the `install.sh` with [k3s](https://k3s.io/) as kubernetes distro. @@ -38,8 +50,8 @@ Installation components are separated by namepaces. - **kube-install.sh:** - Installs OpenReplay on any given kubernetes cluster. Has 3 configuration types: - - small (2cores 8G RAM) + Installs openreplay on any given kubernetes cluster. Has 3 configuration types + - small (4cores 8G RAM) - medium (4cores 16G RAM) - recommened (8cores 32G RAM) diff --git a/scripts/helm/app/README.md b/scripts/helm/app/README.md index a5b73f915..e5faed535 100644 --- a/scripts/helm/app/README.md +++ b/scripts/helm/app/README.md @@ -1,14 +1,13 @@ -## Core OpenReplay application configuration folder +## Core Openreplay application configuration folder - This folder contains configuration for core OpenReplay apps. All applications share common helm chart named *openreplay* which can be overridden by `.yaml` file. + This folder contains configuration for core openreplay apps. All applications share common helm chart named *openreplay* which can be overridden by `.yaml` file. **Below is a sample template.** ```yaml - namespace: app # In which namespace alerts runs. + namespace: app # In which namespace alert runs. image: - repository: rg.fr-par.scw.cloud/foss # Which image to use - name: alerts + repository: 998611063711.dkr.ecr.eu-central-1.amazonaws.com/alert # Which image to use pullPolicy: IfNotPresent tag: "latest" # Overrides the image tag whose default is the chart appVersion. @@ -31,7 +30,7 @@ # env vars for the application env: - ALERT_NOTIFICATION_STRING: http://chalice-openreplay.app.svc.cluster.local:8000/alerts/notifications + ALERT_NOTIFICATION_STRING: https://parrot.openreplay.io/alerts/notifications CLICKHOUSE_STRING: tcp://clickhouse.db.svc.cluster.local:9000/default POSTGRES_STRING: postgres://postgresql.db.svc.cluster.local:5432 ``` diff --git a/scripts/helm/app/alerts.yaml b/scripts/helm/app/alerts.yaml index 4bb397526..4fe30c3cc 100644 --- a/scripts/helm/app/alerts.yaml +++ b/scripts/helm/app/alerts.yaml @@ -1,7 +1,7 @@ namespace: app image: repository: rg.fr-par.scw.cloud/foss - name: alerts + name: alert pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "latest" @@ -22,6 +22,6 @@ resources: memory: 128Mi env: - ALERT_NOTIFICATION_STRING: http://chalice-openreplay.app.svc.cluster.local:8000/alerts/notifications + ALERT_NOTIFICATION_STRING: https://parrot.asayer.io/alerts/notifications CLICKHOUSE_STRING: tcp://clickhouse.db.svc.cluster.local:9000/default POSTGRES_STRING: postgres://postgres:asayerPostgres@postgresql.db.svc.cluster.local:5432 diff --git a/scripts/helm/app/assets.yaml b/scripts/helm/app/assets.yaml index c7b740e22..390fe4e07 100644 --- a/scripts/helm/app/assets.yaml +++ b/scripts/helm/app/assets.yaml @@ -22,8 +22,8 @@ resources: memory: 128Mi env: - ASSETS_ORIGIN: /sessions-assets # TODO: full path (with the minio prefix) - S3_BUCKET_ASSETS: sessions-assets + ASSETS_ORIGIN: /asayer-sessions-assets # TODO: full path (with the minio prefix) + S3_BUCKET_ASSETS: asayer-sessions-assets AWS_ENDPOINT: http://minio.db.svc.cluster.local:9000 AWS_ACCESS_KEY_ID: "minios3AccessKeyS3cr3t" AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" diff --git a/scripts/helm/app/chalice.yaml b/scripts/helm/app/chalice.yaml index 879bb945f..9a02adfaa 100644 --- a/scripts/helm/app/chalice.yaml +++ b/scripts/helm/app/chalice.yaml @@ -40,6 +40,9 @@ env: sessions_region: us-east-1 put_S3_TTL: '20' sourcemaps_bucket: sourcemaps + sourcemaps_bucket_key: minios3AccessKeyS3cr3t + sourcemaps_bucket_secret: m1n10s3CretK3yPassw0rd + sourcemaps_bucket_region: us-east-1 js_cache_bucket: sessions-assets async_Token: '' EMAIL_HOST: '' @@ -53,7 +56,7 @@ env: EMAIL_FROM: OpenReplay SITE_URL: '' announcement_url: '' - jwt_secret: "SetARandomStringHere" + jwt_secret: SET A RANDOM STRING HERE jwt_algorithm: HS512 jwt_exp_delta_seconds: '2592000' # Override with your https://domain_name diff --git a/scripts/helm/app/http.yaml b/scripts/helm/app/http.yaml index 82342c171..d2788970c 100644 --- a/scripts/helm/app/http.yaml +++ b/scripts/helm/app/http.yaml @@ -22,9 +22,9 @@ resources: memory: 128Mi env: - ASSETS_ORIGIN: /sessions-assets # TODO: full path (with the minio prefix) + ASSETS_ORIGIN: /asayer-sessions-assets # TODO: full path (with the minio prefix) TOKEN_SECRET: secret_token_string # TODO: generate on buld - S3_BUCKET_IMAGES_IOS: sessions-mobile-assets + S3_BUCKET_IMAGES_IOS: asayer-sessions-mobile-assets AWS_ACCESS_KEY_ID: "minios3AccessKeyS3cr3t" AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" AWS_REGION: us-east-1 diff --git a/scripts/helm/app/issues.md b/scripts/helm/app/issues.md new file mode 100644 index 000000000..06a6cb91f --- /dev/null +++ b/scripts/helm/app/issues.md @@ -0,0 +1,76 @@ +i [X] alert: + - [X] postgresql app db not found + public.alerts relation doesn't exist +- [X] cache: + - [X] connecting kafka with ssl:// +- [X] events: + - [X] postgresql app db not found + ``` + ERROR: relation "integrations" does not exist (SQLSTATE 42P01) + ``` +- [X] failover: asayer no error logs + - [X] Redis error: NOAUTH Authentication required. + redis cluster should not have password + - [X] Redis has cluster support disabled +- [X] redis-asayer: + - [X] /root/workers/redis/main.go:29: Redis error: no pools available + - [X] /root/workers/pg/main.go:49: Redis error: no cluster slots assigned +- [X] ws-asayer: + - [X] Redis has cluster support disabled +- [X] ender: + - [X] /root/pkg/kafka/consumer.go:95: Consumer error: Subscribed topic not available: ^(raw)$: Broker: Unknown topic or partition + - [X] kafka ssl +- [X] preprocessor: + - [X] kafka ssl +- [X] clickhouse-asayer: + - [X] Table default.sessions doesn't exist. +- [ ] puppeteer: + - [ ] Image not found + ``` + repository 998611063711.dkr.ecr.eu-central-1.amazonaws.com/puppeteer-jasmine not found: name unknown: The repository with name 'puppeteer-jasmine' does not exist in the registry with id '998611063711 + Back-off pulling image "998611063711.dkr.ecr.eu-central-1.amazonaws.com/puppeteer-jasmine:latest" + ``` +- [o] negative: + - [X] Clickhouse prepare error: code: 60, message: Table default.negatives_buffer doesn't exist. + - [ ] kafka ssl issue +- [o] metadata: + - [X] code: 60, message: Table default.sessions_metadata doesn't exist. + - [ ] /root/workers/metadata/main.go:96: Consumer Commit error: Local: No offset stored +- [ ] http: + - [ ] /root/pkg/env/worker_id.go:8: Get : unsupported protocol scheme "" +- [o] chalice: + - [X] No code to start + - [X] first install deps + - [X] then install chalice + - [X] sqs without creds + - [ ] do we need dead-runs as aws put failed in deadruns Q + - [ ] do we have to limit for parallel runs / the retries ? + +## Talk with Mehdi and Sacha +- [X] Do we need new app or old +- [X] in new we don't need redis. so what should we do ? + +# 3 new workers + +This is not in prod +kafka-staging take the new by compare with prod + +1. ender sasha +2. pg_stateless sasha +3. http sasha +4. changed preprocessing: david +5. ios proxy: taha + +Application loadbalancer + +domain: ingest.asayer.io + +ingress with ssl termination. + ios proxy ( in ecs ) + oauth + ws + api + http ( sasha ) + +ws lb with ssl: + ingress diff --git a/scripts/helm/app/openreplay/templates/deployment.yaml b/scripts/helm/app/openreplay/templates/deployment.yaml index a2259a852..12e90714a 100644 --- a/scripts/helm/app/openreplay/templates/deployment.yaml +++ b/scripts/helm/app/openreplay/templates/deployment.yaml @@ -14,9 +14,8 @@ spec: {{- include "openreplay.selectorLabels" . | nindent 6 }} template: metadata: - annotations: - openreplayRolloutID: {{ randAlphaNum 5 | quote }} # Restart nginx after every deployment {{- with .Values.podAnnotations }} + annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: diff --git a/scripts/helm/app/openreplay/values.yaml b/scripts/helm/app/openreplay/values.yaml index 498f88801..e93d4d44d 100644 --- a/scripts/helm/app/openreplay/values.yaml +++ b/scripts/helm/app/openreplay/values.yaml @@ -1,4 +1,4 @@ -# Default values for OpenReplay. +# Default values for openreplay. # This is a YAML-formatted file. # Declare variables to be passed into your templates. diff --git a/scripts/helm/app/storage.yaml b/scripts/helm/app/storage.yaml index aebc2f3e8..18890847a 100644 --- a/scripts/helm/app/storage.yaml +++ b/scripts/helm/app/storage.yaml @@ -34,10 +34,10 @@ env: AWS_ENDPOINT: http://minio.db.svc.cluster.local:9000 AWS_ACCESS_KEY_ID: "minios3AccessKeyS3cr3t" AWS_SECRET_ACCESS_KEY: "m1n10s3CretK3yPassw0rd" - AWS_REGION_WEB: us-east-1 - AWS_REGION_IOS: us-east-1 - S3_BUCKET_WEB: mobs - S3_BUCKET_IOS: mobs + AWS_REGION_WEB: eu-central-1 + AWS_REGION_IOS: eu-central-1 + S3_BUCKET_WEB: asayer-mobs + S3_BUCKET_IOS: asayer-mobs # REDIS_STRING: redis-master.db.svc.cluster.local:6379 KAFKA_SERVERS: kafka.db.svc.cluster.local:9092 diff --git a/scripts/helm/db/init_dbs/postgresql/init_schema.sql b/scripts/helm/db/init_dbs/postgresql/init_schema.sql index 83afd3ba0..ed1449309 100644 --- a/scripts/helm/db/init_dbs/postgresql/init_schema.sql +++ b/scripts/helm/db/init_dbs/postgresql/init_schema.sql @@ -1,9 +1,8 @@ BEGIN; --- --- public.sql --- CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS pgcrypto; --- --- accounts.sql --- + CREATE OR REPLACE FUNCTION generate_api_key(length integer) RETURNS text AS $$ @@ -34,7 +33,7 @@ CREATE TABLE public.tenants created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), edition varchar(3) NOT NULL, version_number text NOT NULL, - license text NULL, + licence text NULL, opt_out bool NOT NULL DEFAULT FALSE, t_projects integer NOT NULL DEFAULT 1, t_sessions bigint NOT NULL DEFAULT 0, @@ -129,6 +128,7 @@ CREATE TABLE basic_authentication token_requested_at timestamp without time zone NULL DEFAULT NULL, changed_at timestamp, UNIQUE (user_id) + -- CHECK ((token IS NULL and token_requested_at IS NULL) or (token IS NOT NULL and token_requested_at IS NOT NULL)) ); CREATE TYPE oauth_provider AS ENUM ('jira', 'github'); @@ -138,32 +138,32 @@ CREATE TABLE oauth_authentication provider oauth_provider NOT NULL, provider_user_id text NOT NULL, token text NOT NULL, - UNIQUE (user_id, provider) + UNIQUE (provider, provider_user_id) ); --- --- projects.sql --- + CREATE TABLE projects ( project_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - project_key varchar(20) NOT NULL UNIQUE DEFAULT generate_api_key(20), + project_key varchar(20) NOT NULL UNIQUE DEFAULT generate_api_key(20), name text NOT NULL, active boolean NOT NULL, - sample_rate smallint NOT NULL DEFAULT 100 CHECK (sample_rate >= 0 AND sample_rate <= 100), - created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), - deleted_at timestamp without time zone NULL DEFAULT NULL, - max_session_duration integer NOT NULL DEFAULT 7200000, - metadata_1 text DEFAULT NULL, - metadata_2 text DEFAULT NULL, - metadata_3 text DEFAULT NULL, - metadata_4 text DEFAULT NULL, - metadata_5 text DEFAULT NULL, - metadata_6 text DEFAULT NULL, - metadata_7 text DEFAULT NULL, - metadata_8 text DEFAULT NULL, - metadata_9 text DEFAULT NULL, - metadata_10 text DEFAULT NULL, - gdpr jsonb NOT NULL DEFAULT '{ + sample_rate smallint NOT NULL DEFAULT 100 CHECK (sample_rate >= 0 AND sample_rate <= 100), + created_at timestamp without time zone NOT NULL DEFAULT (now() at time zone 'utc'), + deleted_at timestamp without time zone NULL DEFAULT NULL, + max_session_duration integer NOT NULL DEFAULT 7200000, + metadata_1 text DEFAULT NULL, + metadata_2 text DEFAULT NULL, + metadata_3 text DEFAULT NULL, + metadata_4 text DEFAULT NULL, + metadata_5 text DEFAULT NULL, + metadata_6 text DEFAULT NULL, + metadata_7 text DEFAULT NULL, + metadata_8 text DEFAULT NULL, + metadata_9 text DEFAULT NULL, + metadata_10 text DEFAULT NULL, + gdpr jsonb NOT NULL DEFAULT '{ "maskEmails": true, "sampleRate": 33, "maskNumbers": false, @@ -185,70 +185,6 @@ CREATE TRIGGER on_insert_or_update FOR EACH ROW EXECUTE PROCEDURE notify_project(); --- --- alerts.sql --- - -CREATE TYPE alert_detection_method AS ENUM ('threshold', 'change'); - -CREATE TABLE alerts -( - alert_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - name text NOT NULL, - description text NULL DEFAULT NULL, - active boolean NOT NULL DEFAULT TRUE, - detection_method alert_detection_method NOT NULL, - query jsonb NOT NULL, - deleted_at timestamp NULL DEFAULT NULL, - created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), - options jsonb NOT NULL DEFAULT '{ - "renotifyInterval": 1440 - }'::jsonb -); - - -CREATE OR REPLACE FUNCTION notify_alert() RETURNS trigger AS -$$ -DECLARE - clone jsonb; -BEGIN - clone = to_jsonb(NEW); - clone = jsonb_set(clone, '{created_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.created_at) * 1000 AS BIGINT))); - IF NEW.deleted_at NOTNULL THEN - clone = jsonb_set(clone, '{deleted_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.deleted_at) * 1000 AS BIGINT))); - END IF; - PERFORM pg_notify('alert', clone::text); - RETURN NEW; -END ; -$$ LANGUAGE plpgsql; - - -CREATE TRIGGER on_insert_or_update_or_delete - AFTER INSERT OR UPDATE OR DELETE - ON alerts - FOR EACH ROW -EXECUTE PROCEDURE notify_alert(); - --- --- webhooks.sql --- - -create type webhook_type as enum ('webhook', 'slack', 'email'); - -create table webhooks -( - webhook_id integer generated by default as identity - constraint webhooks_pkey - primary key, - endpoint text not null, - created_at timestamp default timezone('utc'::text, now()) not null, - deleted_at timestamp, - auth_header text, - type webhook_type not null, - index integer default 0 not null, - name varchar(100) -); - - --- --- notifications.sql --- - CREATE TABLE notifications ( notification_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, @@ -273,23 +209,6 @@ CREATE TABLE user_viewed_notifications constraint user_viewed_notifications_pkey primary key (user_id, notification_id) ); --- --- funnels.sql --- - -CREATE TABLE funnels -( - funnel_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, - name text not null, - filter jsonb not null, - created_at timestamp default timezone('utc'::text, now()) not null, - deleted_at timestamp, - is_public boolean NOT NULL DEFAULT False -); - -CREATE INDEX ON public.funnels (user_id, is_public); - --- --- announcements.sql --- create type announcement_type as enum ('notification', 'alert'); @@ -307,7 +226,92 @@ create table announcements type announcement_type default 'notification'::announcement_type not null ); --- --- integrations.sql --- +CREATE TYPE error_source AS ENUM ('js_exception', 'bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic'); +CREATE TYPE error_status AS ENUM ('unresolved', 'resolved', 'ignored'); +CREATE TABLE errors +( + error_id text NOT NULL PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + source error_source NOT NULL, + name text DEFAULT NULL, + message text NOT NULL, + payload jsonb NOT NULL, + status error_status NOT NULL DEFAULT 'unresolved', + parent_error_id text DEFAULT NULL REFERENCES errors (error_id) ON DELETE SET NULL, + stacktrace jsonb, --to save the stacktrace and not query S3 another time + stacktrace_parsed_at timestamp +); +CREATE INDEX ON errors (project_id, source); +CREATE INDEX errors_message_gin_idx ON public.errors USING GIN (message gin_trgm_ops); +CREATE INDEX errors_name_gin_idx ON public.errors USING GIN (name gin_trgm_ops); +CREATE INDEX errors_project_id_idx ON public.errors (project_id); +CREATE INDEX errors_project_id_status_idx ON public.errors (project_id, status); + +CREATE TABLE user_favorite_errors +( + user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, error_id) +); + +CREATE TABLE user_viewed_errors +( + user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, + PRIMARY KEY (user_id, error_id) +); +CREATE INDEX user_viewed_errors_user_id_idx ON public.user_viewed_errors (user_id); +CREATE INDEX user_viewed_errors_error_id_idx ON public.user_viewed_errors (error_id); + + +CREATE TYPE issue_type AS ENUM ( + 'click_rage', + 'dead_click', + 'excessive_scrolling', + 'bad_request', + 'missing_resource', + 'memory', + 'cpu', + 'slow_resource', + 'slow_page_load', + 'crash', + 'ml_cpu', + 'ml_memory', + 'ml_dead_click', + 'ml_click_rage', + 'ml_mouse_thrashing', + 'ml_excessive_scrolling', + 'ml_slow_resources', + 'custom', + 'js_exception' + ); + +CREATE TABLE issues +( + issue_id text NOT NULL PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + type issue_type NOT NULL, + context_string text NOT NULL, + context jsonb DEFAULT NULL +); +CREATE INDEX ON issues (issue_id, type); +CREATE INDEX issues_context_string_gin_idx ON public.issues USING GIN (context_string gin_trgm_ops); + +create type webhook_type as enum ('webhook', 'slack', 'email'); + +create table webhooks +( + webhook_id integer generated by default as identity + constraint webhooks_pkey + primary key, + endpoint text not null, + created_at timestamp default timezone('utc'::text, now()) not null, + deleted_at timestamp, + auth_header text, + type webhook_type not null, + index integer default 0 not null, + name varchar(100) +); CREATE TYPE integration_provider AS ENUM ('bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic', 'elasticsearch'); --, 'jira', 'github'); CREATE TABLE integrations @@ -351,82 +355,74 @@ create table jira_cloud url text ); --- --- issues.sql --- - -CREATE TYPE issue_type AS ENUM ( - 'click_rage', - 'dead_click', - 'excessive_scrolling', - 'bad_request', - 'missing_resource', - 'memory', - 'cpu', - 'slow_resource', - 'slow_page_load', - 'crash', - 'ml_cpu', - 'ml_memory', - 'ml_dead_click', - 'ml_click_rage', - 'ml_mouse_thrashing', - 'ml_excessive_scrolling', - 'ml_slow_resources', - 'custom', - 'js_exception' - ); - -CREATE TABLE issues +CREATE TABLE funnels ( - issue_id text NOT NULL PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - type issue_type NOT NULL, - context_string text NOT NULL, - context jsonb DEFAULT NULL -); -CREATE INDEX ON issues (issue_id, type); -CREATE INDEX issues_context_string_gin_idx ON public.issues USING GIN (context_string gin_trgm_ops); - --- --- errors.sql --- - -CREATE TYPE error_source AS ENUM ('js_exception', 'bugsnag', 'cloudwatch', 'datadog', 'newrelic', 'rollbar', 'sentry', 'stackdriver', 'sumologic'); -CREATE TYPE error_status AS ENUM ('unresolved', 'resolved', 'ignored'); -CREATE TABLE errors -( - error_id text NOT NULL PRIMARY KEY, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, - source error_source NOT NULL, - name text DEFAULT NULL, - message text NOT NULL, - payload jsonb NOT NULL, - status error_status NOT NULL DEFAULT 'unresolved', - parent_error_id text DEFAULT NULL REFERENCES errors (error_id) ON DELETE SET NULL, - stacktrace jsonb, --to save the stacktrace and not query S3 another time - stacktrace_parsed_at timestamp -); -CREATE INDEX ON errors (project_id, source); -CREATE INDEX errors_message_gin_idx ON public.errors USING GIN (message gin_trgm_ops); -CREATE INDEX errors_name_gin_idx ON public.errors USING GIN (name gin_trgm_ops); -CREATE INDEX errors_project_id_idx ON public.errors (project_id); -CREATE INDEX errors_project_id_status_idx ON public.errors (project_id, status); - -CREATE TABLE user_favorite_errors -( - user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, - error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, - PRIMARY KEY (user_id, error_id) + funnel_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, + name text not null, + filter jsonb not null, + created_at timestamp default timezone('utc'::text, now()) not null, + deleted_at timestamp, + is_public boolean NOT NULL DEFAULT False ); -CREATE TABLE user_viewed_errors +CREATE INDEX ON public.funnels (user_id, is_public); + +CREATE TYPE alert_detection_method AS ENUM ('threshold', 'change'); + +CREATE TABLE alerts ( - user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, - error_id text NOT NULL REFERENCES errors (error_id) ON DELETE CASCADE, - PRIMARY KEY (user_id, error_id) + alert_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, + name text NOT NULL, + description text NULL DEFAULT NULL, + active boolean NOT NULL DEFAULT TRUE, + detection_method alert_detection_method NOT NULL, + query jsonb NOT NULL, + deleted_at timestamp NULL DEFAULT NULL, + created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()), + options jsonb NOT NULL DEFAULT '{ + "renotifyInterval": 1440 + }'::jsonb ); -CREATE INDEX user_viewed_errors_user_id_idx ON public.user_viewed_errors (user_id); -CREATE INDEX user_viewed_errors_error_id_idx ON public.user_viewed_errors (error_id); --- --- sessions.sql --- +CREATE OR REPLACE FUNCTION notify_alert() RETURNS trigger AS +$$ +DECLARE + clone jsonb; +BEGIN + clone = to_jsonb(NEW); + clone = jsonb_set(clone, '{created_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.created_at) * 1000 AS BIGINT))); + IF NEW.deleted_at NOTNULL THEN + clone = jsonb_set(clone, '{deleted_at}', to_jsonb(CAST(EXTRACT(epoch FROM NEW.deleted_at) * 1000 AS BIGINT))); + END IF; + PERFORM pg_notify('alert', clone::text); + RETURN NEW; +END ; +$$ LANGUAGE plpgsql; + + +CREATE TRIGGER on_insert_or_update_or_delete + AFTER INSERT OR UPDATE OR DELETE + ON alerts + FOR EACH ROW +EXECUTE PROCEDURE notify_alert(); + +CREATE TABLE autocomplete +( + value text NOT NULL, + type text NOT NULL, + project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE +); + +CREATE unique index autocomplete_unique ON autocomplete (project_id, value, type); +CREATE index autocomplete_project_id_idx ON autocomplete (project_id); +CREATE INDEX autocomplete_type_idx ON public.autocomplete (type); +CREATE INDEX autocomplete_value_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops); + + CREATE TYPE device_type AS ENUM ('desktop', 'tablet', 'mobile', 'other'); CREATE TYPE country AS ENUM ('UN', 'RW', 'SO', 'YE', 'IQ', 'SA', 'IR', 'CY', 'TZ', 'SY', 'AM', 'KE', 'CD', 'DJ', 'UG', 'CF', 'SC', 'JO', 'LB', 'KW', 'OM', 'QA', 'BH', 'AE', 'IL', 'TR', 'ET', 'ER', 'EG', 'SD', 'GR', 'BI', 'EE', 'LV', 'AZ', 'LT', 'SJ', 'GE', 'MD', 'BY', 'FI', 'AX', 'UA', 'MK', 'HU', 'BG', 'AL', 'PL', 'RO', 'XK', 'ZW', 'ZM', 'KM', 'MW', 'LS', 'BW', 'MU', 'SZ', 'RE', 'ZA', 'YT', 'MZ', 'MG', 'AF', 'PK', 'BD', 'TM', 'TJ', 'LK', 'BT', 'IN', 'MV', 'IO', 'NP', 'MM', 'UZ', 'KZ', 'KG', 'TF', 'HM', 'CC', 'PW', 'VN', 'TH', 'ID', 'LA', 'TW', 'PH', 'MY', 'CN', 'HK', 'BN', 'MO', 'KH', 'KR', 'JP', 'KP', 'SG', 'CK', 'TL', 'RU', 'MN', 'AU', 'CX', 'MH', 'FM', 'PG', 'SB', 'TV', 'NR', 'VU', 'NC', 'NF', 'NZ', 'FJ', 'LY', 'CM', 'SN', 'CG', 'PT', 'LR', 'CI', 'GH', 'GQ', 'NG', 'BF', 'TG', 'GW', 'MR', 'BJ', 'GA', 'SL', 'ST', 'GI', 'GM', 'GN', 'TD', 'NE', 'ML', 'EH', 'TN', 'ES', 'MA', 'MT', 'DZ', 'FO', 'DK', 'IS', 'GB', 'CH', 'SE', 'NL', 'AT', 'BE', 'DE', 'LU', 'IE', 'MC', 'FR', 'AD', 'LI', 'JE', 'IM', 'GG', 'SK', 'CZ', 'NO', 'VA', 'SM', 'IT', 'SI', 'ME', 'HR', 'BA', 'AO', 'NA', 'SH', 'BV', 'BB', 'CV', 'GY', 'GF', 'SR', 'PM', 'GL', 'PY', 'UY', 'BR', 'FK', 'GS', 'JM', 'DO', 'CU', 'MQ', 'BS', 'BM', 'AI', 'TT', 'KN', 'DM', 'AG', 'LC', 'TC', 'AW', 'VG', 'VC', 'MS', 'MF', 'BL', 'GP', 'GD', 'KY', 'BZ', 'SV', 'GT', 'HN', 'NI', 'CR', 'VE', 'EC', 'CO', 'PA', 'HT', 'AR', 'CL', 'BO', 'PE', 'MX', 'PF', 'PN', 'KI', 'TK', 'TO', 'WF', 'WS', 'NU', 'MP', 'GU', 'PR', 'VI', 'UM', 'AS', 'CA', 'US', 'PS', 'RS', 'AQ', 'SX', 'CW', 'BQ', 'SS'); CREATE TYPE platform AS ENUM ('web','ios','android'); @@ -437,7 +433,7 @@ CREATE TABLE sessions project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE, tracker_version text NOT NULL, start_ts bigint NOT NULL, - duration integer NULL, + duration integer DEFAULT NULL, rev_id text DEFAULT NULL, platform platform NOT NULL DEFAULT 'web', is_snippet boolean NOT NULL DEFAULT FALSE, @@ -511,8 +507,6 @@ CREATE INDEX sessions_user_anonymous_id_gin_idx ON public.sessions USING GIN (us CREATE INDEX sessions_user_country_gin_idx ON public.sessions (project_id, user_country); CREATE INDEX ON sessions (project_id, user_country); CREATE INDEX ON sessions (project_id, user_browser); -CREATE INDEX sessions_start_ts_idx ON public.sessions (start_ts) WHERE duration > 0; -CREATE INDEX sessions_project_id_idx ON public.sessions (project_id) WHERE duration > 0; ALTER TABLE public.sessions ADD CONSTRAINT web_browser_constraint CHECK ( (sessions.platform = 'web' AND sessions.user_browser NOTNULL) OR @@ -542,73 +536,6 @@ CREATE TABLE user_favorite_sessions ); --- --- assignments.sql --- - -create table assigned_sessions -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - issue_id text NOT NULL, - provider oauth_provider NOT NULL, - created_by integer NOT NULL, - created_at timestamp default timezone('utc'::text, now()) NOT NULL, - provider_data jsonb default '{}'::jsonb NOT NULL -); - --- --- events_common.sql --- - -CREATE SCHEMA events_common; - -CREATE TYPE events_common.custom_level AS ENUM ('info','error'); - -CREATE TABLE events_common.customs -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - seq_index integer NOT NULL, - name text NOT NULL, - payload jsonb NOT NULL, - level events_common.custom_level NOT NULL DEFAULT 'info', - PRIMARY KEY (session_id, timestamp, seq_index) -); -CREATE INDEX ON events_common.customs (name); -CREATE INDEX customs_name_gin_idx ON events_common.customs USING GIN (name gin_trgm_ops); -CREATE INDEX ON events_common.customs (timestamp); - - -CREATE TABLE events_common.issues -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - seq_index integer NOT NULL, - issue_id text NOT NULL REFERENCES issues (issue_id) ON DELETE CASCADE, - payload jsonb DEFAULT NULL, - PRIMARY KEY (session_id, timestamp, seq_index) -); - - -CREATE TABLE events_common.requests -( - session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, - timestamp bigint NOT NULL, - seq_index integer NOT NULL, - url text NOT NULL, - duration integer NOT NULL, - success boolean NOT NULL, - PRIMARY KEY (session_id, timestamp, seq_index) -); -CREATE INDEX ON events_common.requests (url); -CREATE INDEX ON events_common.requests (duration); -CREATE INDEX requests_url_gin_idx ON events_common.requests USING GIN (url gin_trgm_ops); -CREATE INDEX ON events_common.requests (timestamp); -CREATE INDEX requests_url_gin_idx2 ON events_common.requests USING GIN (RIGHT(url, length(url) - (CASE - WHEN url LIKE 'http://%' - THEN 7 - WHEN url LIKE 'https://%' - THEN 8 - ELSE 0 END)) - gin_trgm_ops); - --- --- events.sql --- CREATE SCHEMA events; CREATE TABLE events.pages @@ -631,7 +558,6 @@ CREATE TABLE events.pages time_to_interactive integer DEFAULT NULL, response_time bigint DEFAULT NULL, response_end bigint DEFAULT NULL, - ttfb integer DEFAULT NULL, PRIMARY KEY (session_id, message_id) ); CREATE INDEX ON events.pages (session_id); @@ -651,11 +577,6 @@ CREATE INDEX pages_base_referrer_gin_idx2 ON events.pages USING GIN (RIGHT(base_ gin_trgm_ops); CREATE INDEX ON events.pages (response_time); CREATE INDEX ON events.pages (response_end); -CREATE INDEX pages_path_gin_idx ON events.pages USING GIN (path gin_trgm_ops); -CREATE INDEX pages_path_idx ON events.pages (path); -CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0; -CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0; -CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0; CREATE TABLE events.clicks @@ -722,6 +643,85 @@ CREATE INDEX ON events.state_actions (name); CREATE INDEX state_actions_name_gin_idx ON events.state_actions USING GIN (name gin_trgm_ops); CREATE INDEX ON events.state_actions (timestamp); + + +CREATE OR REPLACE FUNCTION events.funnel(steps integer[], m integer) RETURNS boolean AS +$$ +DECLARE + step integer; + c integer := 0; +BEGIN + FOREACH step IN ARRAY steps + LOOP + IF step + c = 0 THEN + IF c = 0 THEN + RETURN false; + END IF; + c := 0; + CONTINUE; + END IF; + IF c + 1 = step THEN + c := step; + END IF; + END LOOP; + RETURN c = m; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + + + +CREATE SCHEMA events_common; + +CREATE TYPE events_common.custom_level AS ENUM ('info','error'); + +CREATE TABLE events_common.customs +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + seq_index integer NOT NULL, + name text NOT NULL, + payload jsonb NOT NULL, + level events_common.custom_level NOT NULL DEFAULT 'info', + PRIMARY KEY (session_id, timestamp, seq_index) +); +CREATE INDEX ON events_common.customs (name); +CREATE INDEX customs_name_gin_idx ON events_common.customs USING GIN (name gin_trgm_ops); +CREATE INDEX ON events_common.customs (timestamp); + + +CREATE TABLE events_common.issues +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + seq_index integer NOT NULL, + issue_id text NOT NULL REFERENCES issues (issue_id) ON DELETE CASCADE, + payload jsonb DEFAULT NULL, + PRIMARY KEY (session_id, timestamp, seq_index) +); + + +CREATE TABLE events_common.requests +( + session_id bigint NOT NULL REFERENCES sessions (session_id) ON DELETE CASCADE, + timestamp bigint NOT NULL, + seq_index integer NOT NULL, + url text NOT NULL, + duration integer NOT NULL, + success boolean NOT NULL, + PRIMARY KEY (session_id, timestamp, seq_index) +); +CREATE INDEX ON events_common.requests (url); +CREATE INDEX ON events_common.requests (duration); +CREATE INDEX requests_url_gin_idx ON events_common.requests USING GIN (url gin_trgm_ops); +CREATE INDEX ON events_common.requests (timestamp); +CREATE INDEX requests_url_gin_idx2 ON events_common.requests USING GIN (RIGHT(url, length(url) - (CASE + WHEN url LIKE 'http://%' + THEN 7 + WHEN url LIKE 'https://%' + THEN 8 + ELSE 0 END)) + gin_trgm_ops); + CREATE TYPE events.resource_type AS ENUM ('other', 'script', 'stylesheet', 'fetch', 'img', 'media'); CREATE TYPE events.resource_method AS ENUM ('GET' , 'HEAD' , 'POST' , 'PUT' , 'DELETE' , 'CONNECT' , 'OPTIONS' , 'TRACE' , 'PATCH' ); CREATE TABLE events.resources @@ -779,42 +779,16 @@ CREATE TABLE events.performance ); -CREATE OR REPLACE FUNCTION events.funnel(steps integer[], m integer) RETURNS boolean AS -$$ -DECLARE - step integer; - c integer := 0; -BEGIN - FOREACH step IN ARRAY steps - LOOP - IF step + c = 0 THEN - IF c = 0 THEN - RETURN false; - END IF; - c := 0; - CONTINUE; - END IF; - IF c + 1 = step THEN - c := step; - END IF; - END LOOP; - RETURN c = m; -END; -$$ LANGUAGE plpgsql IMMUTABLE; +ALTER TABLE events.pages + ADD COLUMN ttfb integer DEFAULT NULL; +CREATE INDEX pages_path_gin_idx ON events.pages USING GIN (path gin_trgm_ops); +CREATE INDEX pages_path_idx ON events.pages (path); +CREATE INDEX pages_visually_complete_idx ON events.pages (visually_complete) WHERE visually_complete > 0; +CREATE INDEX pages_dom_building_time_idx ON events.pages (dom_building_time) WHERE dom_building_time > 0; +CREATE INDEX pages_load_time_idx ON events.pages (load_time) WHERE load_time > 0; + +CREATE INDEX sessions_start_ts_idx ON public.sessions (start_ts) WHERE duration > 0; +CREATE INDEX sessions_project_id_idx ON public.sessions (project_id) WHERE duration > 0; --- --- autocomplete.sql --- - -CREATE TABLE autocomplete -( - value text NOT NULL, - type text NOT NULL, - project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE -); - -CREATE unique index autocomplete_unique ON autocomplete (project_id, value, type); -CREATE index autocomplete_project_id_idx ON autocomplete (project_id); -CREATE INDEX autocomplete_type_idx ON public.autocomplete (type); -CREATE INDEX autocomplete_value_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops); - -COMMIT; +COMMIT; \ No newline at end of file diff --git a/scripts/helm/db/sqs/.helmignore b/scripts/helm/db/sqs/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/scripts/helm/db/sqs/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/scripts/helm/db/sqs/Chart.yaml b/scripts/helm/db/sqs/Chart.yaml new file mode 100644 index 000000000..df40d044a --- /dev/null +++ b/scripts/helm/db/sqs/Chart.yaml @@ -0,0 +1,23 @@ +apiVersion: v2 +name: sqs +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +appVersion: 1.16.0 diff --git a/scripts/helm/db/sqs/templates/NOTES.txt b/scripts/helm/db/sqs/templates/NOTES.txt new file mode 100644 index 000000000..1933314a0 --- /dev/null +++ b/scripts/helm/db/sqs/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sqs.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sqs.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sqs.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sqs.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:9325 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 9325:$CONTAINER_PORT +{{- end }} diff --git a/scripts/helm/db/sqs/templates/_helpers.tpl b/scripts/helm/db/sqs/templates/_helpers.tpl new file mode 100644 index 000000000..518fd7cc2 --- /dev/null +++ b/scripts/helm/db/sqs/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "sqs.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "sqs.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "sqs.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "sqs.labels" -}} +helm.sh/chart: {{ include "sqs.chart" . }} +{{ include "sqs.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "sqs.selectorLabels" -}} +app.kubernetes.io/name: {{ include "sqs.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "sqs.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "sqs.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/scripts/helm/db/sqs/templates/configmap.yaml b/scripts/helm/db/sqs/templates/configmap.yaml new file mode 100644 index 000000000..aa6c9f956 --- /dev/null +++ b/scripts/helm/db/sqs/templates/configmap.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "sqs.fullname" . }} + labels: + {{- include "sqs.labels" . | nindent 4 }} +data: + elasticmq.conf: |- + include classpath("application.conf") + akka.http.server.request-timeout = 40 s + + node-address { + protocol = http + host = "*" + port = 9324 + context-path = "" + } + + rest-sqs { + enabled = true + bind-port = 9324 + bind-hostname = "0.0.0.0" + // Possible values: relaxed, strict + sqs-limits = strict + } +{{if .Values.queueConfig }} +{{ .Values.queueConfig | trim | nindent 4 }} +{{ end }} diff --git a/scripts/helm/db/sqs/templates/deployment.yaml b/scripts/helm/db/sqs/templates/deployment.yaml new file mode 100644 index 000000000..62712031f --- /dev/null +++ b/scripts/helm/db/sqs/templates/deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "sqs.fullname" . }} + labels: + {{- include "sqs.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "sqs.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "sqs.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "sqs.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 9325 + protocol: TCP + - name: sqs + containerPort: 9324 + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: elasticmq + mountPath: /opt/elasticmq.conf + subPath: elasticmq.conf + volumes: + - name: elasticmq + configMap: + name: {{ include "sqs.fullname" . }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/scripts/helm/db/sqs/templates/hpa.yaml b/scripts/helm/db/sqs/templates/hpa.yaml new file mode 100644 index 000000000..db0747bcf --- /dev/null +++ b/scripts/helm/db/sqs/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "sqs.fullname" . }} + labels: + {{- include "sqs.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "sqs.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/scripts/helm/db/sqs/templates/ingress.yaml b/scripts/helm/db/sqs/templates/ingress.yaml new file mode 100644 index 000000000..b2dc375fb --- /dev/null +++ b/scripts/helm/db/sqs/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "sqs.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "sqs.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} diff --git a/scripts/helm/db/sqs/templates/service.yaml b/scripts/helm/db/sqs/templates/service.yaml new file mode 100644 index 000000000..fa6b14238 --- /dev/null +++ b/scripts/helm/db/sqs/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "sqs.fullname" . }} + labels: + {{- include "sqs.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.http.port }} + targetPort: http + protocol: TCP + name: http + - port: {{ .Values.service.sqs.port }} + targetPort: sqs + protocol: TCP + name: sqs + selector: + {{- include "sqs.selectorLabels" . | nindent 4 }} diff --git a/scripts/helm/db/sqs/templates/serviceaccount.yaml b/scripts/helm/db/sqs/templates/serviceaccount.yaml new file mode 100644 index 000000000..a2989f188 --- /dev/null +++ b/scripts/helm/db/sqs/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "sqs.serviceAccountName" . }} + labels: + {{- include "sqs.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/scripts/helm/db/sqs/values.yaml b/scripts/helm/db/sqs/values.yaml new file mode 100644 index 000000000..5634a5494 --- /dev/null +++ b/scripts/helm/db/sqs/values.yaml @@ -0,0 +1,111 @@ +# Default values for sqs. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: roribio16/alpine-sqs + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + sqs: + port: 9324 + http: + port: 9325 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 1 + memory: 1Gi + requests: + cpu: 100m + memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Creating the initial queue +# Ref: https://github.com/softwaremill/elasticmq#automatically-creating-queues-on-startup +queueConfig: |- + queues { + scheduled-runs { + defaultVisibilityTimeout = 10 seconds + delay = 5 seconds + receiveMessageWait = 0 seconds + deadLettersQueue { + name = "dead-runs" + maxReceiveCount = 1000 + } + } + ondemand-runs { + defaultVisibilityTimeout = 10 seconds + delay = 5 seconds + receiveMessageWait = 0 seconds + deadLettersQueue { + name = "dead-runs" + maxReceiveCount = 1000 + } + } + dead-runs { + defaultVisibilityTimeout = 10 seconds + delay = 5 seconds + receiveMessageWait = 0 seconds + } + } diff --git a/scripts/helm/install.sh b/scripts/helm/install.sh index a3dfcc4c2..fc50c519b 100755 --- a/scripts/helm/install.sh +++ b/scripts/helm/install.sh @@ -2,22 +2,6 @@ set -o errtrace -# Check for a valid domain_name -domain_name=`grep domain_name vars.yaml | grep -v "example" | cut -d " " -f2 | cut -d '"' -f2` -# Ref: https://stackoverflow.com/questions/15268987/bash-based-regex-domain-name-validation -[[ $(echo $domain_name | grep -P '(?=^.{5,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)') ]] || { - echo "OpenReplay Needs a valid domain name for captured sessions to replay. For example, openreplay.mycompany.com" - echo "Please enter your domain name" - read domain_name - [[ -z domain_name ]] && { - echo "OpenReplay won't work without domain name. Exiting..." - exit 1 - } || { - sed -i "s#domain_name.*#domain_name: \"${domain_name}\" #g" vars.yaml - } -} - - # Installing k3s curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik" sh - mkdir ~/.kube diff --git a/scripts/helm/kube-install.sh b/scripts/helm/kube-install.sh index 5483c4420..e3905c2d4 100755 --- a/scripts/helm/kube-install.sh +++ b/scripts/helm/kube-install.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -o errtrace +set -xo errtrace # color schemes # Ansi color code variables @@ -22,7 +22,7 @@ echo -e ${reset} ## installing kubectl which kubectl &> /dev/null || { - echo "kubectl not installed. Installing it..." + echo "kubectl not installed. installing..." sudo curl -SsL https://dl.k8s.io/release/v1.20.0/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl ; sudo chmod +x /usr/local/bin/kubectl } @@ -34,7 +34,7 @@ which stern &> /dev/null || { ## installing k9s which k9s &> /dev/null || { - echo "k9s not installed. Installing it..." + echo "k9s not installed. installing..." sudo curl -SsL https://github.com/derailed/k9s/releases/download/v0.24.2/k9s_Linux_x86_64.tar.gz -o /tmp/k9s.tar.gz cd /tmp tar -xf k9s.tar.gz @@ -44,13 +44,13 @@ which k9s &> /dev/null || { } which ansible &> /dev/null || { - echo "ansible not installed. Installing it..." + echo "ansible not installed. Installing..." which pip || (sudo apt update && sudo apt install python3-pip -y) sudo pip3 install ansible==2.10.0 } which docker &> /dev/null || { - echo "docker is not installed. Installing it..." + echo "docker is not installed. Installing..." user=`whoami` sudo apt install docker.io -y sudo usermod -aG docker $user @@ -59,7 +59,7 @@ which docker &> /dev/null || { ## installing helm which helm &> /dev/null if [[ $? -ne 0 ]]; then - echo "helm not installed. Installing it..." + echo "helm not installed. installing..." curl -ssl https://get.helm.sh/helm-v3.4.2-linux-amd64.tar.gz -o /tmp/helm.tar.gz tar -xf /tmp/helm.tar.gz chmod +x linux-amd64/helm @@ -77,31 +77,30 @@ fi # make all stderr red color()(set -o pipefail;"$@" 2>&1>&3|sed $'s,.*,\e[31m&\e[m,'>&2)3>&1 -usage() { +usage() +{ echo -e ${bold}${yellow} ''' -This script will install and configure OpenReplay apps and databases on the kubernetes cluster, +This script will install and configure openreplay apps and databases on the kubernetes cluster, which is accesd with the ${HOME}/.kube/config or $KUBECONFIG env variable. ''' -cat <<"EOF" - ___ ____ _ - / _ \ _ __ ___ _ __ | _ \ ___ _ __ | | __ _ _ _ -| | | | '_ \ / _ \ '_ \| |_) / _ \ '_ \| |/ _` | | | | -| |_| | |_) | __/ | | | _ < __/ |_) | | (_| | |_| | - \___/| .__/ \___|_| |_|_| \_\___| .__/|_|\__,_|\__, | - |_| |_| |___/ - +cat << EOF +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +█░▄▄▀█░▄▄█░▄▄▀█░██░█░▄▄█░▄▄▀██ +█░▀▀░█▄▄▀█░▀▀░█░▀▀░█░▄▄█░▀▀▄██ +█▄██▄█▄▄▄█▄██▄█▀▀▀▄█▄▄▄█▄█▄▄██ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ EOF echo -e "${green}Usage: openreplay-cli [ -h | --help ] [ -v | --verbose ] [ -a | --app APP_NAME ] to install/reinstall specific application [ -t | --type small|medium|ideal ]" echo -e "${reset}${blue}type defines the resource limits applied for the installation: - small: 2core 8G machine + small: 4core 8G machine medium: 4core 16G machine ideal: 8core 32G machine apps can specifically be installed/reinstalled: - alerts assets chalice ender http integrations ios-proxy pg redis sink storage frontend + alerts assets auth chalice ender http integrations ios-proxy metadata negative pg-stateless pg preprocessing redis sink storage frontend ${reset}" echo type value: $installation_type exit 0 @@ -123,10 +122,8 @@ type() { function app(){ case $1 in nginx) - # Resetting the redirection rule - sed -i 's/.* return 301 .*/ # return 301 https:\/\/$host$request_uri;/g' nginx-ingress/nginx-ingress/templates/configmap.yaml - [[ NGINX_REDIRECT_HTTPS -eq 1 ]] && { - sed -i "s/# return 301/return 301/g" nginx-ingress/nginx-ingress/templates/configmap.yaml + [[ NGINX_REDIRECT_HTTPS -eq 0 ]] && { + sed -i "/return 301/d" nginx-ingress/nginx-ingress/templates/configmap.yaml } ansible-playbook -c local setup.yaml -e @vars.yaml -e scale=$installation_type --tags nginx -v exit 0 @@ -173,7 +170,3 @@ done { ansible-playbook -c local setup.yaml -e @vars.yaml -e scale=$installation_type --skip-tags pre-check -v } || exit $? - - - - diff --git a/scripts/helm/nginx-ingress/nginx-ingress/README.md b/scripts/helm/nginx-ingress/nginx-ingress/README.md index 76c878b5a..a61fe2bc6 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/README.md +++ b/scripts/helm/nginx-ingress/nginx-ingress/README.md @@ -1,13 +1,13 @@ ## Description -This is the frontend of the OpenReplay web app (internet). +This is the frontend of the openreplay web app to internet. -## Endpoints +## Path information -- /streaming -> ios-proxy -- /api -> chalice -- /http -> http -- / -> frontend (in minio) -- /assets -> sessions-assets bucket in minio -- /minio -> minio api endpoint -- /ingest -> events ingestor +/ws -> websocket +/streaming -> ios-proxy +/api -> chalice +/http -> http +/ -> frontend (in minio) +/assets -> asayer-sessions-assets bucket in minio +/s3 -> minio api endpoint diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml index d02cc26b1..d47e47255 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/configmap.yaml @@ -20,8 +20,6 @@ data: proxy_set_header Connection ""; chunked_transfer_encoding off; - client_max_body_size 50M; - proxy_pass http://minio.db.svc.cluster.local:9000; } @@ -37,9 +35,6 @@ data: proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; - proxy_set_header X-Forwarded-For $real_ip; - proxy_set_header X-Forwarded-Host $real_ip; - proxy_set_header X-Real-IP $real_ip; proxy_set_header Host $host; proxy_pass http://http-openreplay.app.svc.cluster.local; } @@ -107,13 +102,6 @@ data: ; sites.conf: |- - # Need real ip address for flags in replay. - # Some LBs will forward real ips as x-forwarded-for - # So making that as priority - map $http_x_forwarded_for $real_ip { - ~^(\d+\.\d+\.\d+\.\d+) $1; - default $remote_addr; - } map $http_upgrade $connection_upgrade { default upgrade; '' close; @@ -122,8 +110,8 @@ data: listen 80 default_server; listen [::]:80 default_server; # server_name _; - # return 301 https://$host$request_uri; - include /etc/nginx/conf.d/location.list; + return 301 https://$host$request_uri; + # include /etc/nginx/conf.d/location.list; } server { listen 443 ssl; diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml index 9cc018dc1..c9d9d78e5 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/deployment.yaml @@ -13,9 +13,8 @@ spec: {{- include "nginx.selectorLabels" . | nindent 6 }} template: metadata: - annotations: - nginxRolloutID: {{ randAlphaNum 5 | quote }} # Restart nginx after every deployment {{- with .Values.podAnnotations }} + annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: diff --git a/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml b/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml index 38912bf78..38dc08846 100644 --- a/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml +++ b/scripts/helm/nginx-ingress/nginx-ingress/templates/service.yaml @@ -6,8 +6,6 @@ metadata: {{- include "nginx.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} - # Make sure to get client ip - externalTrafficPolicy: Local ports: {{- range .Values.service.ports }} - port: {{ .port }} diff --git a/scripts/helm/openreplay-cli b/scripts/helm/openreplay-cli index 19392c1ce..05a0b55ed 100755 --- a/scripts/helm/openreplay-cli +++ b/scripts/helm/openreplay-cli @@ -1,6 +1,6 @@ #!/bin/bash -## This script is a helper for managing your OpenReplay instance +## This script is a helper for openreplay management set -eE -o pipefail # same as: `set -o errexit -o errtrace` # Trapping the error @@ -37,16 +37,6 @@ CWD=$pwd usage() { clear -cat <<"EOF" - ___ ____ _ - / _ \ _ __ ___ _ __ | _ \ ___ _ __ | | __ _ _ _ -| | | | '_ \ / _ \ '_ \| |_) / _ \ '_ \| |/ _` | | | | -| |_| | |_) | __/ | | | _ < __/ |_) | | (_| | |_| | - \___/| .__/ \___|_| |_|_| \_\___| .__/|_|\__,_|\__, | - |_| |_| |___/ - -EOF - echo -e "${green}Usage: openreplay-cli [ -h | --help ] [ -d | --status ] [ -v | --verbose ] @@ -58,7 +48,7 @@ EOF echo -e "${reset}${blue}services: ${services[*]}${reset}" exit 0 } -services=( alerts assets chalice clickhouse ender sink storage http integrations ios-proxy db pg redis ) +services=( alert auth cache chalice clickhouse ender events failover filesink filestorage http integrations ios-proxy metadata negative pg-stateless pg preprocessing redis ws ) check() { if ! command -v kubectl &> /dev/null @@ -82,14 +72,14 @@ stop() { start() { if [[ $1 == "all" ]]; then - cd ./app + cd ./helm/app for apps in $(ls *.yaml);do app=$(echo $apps | cut -d '.' -f1) helm upgrade --install -n app $app openreplay -f $app.yaml done cd $CWD fi - helm upgrade --install -n app $1 ./app/openreplay -f ./app/$1.yaml + helm upgrade --install -n app $1 ./helm/app/openreplay -f ./helm/app/openreplay/$1.yaml } @@ -105,7 +95,7 @@ install() { } upgrade() { - sed -i "s/tag:.*/ tag: 'latest'/g" ./app/$1.yaml + sed -i "s/tag:.*/ tag: 'latest'/g" helm/app/$1.yaml } logs() { @@ -119,7 +109,7 @@ status() { [[ $# -eq 0 ]] && usage && exit 1 -PARSED_ARGUMENTS=$(color getopt -a -n openreplay-cli -o vhds:S:l:r:i: --long verbose,help,status,start:,stop:,logs:,restart:,install: -- "$@") +PARSED_ARGUMENTS=$(color getopt -a -n openreplay-cli -o vhds:S:l:r:i: --long verbose,help,status,start:,stop:,restart:,install: -- "$@") VALID_ARGUMENTS=$? if [[ "$VALID_ARGUMENTS" != "0" ]]; then usage diff --git a/scripts/helm/roles/openreplay/defaults/main.yml b/scripts/helm/roles/openreplay/defaults/main.yml index f7948e53c..4927d5350 100644 --- a/scripts/helm/roles/openreplay/defaults/main.yml +++ b/scripts/helm/roles/openreplay/defaults/main.yml @@ -6,4 +6,5 @@ db_list: - "nfs-server-provisioner" - "postgresql" - "redis" + - "sqs" enterprise_edition: false diff --git a/scripts/helm/roles/openreplay/tasks/install-apps.yaml b/scripts/helm/roles/openreplay/tasks/install-apps.yaml index 3e511ac19..f442065ac 100644 --- a/scripts/helm/roles/openreplay/tasks/install-apps.yaml +++ b/scripts/helm/roles/openreplay/tasks/install-apps.yaml @@ -9,7 +9,7 @@ executable: /bin/bash when: app_name|length > 0 tags: app -- name: Installing OpenReplay core applications +- name: Installing openreplay core applications shell: | override='' [[ -f /tmp/'{{ item|basename }}' ]] && override='-f /tmp/{{ item|basename }}' || true diff --git a/scripts/helm/roles/openreplay/tasks/main.yml b/scripts/helm/roles/openreplay/tasks/main.yml index 66d31cf4a..ab4a7cd60 100644 --- a/scripts/helm/roles/openreplay/tasks/main.yml +++ b/scripts/helm/roles/openreplay/tasks/main.yml @@ -14,11 +14,11 @@ shell: | kubectl delete -n app secret aws-registry || true kubectl create secret -n app docker-registry aws-registry \ - --docker-server="{{ docker_registry_url }}" \ - --docker-username="{{ docker_registry_username }}" \ - --docker-password="{{ docker_registry_password }}" \ + --docker-server="{{ ecr_docker_registry_server }}" \ + --docker-username="{{ ecr_docker_username }}" \ + --docker-password="{{ ecr_docker_password }}" \ --docker-email=no@email.local - when: docker_registry_username|length != 0 and docker_registry_password|length != 0 + when: ecr_docker_username|length != 0 and ecr_docker_password|length != 0 # Creating helm override files. - name: Creating override files diff --git a/scripts/helm/roles/openreplay/tasks/pre-check.yaml b/scripts/helm/roles/openreplay/tasks/pre-check.yaml index 60801192f..4363b9c34 100644 --- a/scripts/helm/roles/openreplay/tasks/pre-check.yaml +++ b/scripts/helm/roles/openreplay/tasks/pre-check.yaml @@ -4,11 +4,11 @@ block: - name: Checking mandatory variables fail: - msg: "Didn't find OpenReplay docker credentials." - when: kubeconfig_path|length == 0 or docker_registry_url|length == 0 + msg: "Didn't find openreplay docker credentials." + when: kubeconfig_path|length == 0 or ecr_docker_registry_server|length == 0 - name: Generaing minio access key block: - - name: Generating minio access key + - name: Generaing minio access key set_fact: minio_access_key_generated: "{{ lookup('password', '/dev/null length=30 chars=ascii_letters') }}" - name: Updating vars.yaml @@ -16,13 +16,13 @@ regexp: '^minio_access_key' line: 'minio_access_key: "{{ minio_access_key_generated }}"' path: vars.yaml - - name: Generating minio access key + - name: Generaing minio access key set_fact: minio_access_key: "{{ minio_access_key_generated }}" when: minio_access_key|length == 0 - - name: Generating minio secret key + - name: Generaing minio secret key block: - - name: Generating minio access key + - name: Generaing minio access key set_fact: minio_secret_key_generated: "{{ lookup('password', '/dev/null length=30 chars=ascii_letters') }}" - name: Updating vars.yaml @@ -30,33 +30,19 @@ regexp: '^minio_secret_key' line: 'minio_secret_key: "{{minio_secret_key_generated}}"' path: vars.yaml - - name: Generating minio secret key + - name: Generaing minio secret key set_fact: minio_access_key: "{{ minio_secret_key_generated }}" when: minio_secret_key|length == 0 - - name: Generating jwt secret key - block: - - name: Generating jwt access key - set_fact: - jwt_secret_key_generated: "{{ lookup('password', '/dev/null length=30 chars=ascii_letters') }}" - - name: Updating vars.yaml - lineinfile: - regexp: '^jwt_secret_key' - line: 'jwt_secret_key: "{{jwt_secret_key_generated}}"' - path: vars.yaml - - name: Generating jwt secret key - set_fact: - jwt_access_key: "{{ jwt_secret_key_generated }}" - when: jwt_secret_key|length == 0 rescue: - name: Caught error debug: msg: - - Below variables are mandatory. Please make sure it is updated in vars.yaml + - Below variabls are mandatory. Please make sure it's updated in vars.yaml - kubeconfig_path - - docker_registry_username - - docker_registry_password - - docker_registry_url + - ecr_docker_username + - ecr_docker_password + - ecr_docker_registry_server failed_when: true tags: pre-check - name: Creating Nginx SSL certificate diff --git a/scripts/helm/roles/openreplay/templates/alert.yaml b/scripts/helm/roles/openreplay/templates/alert.yaml index 1ba439a63..0200a406a 100644 --- a/scripts/helm/roles/openreplay/templates/alert.yaml +++ b/scripts/helm/roles/openreplay/templates/alert.yaml @@ -4,9 +4,9 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/assets.yaml b/scripts/helm/roles/openreplay/templates/assets.yaml index 1f21147bb..d7be9fa9d 100644 --- a/scripts/helm/roles/openreplay/templates/assets.yaml +++ b/scripts/helm/roles/openreplay/templates/assets.yaml @@ -7,6 +7,6 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/chalice.yaml b/scripts/helm/roles/openreplay/templates/chalice.yaml index 28325eb64..90b6de579 100644 --- a/scripts/helm/roles/openreplay/templates/chalice.yaml +++ b/scripts/helm/roles/openreplay/templates/chalice.yaml @@ -4,7 +4,7 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} env: @@ -13,4 +13,3 @@ env: sourcemaps_bucket_key: "{{ minio_access_key }}" sourcemaps_bucket_secret: "{{ minio_secret_key }}" S3_HOST: "https://{{ domain_name }}" - jwt_secret: "{{ jwt_secret_key }}" diff --git a/scripts/helm/roles/openreplay/templates/db.yaml b/scripts/helm/roles/openreplay/templates/db.yaml index 1ba439a63..0200a406a 100644 --- a/scripts/helm/roles/openreplay/templates/db.yaml +++ b/scripts/helm/roles/openreplay/templates/db.yaml @@ -4,9 +4,9 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/ender.yaml b/scripts/helm/roles/openreplay/templates/ender.yaml index 2d51506ea..560483e94 100644 --- a/scripts/helm/roles/openreplay/templates/ender.yaml +++ b/scripts/helm/roles/openreplay/templates/ender.yaml @@ -4,6 +4,6 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/http.yaml b/scripts/helm/roles/openreplay/templates/http.yaml index 1f21147bb..d7be9fa9d 100644 --- a/scripts/helm/roles/openreplay/templates/http.yaml +++ b/scripts/helm/roles/openreplay/templates/http.yaml @@ -7,6 +7,6 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/integrations.yaml b/scripts/helm/roles/openreplay/templates/integrations.yaml index 2d51506ea..560483e94 100644 --- a/scripts/helm/roles/openreplay/templates/integrations.yaml +++ b/scripts/helm/roles/openreplay/templates/integrations.yaml @@ -4,6 +4,6 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/sink.yaml b/scripts/helm/roles/openreplay/templates/sink.yaml index 2d51506ea..560483e94 100644 --- a/scripts/helm/roles/openreplay/templates/sink.yaml +++ b/scripts/helm/roles/openreplay/templates/sink.yaml @@ -4,6 +4,6 @@ image: tag: {{ image_tag }} {% endif %} -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/roles/openreplay/templates/storage.yaml b/scripts/helm/roles/openreplay/templates/storage.yaml index 1f21147bb..d7be9fa9d 100644 --- a/scripts/helm/roles/openreplay/templates/storage.yaml +++ b/scripts/helm/roles/openreplay/templates/storage.yaml @@ -7,6 +7,6 @@ env: AWS_ACCESS_KEY_ID: "{{ minio_access_key }}" AWS_SECRET_ACCESS_KEY: "{{ minio_secret_key }}" -{% if not (docker_registry_username is defined and docker_registry_username and docker_registry_password is defined and docker_registry_password) %} +{% if not (ecr_docker_username is defined and ecr_docker_username and ecr_docker_password is defined and ecr_docker_password) %} imagePullSecrets: [] {% endif %} diff --git a/scripts/helm/vars.yaml b/scripts/helm/vars.yaml index bdade016c..1bdd7a6bf 100644 --- a/scripts/helm/vars.yaml +++ b/scripts/helm/vars.yaml @@ -7,22 +7,22 @@ # Give absolute file path. # Use following command to get the full file path # `readlink -f ` -kubeconfig_path: /home/rajeshr/.kube/config +kubeconfig_path: "" ################### ## Optional Fields. ################### # If you've private registry, please update the details here. -docker_registry_username: "" -docker_registry_password: "" -docker_registry_url: "rg.fr-par.scw.cloud/foss" -image_tag: "latest" +ecr_docker_username: "" +ecr_docker_password: "" +ecr_docker_registry_server: "rg.fr-par.scw.cloud/foss" +image_tag: v1.0.0 # This is an optional field. If you want to use proper ssl, then it's mandatory -# Using which domain name, you'll be accessing OpenReplay -# for example: domain_name: "test.com" -domain_name: "" +# Using which domain name, you'll be accessing openreplay +# for exmample: domain_name: "openreplay.mycorp.org" +domain_name: "" # Nginx ssl certificates. # in cert format @@ -39,21 +39,16 @@ domain_name: "" nginx_ssl_cert_file_path: "" nginx_ssl_key_file_path: "" -# This key is used to create password for chalice api requests. -# Create a strong password. -# By default, a default key will be generated and will update the value here. -jwt_secret_key: "" - # Enable monitoring # If set, monitoring stack will be installed # including, prometheus, grafana and other core components, -# to scrape the metrics. But this will cost, additional resources (cpu and memory). +# to scrape the metrics. But this will cost, additional resources(cpu and memory). # Monitoring won't be installed on base installation. enable_monitoring: "false" # Random password for minio, # If not defined, will generate at runtime. -# Use following command to generate password +# Use following command to generate passwordwill give # `openssl rand -base64 30` minio_access_key: "" minio_secret_key: "" diff --git a/sourcemap-uploader/cli.js b/sourcemap-uploader/cli.js index c644369f3..f7b124117 100755 --- a/sourcemap-uploader/cli.js +++ b/sourcemap-uploader/cli.js @@ -18,13 +18,10 @@ parser.addArgument(['-p', '-i', '--project-key'], { // -i is depricated help: 'Project Key', required: true, }); + parser.addArgument(['-s', '--server'], { help: 'OpenReplay API server URL for upload', }); -parser.addArgument(['-l', '--log'], { - help: 'Log requests information', - action: 'storeTrue', -}); const subparsers = parser.addSubparsers({ title: 'commands', @@ -53,9 +50,7 @@ dir.addArgument(['-u', '--js-dir-url'], { // TODO: exclude in dir -const { command, api_key, project_key, server, log, ...args } = parser.parseArgs(); - -global.LOG = !!log; +const { command, api_key, project_key, server, ...args } = parser.parseArgs(); try { global.SERVER = new URL(server || "https://api.openreplay.com"); diff --git a/sourcemap-uploader/lib/readDir.js b/sourcemap-uploader/lib/readDir.js index 501a2949f..56a51a72b 100644 --- a/sourcemap-uploader/lib/readDir.js +++ b/sourcemap-uploader/lib/readDir.js @@ -3,9 +3,7 @@ const readFile = require('./readFile'); module.exports = (sourcemap_dir_path, js_dir_url) => { sourcemap_dir_path = (sourcemap_dir_path + '/').replace(/\/+/g, '/'); - if (js_dir_url[ js_dir_url.length - 1 ] !== '/') { // replace will break schema - js_dir_url += '/'; - } + js_dir_url = (js_dir_url + '/').replace(/\/+/g, '/'); return glob(sourcemap_dir_path + '**/*.map').then(sourcemap_file_paths => Promise.all( sourcemap_file_paths.map(sourcemap_file_path => diff --git a/sourcemap-uploader/lib/uploadSourcemaps.js b/sourcemap-uploader/lib/uploadSourcemaps.js index f0c3171fd..a39ce5e4d 100644 --- a/sourcemap-uploader/lib/uploadSourcemaps.js +++ b/sourcemap-uploader/lib/uploadSourcemaps.js @@ -7,21 +7,14 @@ const getUploadURLs = (api_key, project_key, js_file_urls) => } const pathPrefix = (global.SERVER.pathname + "/").replace(/\/+/g, '/'); - const options = { - method: 'PUT', - hostname: global.SERVER.host, - path: pathPrefix + `${project_key}/sourcemaps/`, - headers: { Authorization: api_key, 'Content-Type': 'application/json' }, - } - if (global.LOG) { - console.log("Request: ", options, "\nFiles: ", js_file_urls); - } const req = https.request( - options, + { + method: 'PUT', + hostname: global.SERVER.host, + path: pathPrefix + `${project_key}/sourcemaps/`, + headers: { Authorization: api_key, 'Content-Type': 'application/json' }, + }, res => { - if (global.LOG) { - console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage); - } if (res.statusCode === 403) { reject("Authorisation rejected. Please, check your API_KEY and/or PROJECT_KEY.") return @@ -31,12 +24,7 @@ const getUploadURLs = (api_key, project_key, js_file_urls) => } let data = ''; res.on('data', s => (data += s)); - res.on('end', () => { - if (global.LOG) { - console.log("Server Response: ", data) - } - resolve(JSON.parse(data).data) - }); + res.on('end', () => resolve(JSON.parse(data).data)); }, ); req.on('error', reject); @@ -58,12 +46,8 @@ const uploadSourcemap = (upload_url, body) => }, res => { if (res.statusCode !== 200) { - if (global.LOG) { - console.log("Response Code: ", res.statusCode, "\nMessage: ", res.statusMessage); - } - reject("Unable to upload. Please, contact OpenReplay support."); - return; // TODO: report per-file errors. + return; } resolve(); //res.on('end', resolve); diff --git a/sourcemap-uploader/package.json b/sourcemap-uploader/package.json index 8f0070408..2ffe6f5b7 100644 --- a/sourcemap-uploader/package.json +++ b/sourcemap-uploader/package.json @@ -1,6 +1,6 @@ { "name": "@openreplay/sourcemap-uploader", - "version": "3.0.2", + "version": "3.0.1", "description": "NPM module to upload your JS sourcemaps files to OpenReplay", "bin": "cli.js", "main": "index.js", diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 00bf42046..1ea5ac2bb 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "3.0.3", + "version": "3.0.2", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 3fe006ce8..91dc20322 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -174,24 +174,7 @@ export default class App { _start(reset: boolean): void { // TODO: return a promise instead of onStart handling if (!this.isActive) { this.isActive = true; - if (!this.worker) { - throw new Error("Stranger things: no worker found"); - } - - let pageNo: number = 0; - const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key); - if (pageNoStr != null) { - pageNo = parseInt(pageNoStr); - pageNo++; - } - sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); const startTimestamp = timestamp(); - - this.worker.postMessage({ ingestPoint: this.options.ingestPoint, pageNo, startTimestamp }); // brings delay of 10th ms? - this.observer.observe(); - this.startCallbacks.forEach((cb) => cb()); - this.ticker.start(); - window.fetch(this.options.ingestPoint + '/v1/web/start', { method: 'POST', headers: { @@ -213,7 +196,7 @@ export default class App { .then(r => { if (r.status === 200) { return r.json() - } else { // TODO: handle canceling && 403 + } else { // TODO: handle canceling throw new Error("Server error"); } }) @@ -223,14 +206,26 @@ export default class App { typeof userUUID !== 'string') { throw new Error("Incorrect server responce"); } + if (!this.worker) { + throw new Error("Stranger things: worker is not started"); + } sessionStorage.setItem(this.options.session_token_key, token); localStorage.setItem(this.options.local_uuid_key, userUUID); - if (!this.worker) { - throw new Error("Stranger things: no worker found after start request"); - } - this.worker.postMessage({ token }); - log("OpenReplay tracking started."); + let pageNo: number = 0; + const pageNoStr = sessionStorage.getItem(this.options.session_pageno_key); + if (pageNoStr != null) { + pageNo = parseInt(pageNoStr); + pageNo++; + } + sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); + + this.worker.postMessage({ ingestPoint: this.options.ingestPoint, token, pageNo, startTimestamp }); + this.observer.observe(); + this.startCallbacks.forEach((cb) => cb()); + this.ticker.start(); + log("OpenReplay tracking started."); + if (typeof this.options.onStart === 'function') { this.options.onStart({ sessionToken: token, userUUID, sessionID: token /* back compat (depricated) */ }); } @@ -259,7 +254,7 @@ export default class App { if (this.isActive) { try { if (this.worker) { - this.worker.postMessage("stop"); + this.worker.postMessage(null); } this.observer.disconnect(); this.nodes.clear(); diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index d6c8481df..def27c55a 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -85,8 +85,6 @@ export default class API { ? null : new App(options.projectKey, options.sessionToken, options); if (this.app !== null) { - Viewport(this.app); - CSSRules(this.app); Connection(this.app); Console(this.app, options); Exception(this.app, options); @@ -96,7 +94,9 @@ export default class API { Timing(this.app, options); Performance(this.app); Scroll(this.app); + Viewport(this.app); Longtasks(this.app); + CSSRules(this.app); (window as any).__OPENREPLAY__ = (window as any).__OPENREPLAY__ || this; } else { console.log("OpenReplay: broeser doesn't support API required for tracking.") diff --git a/tracker/tracker/src/webworker/index.ts b/tracker/tracker/src/webworker/index.ts index f61b7bca5..2e9d3b0e0 100644 --- a/tracker/tracker/src/webworker/index.ts +++ b/tracker/tracker/src/webworker/index.ts @@ -1,4 +1,4 @@ -import { classes, BatchMeta, Timestamp, SetPageVisibility, CreateDocument } from '../messages'; +import { classes, BatchMeta, Timestamp, SetPageVisibility } from '../messages'; import Message from '../messages/message'; import Writer from '../messages/writer'; @@ -12,11 +12,8 @@ let ingestPoint: string = ""; let token: string = ""; let pageNo: number = 0; let timestamp: number = 0; -let timeAdjustment: number = 0; let nextIndex: number = 0; -// TODO: clear logic: isEmpty here means presense of BatchMeta but absence of other messages -// BatchWriter should be abstracted -let isEmpty: boolean = true; +let isEmpty: boolean = true; function writeBatchMeta(): boolean { // TODO: move to encoder return new BatchMeta(pageNo, nextIndex, timestamp).encode(writer) @@ -70,7 +67,7 @@ function sendBatch(batch: Uint8Array):void { } function send(): void { - if (isEmpty || token === "" || ingestPoint === "") { + if (isEmpty || ingestPoint === "") { return; } const batch = writer.flush(); @@ -85,45 +82,29 @@ function send(): void { } function reset() { - ingestPoint = "" - token = "" clearInterval(sendIntervalID); writer.reset(); } let restartTimeoutID: ReturnType; -function hasTimestamp(msg: any): msg is { timestamp: number } { - return typeof msg === 'object' && typeof msg.timestamp === 'number'; -} - self.onmessage = ({ data }: MessageEvent) => { if (data === null) { send(); return; } - if (data === "stop") { - send(); - reset(); - return; - } if (!Array.isArray(data)) { - ingestPoint = data.ingestPoint || ingestPoint; - token = data.token || token; - pageNo = data.pageNo || pageNo; - timestamp = data.startTimestamp || timestamp; - timeAdjustment = data.timeAdjustment || timeAdjustment; - if (writer.isEmpty()) { - writeBatchMeta(); - } - if (sendIntervalID == null) { - sendIntervalID = setInterval(send, SEND_INTERVAL); - } + reset(); + ingestPoint = data.ingestPoint; + token = data.token; + pageNo = data.pageNo; + timestamp = data.startTimestamp; + writeBatchMeta(); + sendIntervalID = setInterval(send, SEND_INTERVAL); return; } data.forEach((data: any) => { const message: Message = new (classes.get(data._id))(); - Object.assign(message, data); if (message instanceof Timestamp) { timestamp = (message).timestamp; @@ -135,6 +116,7 @@ self.onmessage = ({ data }: MessageEvent) => { } } + Object.assign(message, data); writer.checkpoint(); nextIndex++; if (message.encode(writer)) { From 30a4a21b7e8bd6bc6fb49cc91446ba28d8896f28 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Sat, 22 May 2021 01:09:12 +0530 Subject: [PATCH 4/9] Update vars.yaml change: version number to 1.0.0 --- scripts/helm/vars.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helm/vars.yaml b/scripts/helm/vars.yaml index bdade016c..dae690403 100644 --- a/scripts/helm/vars.yaml +++ b/scripts/helm/vars.yaml @@ -17,7 +17,7 @@ kubeconfig_path: /home/rajeshr/.kube/config docker_registry_username: "" docker_registry_password: "" docker_registry_url: "rg.fr-par.scw.cloud/foss" -image_tag: "latest" +image_tag: "1.0.0" # This is an optional field. If you want to use proper ssl, then it's mandatory # Using which domain name, you'll be accessing OpenReplay From 32268ae815154eb2c35c456c474b495d95577edd Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Sat, 22 May 2021 01:28:19 +0530 Subject: [PATCH 5/9] Update vars.yaml change: kube path empty --- scripts/helm/vars.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/helm/vars.yaml b/scripts/helm/vars.yaml index dae690403..c12f92c2c 100644 --- a/scripts/helm/vars.yaml +++ b/scripts/helm/vars.yaml @@ -2,12 +2,12 @@ ## Mandatory Fields. ################### -# Give the path of the kubeconfig_path: /home/rajeshr/.kube/config +# Give the path of the kubeconfig_path: /home/user/.kube/config # we can access the kubernetes cluster. # Give absolute file path. # Use following command to get the full file path # `readlink -f ` -kubeconfig_path: /home/rajeshr/.kube/config +kubeconfig_path: "" ################### ## Optional Fields. From 90dae2beee3b7d677ef6cf6b7b37271dbe0c1d9e Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Sat, 22 May 2021 02:11:11 +0530 Subject: [PATCH 6/9] change: typo --- scripts/helm/management/ecr_cron.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/helm/management/ecr_cron.yaml b/scripts/helm/management/ecr_cron.yaml index 843231fa4..fdc559979 100644 --- a/scripts/helm/management/ecr_cron.yaml +++ b/scripts/helm/management/ecr_cron.yaml @@ -51,9 +51,9 @@ spec: - name: AWS_DEFAULT_REGION value: eu-central-1 - name: AWS_SECRET_ACCESS_KEY - value: 6FrL2pMub/XuYla2O1V+HJHkCrWbDUIAzOMAVEzm + value: QWERTYQWERTYQWERTY - name: AWS_ACCESS_KEY_ID - value: AKIA6RAO3SOP4CV7SOV4 + value: QWERTYQWERTYQWERTY command: - /bin/bash - -c From dcaa622b2b6d3429d7c97fbaf9e2d8823e28781c Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Sat, 22 May 2021 02:12:09 +0530 Subject: [PATCH 7/9] change: show webhooks menu item --- .../app/components/Client/PreferencesMenu/PreferencesMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js index 9dc0e2ee7..e7436e854 100644 --- a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js +++ b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js @@ -47,7 +47,7 @@ function PreferencesMenu({ activeTab, appearance, history }) { />
- { appearance.tests && appearance.runs && + {
Date: Sat, 22 May 2021 01:38:37 +0200 Subject: [PATCH 8/9] fix: adding v to image tag --- scripts/helm/vars.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/helm/vars.yaml b/scripts/helm/vars.yaml index c12f92c2c..6c3e081ac 100644 --- a/scripts/helm/vars.yaml +++ b/scripts/helm/vars.yaml @@ -17,7 +17,7 @@ kubeconfig_path: "" docker_registry_username: "" docker_registry_password: "" docker_registry_url: "rg.fr-par.scw.cloud/foss" -image_tag: "1.0.0" +image_tag: "v1.0.0" # This is an optional field. If you want to use proper ssl, then it's mandatory # Using which domain name, you'll be accessing OpenReplay From 011319439785a3b01758de681f5ede7ed6cd047e Mon Sep 17 00:00:00 2001 From: Rajesh Rajendran Date: Sat, 22 May 2021 05:14:49 +0530 Subject: [PATCH 9/9] ci(deployment): use script to push the image Signed-off-by: Rajesh Rajendran --- .github/workflows/workers.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/workers.yaml b/.github/workflows/workers.yaml index 9055a7211..8e1cbb9dc 100644 --- a/.github/workflows/workers.yaml +++ b/.github/workflows/workers.yaml @@ -56,8 +56,7 @@ jobs: for image in $(cat images_to_build.txt); do echo "Bulding $image" - bash -x ./build.sh skip $image - docker push $DOCKER_REPO/$image:$IMAGE_TAG + PUSH_IMAGE=1 bash -x ./build.sh skip $image echo "::set-output name=image::$DOCKER_REPO/$image:$IMAGE_TAG" done